Переменные по умолчанию класса Python являются объектами класса?

Возможный дубликат:
"Наименьшее удивление" в Python: аргумент аргумента, разрешаемый аргументом

Я писал код сегодня днем ​​и наткнулся на ошибку в моем коде. Я заметил, что значения по умолчанию для одного из моих вновь созданных объектов переносятся с другого объекта! Например:

class One(object):
    def __init__(self, my_list=[]):
        self.my_list = my_list

one1 = One()
print(one1.my_list)
[] # empty list, what you'd expect.

one1.my_list.append('hi')
print(one1.my_list)
['hi'] # list with the new value in it, what you'd expect.

one2 = One()
print(one2.my_list)
['hi'] # Hey! It saved the variable from the other One!

Поэтому я знаю, что это можно решить, сделав это:

class One(object):
    def __init__(self, my_list=None):
        self.my_list = my_list if my_list is not None else []

Что я хотел бы знать... Почему? Почему классы Python структурированы так, что значения по умолчанию сохраняются в экземплярах класса?

Спасибо заранее!

Ответы

Ответ 1

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

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

>>> class One(object):
...     def __init__(self, my_list=[]):
...         self.my_list = my_list
...
>>> alist = ['hello']
>>> one1 = One(alist)
>>> alist.append('world')
>>> one2 = One(alist)
>>> 
>>> print(one1.my_list) # Huh? This isn't what I initialised one1 with!
['hello', 'world']
>>> print(one2.my_list) # At least this one okay...
['hello', 'world']
>>> del alist[0]
>>> print one2.my_list # What the hell? I just modified a local variable and a class instance somewhere else got changed?
['world']

9 раз из 10, если вы обнаружите, что достигли "шаблона" использования None в качестве значения по умолчанию и с помощью if value is None: value = default, этого не должно быть. Вы должны просто не изменять свои аргументы! Аргументы должны not рассматриваться как принадлежащие вызываемому коду, если только это явно не задокументировано как право собственности на них.

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

class One(object):
    def __init__(self, my_list=[])
        self.my_list = list(my_list)

Теперь вы инициализируете данные своего класса из списка, представленного как вход, вместо того, чтобы владеть уже существующим списком. Нет никакой опасности, что два отдельных экземпляра в конечном итоге будут использовать один и тот же список, а также то, что список разделяется с переменной в вызывающем абоненте, которую вызывающий может продолжать использовать. Он также имеет приятный эффект, что ваши абоненты могут предоставлять кортежи, генераторы, строки, наборы, словари, домашние шаблонные итерационные классы и т.д., И вы знаете, что все еще можете рассчитывать на self.my_list, имеющий метод append, потому что вы сделал это сам.

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

Другая проблема заключается в том, что если my_list может быть очень большим, копия может быть дорогостоящей. Там вам нужно сделать компромисс. В таком случае, может быть, лучше всего использовать список переданных в конце, и использовать шаблон if my_list is None: my_list = [], чтобы все экземпляры по умолчанию, связанные с одним списком. Но если вы это сделаете, вам нужно прояснить, будь то в документации или имени класса, что вызывающие лица отказываются от владения списками, которые они используют для инициализации экземпляра. Или, если вы действительно хотите создавать список исключительно с целью обертывания в экземпляре One, возможно, вам стоит выяснить, как инкапсулировать создание списка внутри инициализации One, а не создавать это сначала; в конце концов, это действительно часть экземпляра, а не инициализирующее значение. Иногда это недостаточно гибко.

И иногда вы действительно честно хотите, чтобы наложение псевдонимов продолжалось, и передайте код, изменяя значения, к которым у них есть доступ. Тем не менее, я думаю, очень тяжело, прежде чем я посвящу себя такому дизайну. И это удивит других (и вы, когда вернетесь к коду через X месяцев), так что документация - ваш друг!

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

Ответ 2

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

Чтобы исправить это, попробуйте:

def __init__(self, my_list=None):
    if my_list is None:
        my_list = []
    self.my_list = my_list

Ответ 4

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

Ответ 5

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

Ответ 6

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

Пример:

>>> class one:
...     myList = []
...
>>>
>>> one1 = one()
>>> one1.myList
[]
>>> one2 = one()
>>> one2.myList.append("Hello Thar!")
>>>
>>> one1.myList
['Hello Thar!']
>>>

Обратите внимание, что не только значение myList сохраняется, но каждый экземпляр из myList указывает на тот же список.

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

Это ухудшилось, поскольку это не просто аргумент по умолчанию. Вы не можете просто поместить myList в определение класса, его нужно установить равным чему-либо, хотя независимо от того, что он установлен равным, оценивается только один раз.

Решение, по крайней мере для меня, состояло в том, чтобы просто создать всю переменную класса внутри __init__.