Статьи

Изменяемая ловушка по умолчанию … И как ее избежать

Перейти к началу серии

Примечание: примеры написаны на языке Python 2.x, но основной смысл поста относится ко всем версиям Python.

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

Изменяемые значения по умолчанию для аргументов функций

Получается, когда вы кодируете значения по умолчанию для аргументов функции или метода. Вот пример для функции с именем foobar:

def foobar(arg_string = "abc", arg_list = []):
    ...

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

Будет создан новый строковый объект, содержащий «abc» и связанный с именем переменной «arg_string». Будет создан новый пустой объект списка, связанный с именем переменной «arg_list». Вкратце, если аргументы опущены вызывающей стороной, foobar всегда получит «abc» и [] в своих аргументах.

Это, однако, не то, что произойдет. Вот почему

Объекты, предоставляющие значения по умолчанию, не создаются во время вызова foobar. Они создаются в то время, когда выполняется оператор, определяющий функцию . (См. Обсуждение в разделе « Аргументы по умолчанию в Python: две простые ошибки :« Выражения в аргументах по умолчанию вычисляются при определении функции, а не при ее вызове ».)

Если, например, foobar содержится в модуле с именем foo_module, то оператор, который определяет foobar, вероятно, будет выполнен во время импорта foo_module.

Когда выполняется оператор def, который создает foobar:

  • Создается новый объект функции, связанный с именем foobar и сохраняемый в пространстве имен foo_module.
  • В объекте функции foobar для каждого аргумента со значением по умолчанию создается объект для хранения объекта по умолчанию. В случае foobar строковый объект, содержащий «abc», создается в качестве значения по умолчанию для аргумента arg_string, а пустой объект списка создается как значение по умолчанию для аргумента arg_list.

После этого всякий раз, когда foobar вызывается без аргументов, arg_string будет привязан к строковому объекту по умолчанию, а arg_list будет привязан к объекту списка по умолчанию. В таком случае arg_string всегда будет «abc», но arg_list может быть или не быть пустым списком. Вот почему

Существует принципиальное различие между строковым объектом и объектом списка. Строковый объект является неизменным, тогда как объект списка является изменяемым. Это означает, что значение по умолчанию для arg_string никогда не может быть изменено, но значение по умолчанию для arg_list может быть изменено.

Давайте посмотрим, как можно изменить значение по умолчанию для arg_list. Вот программа. Это вызывает foobar четыре раза. Каждый раз, когда вызывается foobar, он отображает значения полученных им аргументов, а затем добавляет что-то к каждому из аргументов.

def foobar(arg_string="abc", arg_list = []): 
    print arg_string, arg_list 
    arg_string = arg_string + "xyz" 
    arg_list.append("F")

for i in range(4): 
    foobar()

Выход этой программы:

abc [] 
abc ['F'] 
abc ['F', 'F'] 
abc ['F', 'F', 'F']

Как видите, в первый раз аргумент имеет именно то значение по умолчанию, которое мы ожидаем. На втором и всех последующих проходах значение arg_string остается неизменным — именно то, что мы ожидаем от неизменяемого объекта. Линия

arg_string = arg_string + "xyz"

создает новый объект — строку «abcxyz» — и связывает имя «arg_string» с этим новым объектом, но не меняет объект по умолчанию для аргумента arg_string.

Но дело обстоит иначе с arg_list, значением которого является список — изменяемый объект. При каждом проходе мы добавляем участника в список, и этот список увеличивается. При четвертом вызове foobar, то есть после трех предыдущих вызовов, arg_list содержит три члена.

Решение

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

Но для большинства программистов — особенно начинающих Pythonistas — такое поведение является ошибкой. Поэтому для большинства случаев мы принимаем следующие правила.

  1. Никогда не используйте изменяемый объект, то есть список, словарь или экземпляр класса, в качестве значения аргумента по умолчанию.
  2. Игнорируйте правило 1, только если вы действительно, действительно , ДЕЙСТВИТЕЛЬНО знаете, что делаете.

Итак … мы планируем всегда следовать правилу № 1. Теперь вопрос в том, как это сделать … как написать код в foobar, чтобы получить поведение, которое мы хотим.

К счастью, решение является простым. Изменяемые объекты, используемые как значения по умолчанию, заменяются на None, а затем аргументы проверяются на None.

def foobar(arg_string="abc", arg_list = None): 
    if arg_list is None: arg_list = [] 
    ...

Другое решение, которое вы иногда увидите, это:

def foobar(arg_string="abc", arg_list=None): 
    arg_list = arg_list or [] 
    ...

Это решение, однако,
не эквивалентно первому, и его следует избегать. Смотрите
Обучение Python с. 123 для обсуждения различий.
Спасибо Ллойд Квам за то, что указал мне на это.

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

Изменяемые значения по умолчанию для аргументов метода

Теперь давайте посмотрим, как изменяемые аргументы, которые получает gotcha, когда для метода класса задается изменяемое значение по умолчанию для одного из аргументов. Вот полная программа.

# (1) define a class for company employees 
class Employee:
    def __init__ (self, arg_name, arg_dependents=[]): 
        # an employee has two attributes: a name, and a list of his dependents 
        self.name = arg_name 
        self.dependents = arg_dependents
    
    def addDependent(self, arg_name): 
        # an employee can add a dependent by getting married or having a baby 
        self.dependents.append(arg_name)
    
    def show(self): 
        print
        print "My name is.......: ", self.name 
        print "My dependents are: ", str(self.dependents)
#--------------------------------------------------- 
#   main routine -- hire employees for the company 
#---------------------------------------------------

# (2) hire a married employee, with dependents 
joe = Employee("Joe Smith", ["Sarah Smith", "Suzy Smith"])

# (3) hire a couple of unmarried employess, without dependents 
mike = Employee("Michael Nesmith") 
barb = Employee("Barbara Bush")

# (4) mike gets married and acquires a dependent 
mike.addDependent("Nancy Nesmith")

# (5) now have our employees tell us about themselves 
joe.show() 
mike.show() 
barb.show()

Давайте посмотрим, что происходит при запуске этой программы.

  1. Сначала выполняется код, который определяет класс Employee.
  2. Тогда мы нанимаем Джо. У Джо есть два иждивенца, поэтому этот факт записывается во время создания объекта Джо.
  3. Далее мы нанимаем Майка и Барб.
  4. Тогда Майк приобретает иждивенца.
  5. Наконец, последние три утверждения программы просят каждого сотрудника рассказать нам о себе.

Вот результат.

My name is.......:  Joe Smith 
My dependents are:  ['Sarah Smith', 'Suzy Smith']

My name is.......:  Michael Nesmith 
My dependents are:  ['Nancy Nesmith']

My name is.......:  Barbara Bush 
My dependents are:  ['Nancy Nesmith']

Джо просто отлично. Но каким-то образом, когда Майк приобрел Нэнси в качестве своего иждивенца, Барб также приобрел Нэнси в качестве иждивенца. Это, конечно, неправильно. И теперь мы в состоянии понять, что заставляет программу вести себя таким образом.

Когда выполняется код, который определяет класс Employee, создаются объекты для определения класса, определения методов и значения по умолчанию для каждого аргумента. У конструктора есть аргумент arg_dependents, значением по умолчанию которого является пустой список, поэтому пустой объект списка создается и присоединяется к методу __init__ в качестве значения по умолчанию для arg_dependents.

Когда мы нанимаем Джо, у него уже есть список зависимых лиц, который передается в конструктор Employee — поэтому атрибут arg_dependents не использует пустой объект списка по умолчанию.

Далее мы нанимаем Майка и Барб. Поскольку у них нет зависимых, используется значение по умолчанию для arg_dependents. Помните — это пустой объект списка, который был создан при запуске кода, который определил класс Employee. Таким образом, в обоих случаях пустой список связан с аргументом arg_dependents, а затем — опять же в обоих случаях — с атрибутом self.dependents. Результатом является то , что после того, как Майк и Барб нанимаются, то self.dependents атрибутом как Майк и Барб указывают на тот же объект — по умолчанию пустой объект списка.

Когда Майкл выходит замуж, и Нэнси Несмит добавляется в его список self.dependents, Барб также получает Нэнси в качестве зависимой, поскольку имя переменной Barb.am привязывается к тому же объекту списка, что и имя переменной Майка self.dependents.

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

И что именно поэтому вы не должны никогда, никогда , НИКОГДА не использовать список или словарь в качестве значения по умолчанию для аргумента метода класса. Если, конечно, вы действительно, действительно , действительно знаете, что делаете.