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 []


Вот, собственно, и все :)


Тэги: ,

0
Разных мыслей по теме

Выразите свои мысли по теме.