Параметры по умолчанию в Python
Недавно в программе столкнулся с ошибкой, которую хотя и искал не долго, но уж больно меня удивила. Точнее удивительным мне показалось поведение Python в данном случае. Это касалось параметров по умолчанию в функциях. И я решил поэкспериментировать с таким поведением. Далее я и описываю эти эксперименты.
Рассмотрим простой пример.
class A:
def __init__ (self, param):
self.param = param
def test1():
print ("\n=== Test1 ===")
a1 = A([])
a2 = A([])
print ('1, a1: ' + str (len (a1.param)))
print ('1, a2: ' + str (len (a2.param)))
a1.param.append ("new element")
print ('2, a1: ' + str (len (a1.param)))
print ('2, a2: ' + str (len (a2.param)))
if (__name__ == __main__):
test1()
Здесь нет ничего необычного, создаем два экземпляра класса, в конструкторы которых передаем по пустому списку. Убеждаемся, что списки пусты, а после этого в список одного из экземпляров добавляем новый элемент и смотрим размеры обоих списков. Результат работы этого скрипта выглядит так:
=== Test1 === 1, a1: 0 1, a2: 0 2, a1: 1 2, a2: 0
Допустим теперь, что мы решили себе облегчить жизнь и добавили в конструктор параметр по умолчанию, который представляет собой тоже пустой список. Новый скрипт теперь выглядит так:
class A:
def __init__ (self, param = []):
self.param = param
def test2():
print ("\n=== Test2 ===")
a1 = A()
a2 = A()
print ('1, a1: ' + str (len (a1.param)))
print ('1, a2: ' + str (len (a2.param)))
a1.param.append ("new element")
print ('2, a1: ' + str (len (a1.param)))
print ('2, a2: ' + str (len (a2.param)))
if (__name__ == __main__):
test2()
Запускаем на выполнение этот скрипт и получаем следующий результат:
=== Test2 === 1, a1: 0 1, a2: 0 2, a1: 1 2, a2: 1
У нас получилось, что при добавлении элемента в первый экземпляр класса A, элемент добавился так же и во второй, а, если быть точнее, у обоих экземпляров класса снутри хранится ссылка на один и тот же список. А такое может быть только в том случае, если список для значения по умолчанию параметра конструктора создавался только один раз, а при вызове конструктора класса во время создания второго экземпляра просто передавался "старый" список. Т.е. список не создается каждый раз при вызове конструктора.
Посмотрим, что происходит при создании значения по умолчанию не списка, а другого класса. Для этого напишем следующий пример:
class B:
def __init__ (self):
pass
class C:
def __init__ (self, param = B()):
self.param = param
def test3 ():
print ("\n=== Test3 ===")
print ("= C() =")
c1 = C()
c2 = C()
print c1.param
print c2.param
print ("= C(B()) =")
c3 = C(B())
c4 = C(B())
print c3.param
print c4.param
if (__name__ == __main__):
test3()
Этот пример, в принципе, ничем не отличается от предыдущего кроме того, что в качестве параметра конструктора класса C у нас не список, а экземпляр класса B. Также для большей убедительности, что мы имеем дело именно с одним и тем же объектом при использовании параметра по умолчанию, выведем дескрипторы объектов.
=== Test3 === = C() = <__main__.B instance at 0x00AF63A0> <__main__.B instance at 0x00AF63A0> = C(B()) = <__main__.B instance at 0x00AF6418> <__main__.B instance at 0x00AF6468>
Как и ожидалось, при создании экземплаторв класса с параметром по умолчанию, значение дескрипторов члена param оказалось одинаковым, а при явной передаче параметров разным.
Осталось выяснить когда именно создается параметр по умолчанию. Для этого объединим все предыдущие примеры:
class A:
def __init__ (self, param = []):
self.param = param
print "A.__init__"
class B:
def __init__ (self):
print ("B.__init__")
def foo (self):
print ("B.Foo")
class C:
def __init__ (self, param = B()):
self.param = param
print "C.__init__"
def test1():
print ("\n=== Test1 ===")
a1 = A([])
a2 = A([])
print ('1, a1: ' + str (len (a1.param)))
print ('1, a2: ' + str (len (a2.param)))
a1.param.append ("new element")
print ('2, a1: ' + str (len (a1.param)))
print ('2, a2: ' + str (len (a2.param)))
def test2():
print ("\n=== Test2 ===")
a1 = A()
a2 = A()
print ('1, a1: ' + str (len (a1.param)))
print ('1, a2: ' + str (len (a2.param)))
a1.param.append ("new element")
print ('2, a1: ' + str (len (a1.param)))
print ('2, a2: ' + str (len (a2.param)))
def test3 ():
print ("\n=== Test3 ===")
print ("= C() =")
c1 = C()
c2 = C()
c1.param.foo()
c2.param.foo()
print ("= C(B()) =")
c3 = C(B())
c4 = C(B())
c3.param.foo()
c4.param.foo()
if (__name__ == __main__):
test1()
test2()
test3()
Запускаем скрипт и получаем следующий результат:
B.__init__ === Test1 === A.__init__ A.__init__ 1, a1: 0 1, a2: 0 2, a1: 1 2, a2: 0 === Test2 === A.__init__ A.__init__ 1, a1: 0 1, a2: 0 2, a1: 1 2, a2: 1 === Test3 === = C() = C.__init__ C.__init__ B.Foo B.Foo = C(B()) = B.__init__ C.__init__ B.__init__ C.__init__ B.Foo B.Foo
Отсюда можно сделать вывод, что конструктор класса B вызывается в самом начале скрипта, причем даже до того момента как реально нужно будет создать хоть один экземпляр класса C, которому и нужен экземпляр класса B. Заметьте, что конструктор класса B создается даже раньше того момента как будут запущены функции тестов, которые и используют любые классы. Метод foo я добавил в класс B только для того, чтобы еще раз убедиться, что класс B действительно создается и работает.
Вот такое вот оказывается интересное поведение у параметров по умолчанию в Python. А про такое поведение написано на этой странице документации.
Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.