Вопрос: «Наименьшее удивление» и параметр Mutable Default Argument


Любой, владеющий 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строка будет «гибридной» в том смысле, что часть привязки (функционального объекта) произойдет при определении, а часть (присвоение параметров по умолчанию) во время вызова функции.

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


2020


источник


Ответы:


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

Как только вы начнете думать таким образом, тогда это полностью имеет смысл: функция - это объект, оцениваемый по его определению; параметры по умолчанию являются «данными-членами», и поэтому их состояние может меняться от одного вызова к другому - точно так же, как и в любом другом объекте.

В любом случае, у Effbot есть очень хорошее объяснение причин такого поведения в Значения параметров по умолчанию в Python ,
Я нашел это очень ясным, и я действительно предлагаю прочитать его, чтобы лучше узнать, как работают объекты функций.


1332



Предположим, у вас есть следующий код

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Когда я вижу декларацию о еде, наименее удивительной является мысль, что если первый параметр не указан, то он будет равен кортежу ("apples", "bananas", "loganberries")

Однако, предположил позже в коде, я делаю что-то вроде

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

то если параметры по умолчанию были связаны с выполнением функции, а не с объявлением функции, то я был бы удивлен (очень плохо) обнаружить, что фрукты были изменены. Это было бы более удивительно ИМО, чем открытие того, что ваш fooвышеперечисленная функция мутировала список.

Реальная проблема связана с изменяемыми переменными, и все языки имеют определенную проблему. Вот вопрос: допустим, в Java у меня есть следующий код:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Теперь, моя карта использует значение StringBufferкогда он был помещен в карту или хранит ключ по ссылке? В любом случае, кто-то удивлен; либо человек, который пытался получить объект из Mapиспользуя значение, идентичное тому, с которым они его вставляли, или человек, который не может получить свой объект, даже если используемый ключ является буквально тем же самым объектом, который использовался для размещения его на карте (это на самом деле, почему Python не позволяет использовать его изменяемые встроенные типы данных в качестве словарных клавиш).

Ваш пример - хороший случай, когда новички Python будут удивлены и укушены. Но я бы сказал, что если бы мы «исправили» это, тогда это создало бы другую ситуацию, в которой они были бы укушены, и это было бы еще менее интуитивным. Более того, это всегда имеет место при работе с изменяемыми переменными; вы всегда сталкиваетесь с ситуациями, когда кто-то может интуитивно ожидать одно или наоборот поведения в зависимости от того, какой код они пишут.

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


222



AFAICS никто еще не опубликовал соответствующую часть документация :

Значения параметров по умолчанию оцениваются при выполнении определения функции. Это означает, что выражение оценивается один раз, когда функция определена, и что для каждого вызова используется одно и то же «предварительно вычисленное» значение. Это особенно важно для понимания, когда параметр по умолчанию является изменяемым объектом, таким как список или словарь: если функция изменяет объект (например, добавив элемент в список), значение по умолчанию изменяется. Обычно это не то, что было предназначено. Способ вокруг этого состоит в том, чтобы использовать None как значение по умолчанию и явно проверить его в теле функции [...]


191



Я ничего не знаю о внутренней интерпретации интерпретатора Python (и я тоже не являюсь экспертом в компиляторах и переводчиках), поэтому не обвиняйте меня, если я предлагаю что-либо недоступное или невозможное.

При условии, что объекты python изменчивы Я думаю, что это нужно учитывать при разработке аргументов аргументов по умолчанию. Когда вы создаете экземпляр списка:

a = []

вы ожидаете получить новый список, на который ссылается ,

Почему a = [] в

def x(a=[]):

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

def x(a=datetime.datetime.now()):

пользователь, вы хотите по умолчанию - время, соответствующее тому, когда вы определяете или выполняете Икс ? В этом случае, как и в предыдущем, я буду придерживаться такого же поведения, как если бы аргумент по умолчанию «назначение» был первой инструкцией функции (datetime.now (), вызванной вызовом функции). С другой стороны, если пользователь хотел отобразить время-отображение, он мог бы написать:

b = datetime.datetime.now()
def x(a=b):

Я знаю, я знаю: это закрытие. В качестве альтернативы Python может предоставить ключевое слово для привязки определения времени:

def x(static a=b):

95



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

Сравните это:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Этот код страдает от такого же неожиданного случая. bananas - это атрибут класса, и, следовательно, когда вы добавляете к нему вещи, он добавляется ко всем экземплярам этого класса. Причина точно такая же.

Это просто «Как это работает», и заставить его работать по-другому в функциональном случае, вероятно, будет сложно, а в случае класса, вероятно, невозможно или, по крайней мере, замедлить создание экземпляра объекта, поскольку вам придется хранить код класса вокруг и выполнять его при создании объектов.

Да, это неожиданно. Но как только пенни падает, она прекрасно вписывается в то, как работает Python в целом. На самом деле, это хорошее учебное пособие, и как только вы поймете, почему это происходит, вы будете намного лучше использовать python.

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


71



Я думал, что создание объектов во время выполнения будет лучшим подходом. Теперь я менее уверен, так как вы теряете некоторые полезные функции, хотя это может стоить того, что было бы просто для предотвращения путаницы новичков. Недостатки этого:

1. Производительность

def foo(arg=something_expensive_to_compute())):
    ...

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

2. Формирование связанных параметров

Полезный трюк заключается в привязке параметров лямбда к текущий привязка переменной при создании лямбда. Например:

funcs = [ lambda i=i: i for i in range(10)]

Это возвращает список функций, возвращающих 0,1,2,3 ... соответственно. Если поведение изменено, они вместо этого свяжутся iк Время звонка значение i, поэтому вы получите список функций, которые все возвратили 9,

Единственный способ реализовать это в противном случае - создать дальнейшее закрытие с привязкой i, то есть:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Интроспекция

Рассмотрим код:

def foo(a='test', b=100, c=[]):
   print a,b,c

Мы можем получить информацию о аргументах и ​​значениях по умолчанию, используя inspectмодуль, который

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

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

Теперь предположим, что поведение дефолтов может быть изменено так, что это эквивалентно:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

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


49



5 очков в защиту Python

  1. Простота : Поведение прост в следующем смысле: Большинство людей попадают в эту ловушку только один раз, а не несколько раз.

  2. консистенция : Python всегда передает объекты, а не имена. Параметр по умолчанию, очевидно, является частью функции заголовок (не тело функции). Поэтому его следует оценивать при времени загрузки модуля (и только при времени загрузки модуля, если не вложен), а не при вызове функции.

  3. Полезность : Как отмечает Фредерик Лунд в своем объяснении из «Значения параметров по умолчанию в Python» , текущее поведение может быть весьма полезным для расширенного программирования. (Используйте экономно.)

  4. Достаточная документация : В самой базовой документации Python, учебник, вопрос громко объявляется как «Важное предупреждение» в первый подраздел раздела «Подробнее о определении функций» , Предупреждение даже использует жирный шрифт, который редко применяется за пределами заголовков. RTFM: прочитайте точное руководство.

  5. Мета-обучение : Падение в ловушку на самом деле очень полезный момент (по крайней мере, если вы являетесь рефлексивным учеником), потому что впоследствии вы лучше поймете суть «Консистенция» выше, и это будет много расскажите о Python.


46



Why don't you introspect?

I'm really surprised no one has performed the insightful introspection offered by Python (2 and 3 apply) on callables.

Given a simple little function func defined as:

>>> def func(a = []):
...    a.append(5)

When Python encounters it, the first thing it will do is compile it in order to create a code object for this function. While this compilation step is done, Python evaluates* and then stores the default arguments (an empty list [] here) in the function object itself. As the top answer mentioned: the list a can now be considered a member of the function func.

So, let's do some introspection, a before and after to examine how the list gets expanded inside the function object. I'm using Python 3.x for this, for Python 2 the same applies (use __defaults__ or func_defaults in Python 2; yes, two names for the same thing).

Function Before Execution:

>>> def func(a = []):
...     a.append(5)
...     

After Python executes this definition it will take any default parameters specified (a = [] here) and cram them in the __defaults__ attribute for the function object (relevant section: Callables):

>>> func.__defaults__
([],)

O.k, so an empty list as the single entry in __defaults__, just as expected.

Function After Execution:

Let's now execute this function:

>>> func()

Now, let's see those __defaults__ again:

>>> func.__defaults__
([5],)

Astonished? The value inside the object changes! Consecutive calls to the function will now simply append to that embedded list object:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

So, there you have it, the reason why this 'flaw' happens, is because default arguments are part of the function object. There's nothing weird going on here, it's all just a bit surprising.

The common solution to combat this is to usual None as the default and then initialize in the function body:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Since the function body is executed anew each time, you always get a fresh new empty list if no argument was passed for a.


To further verify that the list in __defaults__ is the same as that used in the function func you can just change your function to return the id of the list a used inside the function body. Then, compare it to the list in __defaults__ (position [0] in __defaults__) and you'll see how these are indeed refering to the same list instance:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

All with the power of introspection!


* To verify that Python evaluates the default arguments during compilation of the function, try executing the following:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

as you'll notice, input() is called before the process of building the function and binding it to the name bar is made.


41



This behavior is easy explained by:

  1. function (class etc.) declaration is executed only once, creating all default value objects
  2. everything is passed by reference

So:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a doesn't change - every assignment call creates new int object - new object is printed
  2. b doesn't change - new array is build from default value and printed
  3. c changes - operation is performed on same object - and it is printed

39



What you're asking is why this:

def func(a=[], b = 2):
    pass

isn't internally equivalent to this:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

except for the case of explicitly calling func(None, None), which we'll ignore.

In other words, instead of evaluating default parameters, why not store each of them, and evaluate them when the function is called?

One answer is probably right there--it would effectively turn every function with default parameters into a closure. Even if it's all hidden away in the interpreter and not a full-blown closure, the data's got to be stored somewhere. It'd be slower and use more memory.


30