Параметры по умолчанию в 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.
5 stars

Рейтинг 5.0/5. Всего 2 голос(а, ов)


Немного рекламы


vadim 04.09.2008 - 01:46

опечатка :)

"У нас получилось, что при добавлнии элемента в первый экземпляр класса A,"
добавлнии <-

Jenyay 04.09.2008 - 09:50

Спасибо, сейчас исправлю :)

Andrej 18.10.2009 - 01:39

Параметры по умолчанию в Python (пробелы)

Хочу уточнить на всякий случай:
когда-то в одной из книг я читал что при присвоении параметру значения по умолчанию не должно(не рекомендуется) быть пробелов, а именно - param = B() --> param=B()

Andrej 18.10.2009 - 01:44

Спасибо за статьи, в целом очень познавательно !!!

 08.04.2012 - 01:53

shrieking smileyshrieking smileyshrieking smileyshrieking smiley


Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 732