7 октября 2008. Питонья подножка: list в качестве аргумента по умолчанию
По долгу службы столкнулся с интересным явлением в Python (речь идет о версии 2.5, но подобное поведение сохраняют и младшие версии). В моем классе была явная ошибка и экземпляры класса как-то странно инициализировались. Я довольно много времени провел за дебаггингом, пока однозначно не решил, что виной всему ошибка в интерпретаторе. И был, как оказалось, по-своему прав.
Проиллюстрирую эту особенность языка на примере простого класса:
class Test():
def __init__(self,text="",arr=[],num=0):
self.text=text
self.arr=arr
self.num=num
def compile (self):
output=self.text +" - "
output+=str(self.arr) +" - "
output+=str(self.num)
return output
def __str__(self):
return self.compile()
Класс довольно простой и объяснять ничего из написанного выше, я думаю, не нужно. Поэтому сразу перейдем к эксперименту. Попробуем запустить следующие выражения на исполнение:
a=Test("something", ['sin','sex','salvation'], 8)
b=Test()
c=Test("",[])
d=Test()
d.num+=1
e=Test()
e.arr.append("appendedStuff")
И распечатаем их (полученный вывод каждого выражения указан в комментариях):
print a # something - ['sin', 'sex', 'salvation'] - 8
print b # - ['appendedStuff'] - 0
print c # - [] - 0
print d # - ['appendedStuff'] - 1
print e # - ['appendedStuff'] - 0
Немного не то, что мы ожидали, верно? Элемент массива мы присоединяли к свойству экземпляра e, а присоединился он ко всем, кому массив явно не был указан (не стандартным значением в определении функции).
Многие считают это неправильным поведением интерпретатора, большая же часть находит этот феномен совершенно нормальным и просто живет с ним.
Почему же так происходит? Мы ведь в def указали стандартное значение для arr, равное пустому массиву ([]). Почему же обновляются все экземпляры, у которых arr было инициализировано как стандартное значение по умолчанию? А дело все в том, что def-выражение исполняется только один раз, а не каждый раз когда мы вызываем функцию. В памяти просто сохраняются объекты пустого массива arr, строки text и числа num. Но почему же тогда, возможно спросите вы, так происходит только с массивом? Почему строки инициализируются как новые пустые строки и число тоже не делится между экземплярами, которым принудительно не было указано значение числа? А все просто: массив (как и словарь, кстати), в отличие от чисел и строк присваивается переменным не по значению, а по ссылке. Поэтому каждый раз, когда вы инициализируете объект массива по умолчанию, вы получаете ссылку на массив, который, возможно, был ранее изменен предшествующими экземплярами.
В общем случае, подобные казусы избегают вполне очевидным способом:
class Test():
def __init__(self, arr=None):
if not arr:
self.arr=[]
else:
self.arr=arr
Можно записать более лаконично, используя питоновскую логику коротких заключений (short circuit logic):
class Test():
def __init__(self, arr=None):
self.arr = arr or []
Вот, собственно, и все :)