«Наименьшее удивление» и изменчивый аргумент по умолчанию - 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 ]

1501 голосов
/ 18 июля 2009

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

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

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

257 голосов
/ 15 июля 2009

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

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

def eat(food=fruits):
    ...

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

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

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

тогда, если бы параметры по умолчанию были связаны при выполнении функции, а не при объявлении функции, то я был бы изумлен (очень плохо), обнаружив, что фрукты были изменены. Это было бы более поразительным IMO, чем обнаружение того, что ваша функция 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: аргументы функции по умолчанию оцениваются, когда функция определена, и этот объект всегда является значением по умолчанию. Я полагаю, что они могли бы использовать особый случай, используя пустой список, но такой особый случай может вызвать еще большее удивление, не говоря уже о несовместимости в обратном направлении.

226 голосов
/ 10 июля 2012

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

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

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

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

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

a = []

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

Почему a=[] в

def x(a=[]):

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

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

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

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

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

def x(static a=b):
78 голосов
/ 15 июля 2009

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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

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

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

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

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

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

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

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

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

Возвращает список функций, которые возвращают 0,1,2,3 ... соответственно. Если поведение будет изменено, они вместо этого будут связывать i со значением call-time для 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=[]

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

55 голосов
/ 09 декабря 2015

Почему бы тебе не заняться самоанализом?

Я действительно удивлен, что никто не выполнил проницательный самоанализ, предложенный Python (применимо 2 и 3) к вызываемым объектам.

Учитывая простую маленькую функцию func, определенную как:

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

Когда Python встречает его, первое, что он сделает, это скомпилирует его, чтобы создать объект code для этой функции. Пока этот шаг компиляции выполнен, Python оценивает *, а затем сохраняет аргументы по умолчанию (пустой список [] здесь) в самом объекте функции . Как указано в верхнем ответе: список a теперь можно рассматривать как член функции func.

.

Итак, давайте проведем некоторый самоанализ, до и после, чтобы проверить, как список расширяется внутри объекта функции. Я использую Python 3.x для этого, для Python 2 применяется то же самое (используйте __defaults__ или func_defaults в Python 2; да, два имени для одной и той же вещи).

Функция перед выполнением:

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

После того как Python выполнит это определение, он примет любые заданные по умолчанию параметры (a = [] здесь) и поместит их в атрибут __defaults__ для объекта функции (соответствующий раздел: Callables):

>>> func.__defaults__
([],)

О.К., поэтому пустой список как отдельная запись в __defaults__, как и ожидалось.

Функция после выполнения:

Давайте теперь выполним эту функцию:

>>> func()

Теперь, давайте посмотрим эти __defaults__ снова:

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

Удивлен? Значение внутри объекта изменяется! Последовательные вызовы функции теперь просто добавляются к этому внедренному list объекту:

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

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

Распространенным решением для борьбы с этим является использование None по умолчанию и затем инициализация в теле функции:

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

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


Для дальнейшей проверки того, что список в __defaults__ совпадает со списком, используемым в функции func, вы можете просто изменить свою функцию, чтобы она возвращала id списка a, используемого внутри тела функции. Затем сравните его со списком в __defaults__ (позиция [0] в __defaults__), и вы увидите, как они действительно ссылаются на тот же экземпляр списка:

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

Все с силой самоанализа!


* Чтобы убедиться, что Python оценивает аргументы по умолчанию во время компиляции функции, попробуйте выполнить следующее:

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

Как вы заметите, input() вызывается до того, как будет построен процесс привязки функции к имени bar.

54 голосов
/ 30 марта 2015

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

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

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

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

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

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

48 голосов
/ 15 июля 2009

Такое поведение легко объяснимо:

  1. Объявление функции (класса и т. Д.) Выполняется только один раз, создавая все объекты значений по умолчанию
  2. все передается по ссылке

Итак:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a не изменяется - каждый вызов присваивания создает новый объект int - печатается новый объект
  2. b не изменяется - новый массив строится из значения по умолчанию и печатается
  3. c изменения - операция выполняется над тем же объектом - и она печатается
33 голосов
/ 16 июля 2009

То, что вы спрашиваете, почему это:

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

внутренне не эквивалентно этому:

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()

за исключением случая явного вызова func (None, None), который мы проигнорируем.

Другими словами, вместо оценки параметров по умолчанию, почему бы не сохранить каждый из них и оценить их при вызове функции?

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

...