«Наименьшее удивление» и изменчивый аргумент по умолчанию - PullRequest
2345 голосов
/ 15 июля 2009

Любой, кто возился с Python достаточно долго, был укушен (или разорван на части) следующей проблемой:

def foo(a=[]):
    a.append(5)
    return a

Новички в Python ожидают, что эта функция всегда будет возвращать список только с одним элементом: [5]. Вместо этого результат совсем другой и очень удивительный (для новичка):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Мой менеджер однажды впервые столкнулся с этой функцией и назвал ее «драматическим недостатком дизайна» языка. Я ответил, что у поведения есть объяснение, лежащее в основе, и оно действительно очень загадочное и неожиданное, если вы не понимаете внутренностей. Однако я не смог ответить (сам себе) на следующий вопрос: в чем причина привязки аргумента по умолчанию при определении функции, а не при выполнении функции? Я сомневаюсь, что опытное поведение имеет практическое применение (кто на самом деле использовал статические переменные в C, без выявления ошибок?)

Редактировать :

Бачек сделал интересный пример. Вместе с большинством ваших комментариев и, в частности, с Утаалом, я уточнил:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Мне кажется, что проектное решение было относительно того, куда поместить область параметров: внутри функции или "вместе" с ней?

Выполнение привязки внутри функции будет означать, что x эффективно привязан к указанному значению по умолчанию, когда функция вызывается, а не определяется, что может привести к серьезному недостатку: строка def будет "гибридной" в ощущение, что часть привязки (объекта функции) будет происходить при определении, а часть (назначение параметров по умолчанию) во время вызова функции.

Фактическое поведение является более последовательным: все, что в этой строке оценивается при выполнении этой строки, что означает определение функции.

Ответы [ 31 ]

31 голосов
/ 22 ноября 2012

1) Так называемая проблема «изменяемого аргумента по умолчанию», как правило, представляет собой особый пример, демонстрирующий, что:
«Все функции с этой проблемой также страдают от аналогичной проблемы побочных эффектов для фактического параметра ,
Это противоречит правилам функционального программирования, обычно нежелательным и должно быть исправлено вместе.

Пример:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Решение : копия
Абсолютно безопасным решением является copy или deepcopy сначала входной объект, а затем делать что-либо с копией.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Многие встроенные изменяемые типы имеют метод копирования, например some_dict.copy() или some_set.copy(), или могут быть легко скопированы, например somelist[:] или list(some_list). Каждый объект также может быть скопирован с помощью copy.copy(any_object) или более подробно с помощью copy.deepcopy() (последний полезен, если изменяемый объект составлен из изменяемых объектов). Некоторые объекты в основном основаны на побочных эффектах, таких как «файловый» объект, и не могут быть воспроизведены путем копирования. копирование

Пример задачи для аналогичного вопроса SO

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

Его не следует сохранять ни в каком атрибуте public экземпляра, возвращаемого этой функцией. (При условии, что частные атрибуты экземпляра не должны изменяться извне этого класса или подклассов в соответствии с соглашением. То есть _var1 является частным атрибутом)

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

2)
Только если побочный эффект для фактического параметра требуется, но нежелателен для параметра по умолчанию, тогда полезным решением будет def ...(var1=None): if var1 is None: var1 = [] Подробнее ..

3) В некоторых случаях изменяемое поведение параметров по умолчанию полезно .

28 голосов
/ 23 мая 2011

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

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

В этом коде нет никаких значений по умолчанию, но вы получите точно такую ​​же проблему.

Проблема в том, что foo является изменяющим изменяемой переменной, передаваемой от вызывающей стороны, когда вызывающая сторона не ожидает этого. Код, подобный этому, был бы хорош, если бы функция вызывалась как-то вроде append_5; тогда вызывающая сторона будет вызывать функцию, чтобы изменить передаваемое значение, и поведение будет ожидаемым. Но такая функция вряд ли будет принимать аргумент по умолчанию и, вероятно, не будет возвращать список (так как вызывающая сторона уже имеет ссылку на этот список; тот, который она только что передала).

Ваш исходный foo с аргументом по умолчанию не должен изменять a независимо от того, был ли он передан явно или получил значение по умолчанию. Ваш код должен оставлять изменяемые аргументы в покое, если из контекста / имени / документации не ясно, что аргументы должны быть изменены. Использование изменяемых значений, передаваемых в качестве аргументов в качестве локальных временных файлов, является крайне плохой идеей, независимо от того, находимся ли мы в Python или нет, и используются ли аргументы по умолчанию или нет.

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

26 голосов
/ 27 марта 2015

Уже занятая тема, но из того, что я прочитал здесь, следующее помогло мне понять, как это работает внутри:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232
25 голосов
/ 16 июля 2009

Это оптимизация производительности. Как вы думаете, какой из этих двух вызовов функций в результате этой функции быстрее?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Я дам вам подсказку. Вот разборка (см. http://docs.python.org/library/dis.html):

# 1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

# 2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Я сомневаюсь, что опытное поведение имеет практическое применение (кто на самом деле использовал статические переменные в C, без выявления ошибок?)

Как видите, - это выигрыш в производительности при использовании неизменяемых аргументов по умолчанию. Это может иметь значение, если это часто вызываемая функция или для создания аргумента по умолчанию требуется много времени. Также имейте в виду, что Python не C. В C у вас есть константы, которые в значительной степени свободны. В Python у вас нет этого преимущества.

22 голосов
/ 16 июля 2009

Самый короткий ответ, вероятно, будет "определение есть исполнение", поэтому весь аргумент не имеет строгого смысла. В качестве более надуманного примера вы можете привести это:

def a(): return []

def b(x=a()):
    print x

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

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

22 голосов
/ 01 мая 2016

Python: изменяемый аргумент по умолчанию

Аргументы по умолчанию оцениваются во время компиляции функции в объект функции. При использовании этой функцией несколько раз этой функцией они являются и остаются одним и тем же объектом.

Когда они являются изменяемыми, при мутировании (например, путем добавления к нему элемента) они остаются мутированными при последовательных вызовах.

Они остаются мутированными, потому что каждый раз они являются одним и тем же объектом.

Эквивалентный код:

Поскольку список привязан к функции, когда объект функции компилируется и создается, это:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

почти в точности соответствует этому:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Демонстрация

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

  • видя, что список создается до завершения компиляции функции в объект функции,
  • наблюдая, что идентификатор одинаков при каждом обращении к списку,
  • заметив, что список остается измененным, когда функция, которая использует его, вызывается во второй раз,
  • соблюдая порядок, в котором вывод печатается из источника (который я для вас удобно пронумеровал):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

и запустить его с python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Это нарушает принцип "наименьшего удивления"?

Этот порядок выполнения часто сбивает с толку новых пользователей Python. Если вы понимаете модель исполнения Python, то она становится вполне ожидаемой.

Обычная инструкция для новых пользователей Python:

Но именно поэтому обычная инструкция для новых пользователей состоит в том, чтобы вместо этого создавать их аргументы по умолчанию:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

При этом используется синглтон None в качестве сторожевого объекта, чтобы сообщить функции, получили ли мы аргумент, отличный от значения по умолчанию. Если мы не получим аргумента, то мы фактически хотим использовать новый пустой список, [], по умолчанию.

Как указано в учебном разделе о потоке управления :

Если вы не хотите, чтобы настройки по умолчанию распределялись между последующими вызовами, вместо этого вы можете написать такую ​​функцию:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
19 голосов
/ 28 февраля 2013

Простой обходной путь с использованием None

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
19 голосов
/ 24 апреля 2012

Такое поведение неудивительно, если принять во внимание следующее:

  1. Поведение атрибутов класса только для чтения при попытках присваивания, и это
  2. Функции являются объектами (хорошо объяснено в принятом ответе).

Роль (2) широко освещалась в этой теме. (1) , вероятно, является фактором, вызывающим удивление, так как это поведение не является «интуитивным» при переходе с других языков.

(1) описано в руководстве Python по классам . При попытке присвоить значение атрибуту класса только для чтения:

... все переменные, найденные вне самой внутренней области видимости, только для чтения ( попытка записи в такую ​​переменную просто создаст новая локальная переменная в самой внутренней области, оставляя идентично именованная внешняя переменная без изменений ).

Вернитесь к исходному примеру и рассмотрите следующие пункты:

def foo(a=[]):
    a.append(5)
    return a

Здесь foo является объектом, а a является атрибутом foo (доступно в foo.func_defs[0]). Поскольку a является списком, a является изменяемым и, таким образом, является атрибутом чтения-записи foo. Он инициализируется пустым списком, как указано в сигнатуре, когда создается экземпляр функции, и доступен для чтения и записи, пока существует объект функции.

Вызов foo без переопределения значения по умолчанию использует значение по умолчанию от foo.func_defs. В этом случае foo.func_defs[0] используется для a в пределах области кода объекта функции. Изменяется на a change foo.func_defs[0], который является частью объекта foo и сохраняется между выполнением кода в foo.

Теперь сравните это с примером из документации по , имитирующей поведение аргументов по умолчанию для других языков , так что значения сигнатур функции по умолчанию используются каждый раз, когда функция выполняется:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Принимая во внимание (1) и (2) , можно понять, почему это приводит к желаемому поведению:

  • Когда создается экземпляр функционального объекта foo, foo.func_defs[0] устанавливается на None, неизменный объект.
  • Когда функция выполняется со значениями по умолчанию (без указания параметра для L в вызове функции), foo.func_defs[0] (None) доступно в локальной области как L.
  • После L = [] назначение не может быть успешно выполнено на foo.func_defs[0], поскольку этот атрибут только для чтения.
  • За (1) , новая локальная переменная также называется L, создается в локальной области действия и используется для оставшейся части вызова функции , foo.func_defs[0], таким образом, остается неизменным для будущих вызовов foo.
17 голосов
/ 20 марта 2012

Решения здесь:

  1. Используйте None в качестве значения по умолчанию (или одноразового номера object) и включите его, чтобы создавать значения во время выполнения; или
  2. Используйте lambda в качестве параметра по умолчанию и вызывайте его в блоке try, чтобы получить значение по умолчанию (для такого типа лямбда-абстракции).

Второй вариант хорош, потому что пользователи функции могут передать вызываемый объект, который может уже существовать (например, type)

17 голосов
/ 12 сентября 2015

Я собираюсь продемонстрировать альтернативную структуру для передачи значения списка по умолчанию в функцию (она одинаково хорошо работает со словарями).

Как другие подробно прокомментировали, параметр списка привязан к функции, когда он определен, а не когда он выполняется. Поскольку списки и словари являются изменчивыми, любое изменение этого параметра повлияет на другие вызовы этой функции. В результате последующие вызовы функции получат этот общий список, который мог быть изменен любыми другими вызовами функции. Хуже того, два параметра одновременно используют общий параметр этой функции, не обращая внимания на изменения, сделанные другим.

Неправильный метод (вероятно ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Вы можете проверить, что это один и тот же объект, используя id:

>>> id(a)
5347866528

>>> id(b)
5347866528

По Бретту Слаткину «Эффективный Python: 59 специальных способов написать лучший Python», Item 20: Используйте None и строки документации для указания динамических аргументов по умолчанию (стр. 48)

Соглашение для достижения желаемого результата в Python заключается в предоставить значение по умолчанию None и задокументировать фактическое поведение в строке документации.

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

Предпочтительный метод :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

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

...