Почему списки Python не создают копии аргументов, поэтому реальные объекты не могут быть видоизменены?

1

Возможно, я слишком много пил из функционального программирования Kool Aid, но такое поведение в представлении списка кажется плохим выбором дизайна:

>>> d = [1, 2, 3, 4, 5]
>>> [d.pop() for _ in range(len(d))]
[5, 4, 3, 2, 1]
>>> d
[]

Почему d не копируется, а затем скопированная версия с лексической областью не изменена (а затем потеряна)? Точка понимания списка кажется, что она должна возвращать нужный список, а не возвращать список и тихо мутировать какой-то другой объект за кулисами. Разрушение d несколько неявное, что кажется непутоническим. Есть ли хороший вариант для этого?

Почему выгодно, чтобы список comps вел себя точно так же, как и для циклов, вместо того, чтобы больше походить на функции (с функционального языка с локальной областью)?

  • 4
    как придира: for _ in range(len(d)) действительно непитонический
  • 1
    @msw - согласен. Я никогда не видел эту конструкцию раньше. Использование _ в качестве имени переменной для заполнителя - это то, что я видел только в функциональном программировании.
Показать ещё 7 комментариев
Теги:
functional-programming
list-comprehension

14 ответов

21
Лучший ответ

Python никогда не копирует, если вы специально не попросите его сделать копию. Это совершенно простое, понятное и понятное правило. Помещение исключений и различий на нем, таких как "за исключением следующих обстоятельств в понимании списка...", было бы глупо: если бы дизайн Python когда-либо находился под управлением кого-то с такими сумасшедшими идеями, Python был бы больным, искаженный, полуразрушенный язык не стоит изучать. Спасибо за то, что я снова обрадовался осознанию, что это определенно не случай!

Вам нужна копия? Сделайте копию! Это всегда решение на Python, когда вы предпочитаете накладные расходы на копирование, потому что вам нужно выполнить некоторые изменения, которые не должны отражаться в оригинале. То есть, в чистом подходе, вы бы сделали

dcopy = list(d)
[dcopy.pop() for _ in range(len(d))]

Если вы очень заинтересованы в том, чтобы иметь все в одном выражении, вы можете, хотя это, возможно, не код, который бы назвал ровно "чистым":

[dcopy.pop() for dcopy in [list(d)] for _ in range(len(d))]

т.е. обычный трюк используется, когда нужно было бы свернуть присваивание в понимание списка (добавьте предложение for, причем "управляющая переменная" будет именем, которое вы хотите назначить, и "loop" "находится над последовательностью с одним элементом значения, которое вы хотите назначить).

Функциональные языки никогда не мутируют данные, поэтому они не делают копии (и не нужны). Python не является функциональным языком, но, конечно, в Python есть много вещей, которые вы можете сделать "функциональным способом", и часто это лучший способ. Например, гораздо лучшая замена для вашего понимания списка (гарантированно иметь одинаковые результаты и не влиять на d, а значительно быстрее, более кратким и чистым):

d[::-1]

(AKA "Марсианский смайлик", за мою жену Анну;-). Нарезка (не slice присваивание, которая является другой операцией) всегда выполняет копию в ядре Python (язык и стандартную библиотеку), хотя, конечно, необязательно в независимо разработанных сторонних модулях, таких как популярный numpy (который предпочитает видеть срез как "вид" на оригинале numpy.array).

  • 0
    Это вовсе не отвечает на вопрос OPs и имеет только периферийное отношение.
  • 0
    Спасибо Алекс, это имеет смысл. Теперь я вижу баланс, с которым столкнулся Python (поправьте меня, если я ошибаюсь): мутирование объектов в списочных компиляциях может привести к неуклюжему неявному коду (как мой пример попытался проиллюстрировать), но любой неявный код, придуманный кодером, не может прийти близко к ущербу, нанесенному неявным созданием копий в одних местах, но не в других с помощью основного языка.
Показать ещё 20 комментариев
7

В этом выражении:

[d.pop() for _ in range(len(d))]

какую переменную вы хотите неявно скопировать или скопировать? Единственная переменная здесь с любым специальным статусом в понимании - _, который не является тем, который вы хотите защитить.

Я не вижу, как вы могли бы использовать семантику понимания списков, которая могла бы как-то идентифицировать все изменяемые переменные и каким-то образом их неявным образом скопировать. Или знать, что .pop() изменяет свой объект?

Вы упоминаете функциональные языки, но они выполняют то, что вы хотите, делая все переменные неизменными. Python просто не разработан таким образом.

5

Почему это должно создать (возможно, очень дорогостоящую) копию, когда идиоматический код не будет иметь побочных эффектов? И почему бы (редкие, но существующие) случаи использования, когда желательны побочные эффекты (и нормально) запрещено?

Python - это, прежде всего, императивный язык. Мотивируемое состояние не только разрешено, но и важно - да, списки понятий предназначены для того, чтобы быть чистыми, но если бы это было соблюдено, это было бы асинхронно с семантикой остальной части языка. Итак, d.pop() мутирует d, но только если это не в понимании списка и если звезды правы? Это было бы бессмысленно. Вы свободны (и предположили) не использовать его, но никто не собирается устанавливать больше правил в камне и сделать функцию более сложной - идиоматический код (и что единственный код, который должен заботиться о ;)), не нужно такое правило. Он делает это в любом случае и при необходимости делает иначе.

4

d не копируется, потому что вы не копировали его, а списки изменяемы, а pop выполняет управление списком по контракту.

Если бы вы использовали кортеж, он бы не мутировал:

>>> x = (1, 2, 3, 4)
>>> type(x)
<type 'tuple'>
>>> x.pop()
AttributeError: 'tuple' object has no attribute 'pop'
  • 0
    Я согласен, что кортежи решают проблему ... и не копировать d не потому, что я не знаю о копировании объектов в Python. Мне просто интересно, почему выгодно иметь списочные компы, которые ведут себя точно так же, как for циклов, а не ведут себя как функции (с локальной областью видимости).
  • 1
    Даже если она была передана функции, она была бы ссылкой, поэтому любые вызываемые ей методы по-прежнему изменяли исходный объект.
Показать ещё 4 комментария
4

Почему выгодно иметь список comps ведут себя точно так же, как для циклов,

Потому что это наименее удивительно.

вместо того, чтобы вести себя как функции (с локальной областью)?

О чем ты говоришь? Функции могут мутировать свои аргументы:

>>> def mutate(d):
...     d.pop()
... 
>>> d = [1, 2, 3, 4, 5]
>>> mutate(d)
>>> d
[1, 2, 3, 4]

Я не вижу несогласованности вообще.

То, что вы, похоже, не узнаете, это то, что Python не является функциональным языком. Это императивный язык, который имеет несколько функциональных функций. Python позволяет изменять объекты. Если вы не хотите, чтобы они мутировали, просто не вызывайте методы типа list.pop, которые документированы для их изменения.

3

Кажется, вы неправильно понимаете функции:

def fun(lst):
    for _ in range(len(lst)):
        lst.pop()

будет иметь тот же эффект, что и

(lst.pop() for _ in range(len(lst)))

Это потому, что lst не является "списком", а ссылкой на него. Когда вы передаете эту ссылку, она остается указателем на тот же список. Если вы хотите скопировать список, просто используйте lst[:]. Если вы хотите скопировать его содержимое, используйте copy.deepcopy из модуля copy.

  • 0
    Функции в функциональном языке программирования.
  • 0
    Мой предпочтительный способ скопировать список - сделать lst[:] .
Показать ещё 2 комментария
2

Если я думаю о вашем "понимании списка", он сам по себе "неспокойный". Вы ссылаетесь на d.pop() на d, и вы фактически не ссылаетесь на список в "понимании списка". Таким образом, на самом деле вы злоупотребляете пониманием списка для простого цикла for, используя измененную переменную '_', которую вы не используете для того, что вы на самом деле делаете или собираете в этом выражении: 'd.pop()'. Метод pop() применяется к d. И не имеет ничего общего с _ или с диапазоном (len (d)), который создает только другой список, используя длину d. Методы списков мутируют сам список. Таким образом, "логично", что d изменяется с помощью применения этого метода.

Как ответил Алекс Мартелли, d [:: - 1] делает то, что это выражение должно делать в "питоническом" способе.

2

Why is d not copied, and then the copied lexically-scoped version not mutated (and then lost)?

Поскольку python является объектно-ориентированным языком программирования, и это будет невероятно плохая идея. Все это объект.

Что заставляет вас думать, что можно создать "лексически скопированные копии" произвольных объектов?

Возможность вызова pop на объект не означает, что его можно скопировать. Он может получить доступ к дескриптору файла, сетевому сокету или очереди команд для космического зонда, вращающегося вокруг Сатурна.


Why is it advantageous to have list comps behave exactly like for loops, rather than behave more like functions (with local scope)?

  • Потому что он создает сжатый, читаемый код.
  • Как отмечали все остальные, функции не работают так, как вы думаете, что они делают. Они также не делают эту "лексически скопированную копию". Я думаю, что вы запутались в локальном задании.

Я рекомендую прочитать статьи здесь: http://www.cafepy.com/article/python_types_and_objects/python_types_and_objects.html

Они очень информативны в отношении того, как работает python.

  • 0
    Будет ли у модератора благодать, чтобы объяснить, что не так с этим ответом?
  • 0
    Здесь много чего происходит: P
2

Вы говорите, что хотите, чтобы методы велись по-разному в зависимости от контекста исполнения? Звучит очень опасно для меня.

Хорошо, что метод, называемый объектом Python, всегда будет делать то же самое - я был бы обеспокоен использованием языка, где вызов метода внутри какой-то синтаксической конструкции приводил к тому, что он вел себя по-другому.

  • 1
    Не методы ... объект, на который он действует, отсюда и фраза в лексической области.
  • 0
    Аминь! Я ненавижу такие языки, как MATLAB, где поведение f в x, y = f(z) может зависеть от x и y.
Показать ещё 1 комментарий
2

Всегда существует способ не мутировать list при использовании списков. Но вы можете мутировать list тоже, если это то, что вы хотите. В вашем случае, например:

c = [a for a in reversed(d)]
c = d[::-1]
c = [d[a] for a in xrange(len(d)-1, -1, -1)]

все вы получите обратную копию list. В то время как

d.reverse()

изменит list на место.

2

Конечно, есть (например, обработка очереди). Но ясно, что вы показали, это не одно.

Python, как и любой язык программирования, который стоит использовать, делает именно то, что вы ему рассказываете, не больше и не меньше. Если вы хотите, чтобы он сделал что-то еще, тогда скажите ему сделать что-то еще.

  • 0
    Это действительно плохая форма, чтобы обходить моддингом без объяснения причин. На момент написания этого, три ответа были изменены (включая этот ответ), и не было никаких объяснений.
2

Python не является функциональным языком и никогда не будет. Поэтому, когда вы используете понимание списка, вы можете изменить состояние несвязанных структур данных. Это не может быть разумно предотвращено, и такие меры, как тот, который вы описали, помогут только конкретному случаю, который вы выделили.

В общем, это зависит от человека, использующего понимание списков для написания кода, который довольно легко понять и как можно более свободный от побочных эффектов. Я считаю, что код, который вы опубликовали, является плохим стилем программирования и тупым способом создания обратного списка, когда существует list.reverse.

Хотя, я полагаю, если список, который вы выходите из этого примера, представляет собой очередь, которую можно добавить в код обработки очереди (то есть нечто гораздо более сложное, чем d.pop()) или другим потоком, затем код это своего рода разумный способ сделать что-то. Хотя я действительно думаю, что это должен быть цикл, а не понимание списка.

  • 0
    Даун мод без комментариев. Woohoo!
  • 2
    Извините, модом был я. Код был намеренно тривиальным и плохим, но вы предлагаете list.reverse. Это дало мне ощущение, что вы не читали мой вопрос - как я говорил много раз, я не пытался создать реверсер списка, а фактически спрашивал о выборе дизайна Python.
Показать ещё 1 комментарий
1

dan04 имеет правильный ответ, а для сравнения здесь немного Haskell...

[print s | s <- ["foo", "bar", "baz"]]

Здесь у вас есть побочный эффект (печать) прямо в середине понимания списка в Haskell. Haskell ленив, поэтому вы должны явно запустить его с помощью sequence_:

main = sequence_ [print s | s <- ["foo", "bar", "baz"]]

Но это практически то же самое, что и Python

_ = list(print(s) for s in ["foo", "baz", "baz"])

За исключением того, что Haskell обертывает идиому _ = list... в функции с именем sequence_.

Смысл списка не имеет ничего общего с предотвращением побочных эффектов. Это просто неожиданно, чтобы увидеть их там. И вы вряд ли можете получить больше "функциональных", чем Haskell, поэтому ответ "Python - это императивный язык" в этом контексте не совсем прав.

1

Я не уверен, что вы спрашиваете. Возможно, вы спрашиваете, должен ли d.pop() возвращать копию вместо того, чтобы мутировать себя. (Это не имеет ничего общего с пониманием списков.) Ответ на этот вопрос, конечно, не таков: это превратило бы его из операции O (1) в O (n), что было бы катастрофическим недостатком дизайна.

Что касается понимания списков, ничто не останавливает выражения внутри них от вызова функций с побочными эффектами. Это не ошибка списков, если вы их неправильно используете. Это не проблема языкового дизайна, чтобы заставить программистов не путать вещи.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню