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

16 голосов
/ 06 февраля 2015

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

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Если singleton используется только use_singleton, мне нравится следующий шаблон в качестве замены:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

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

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

15 голосов
/ 12 сентября 2014

Когда мы делаем это:

def foo(a=[]):
    ...

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

Чтобы упростить обсуждение, давайте временно дадим неназванному списку имя. Как насчет pavlo?

def foo(a=pavlo):
   ...

В любое время, если абонент не сообщает нам, что такое a, мы повторно используем pavlo.

Если pavlo является изменяемым (модифицируемым), и foo заканчивает его изменение, эффект, который мы заметим при следующем вызове foo без указания a.

Итак, это то, что вы видите (Помните, pavlo инициализируется как []):

 >>> foo()
 [5]

Теперь pavlo - это [5].

Вызов foo() снова изменяет pavlo, снова:

>>> foo()
[5, 5]

Указание a при вызове foo() гарантирует, что pavlo не будет затронут.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Итак, pavlo по-прежнему [5, 5].

>>> foo()
[5, 5, 5]
15 голосов
/ 15 января 2013

Вы можете обойти это, заменив объект (и, следовательно, связь с областью действия):

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

Ужасно, но работает.

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

Может быть так:

  1. Кто-то использует все функции языка / библиотеки, а
  2. Переключение здесь поведения было бы необоснованным, но

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

  1. Это сбивающая с толку особенность, и к сожалению, в Python.

Другие ответы, или, по крайней мере, некоторые из них, либо дают баллы 1 и 2, но не 3, либо ставят балл 3 и понижают баллы 1 и 2. Но все три верны.

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

Существующее поведение не является Pythonic, и Python является успешным, потому что очень мало о языке нарушает принцип наименьшего удивления где-либо вблизи это ужасно. Это реальная проблема, было бы разумно искоренить это. Это недостаток дизайна. Если вы гораздо лучше понимаете язык, пытаясь отследить поведение, я могу сказать, что C ++ делает все это и даже больше; Вы многому научитесь, перемещаясь, например, по тонким ошибкам указателя. Но это не Pythonic: люди, которые заботятся о Python достаточно, чтобы выдержать это поведение, являются людьми, которых привлекает язык, потому что Python имеет гораздо меньше сюрпризов, чем другой язык. Dabblers и любопытные становятся Pythonistas, когда они удивляются тому, как мало времени требуется, чтобы что-то заработало - не из-за дизайна, я имею в виду скрытую логическую головоломку, которая прорезает интуицию программистов, которые тянутся к Python потому что это Просто работает .

9 голосов
/ 22 июля 2013

Эта «ошибка» дала мне много сверхурочных часов работы! Но я начинаю видеть его потенциальное использование (но мне бы все же хотелось, чтобы оно было во время исполнения)

Я дам вам то, что вижу в качестве полезного примера.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

печатает следующее

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
8 голосов
/ 17 октября 2017

Это не недостаток дизайна . Любой, кто спотыкается об этом, делает что-то не так.

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

  1. Вы намереваетесь изменить аргумент как побочный эффект функции. В этом случае никогда не имеет смысла иметь аргумент по умолчанию. Единственное исключение - когда вы злоупотребляете списком аргументов, чтобы иметь атрибуты функции, например cache={}, и вы не должны вызывать функцию с фактическим аргументом вообще.
  2. Вы намереваетесь оставить аргумент без изменений, но вы случайно изменили его . Это ошибка, исправьте ее.
  3. Вы намереваетесь изменить аргумент для использования внутри функции, но не ожидали, что изменение будет доступно для просмотра вне функции. В этом случае вам нужно сделать копию аргумента, независимо от того, был он по умолчанию или нет! Python не является языком вызова по значению, поэтому он не делает копию для вас, вам нужно четко об этом сказать.

Пример в вопросе может попасть в категорию 1 или 3. Странно, что он и изменяет переданный список, и возвращает его; Вы должны выбрать один или другой.

8 голосов
/ 22 августа 2013

Я думаю, что ответ на этот вопрос заключается в том, как python передает данные параметру (передача по значению или по ссылке), а не изменчивости или как python обрабатывает оператор "def".

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

Принимая во внимание два вышеизложенных момента, давайте объясним, что случилось с кодом Python. Это происходит только из-за передачи по ссылке для объектов, но не имеет ничего общего с изменяемым / неизменным или, возможно, с тем фактом, что оператор def выполняется только один раз, когда он определен.

[] является объектом, поэтому python передает ссылку [] на a, т.е. a является только указателем на [], который лежит в памяти как объект. Существует только одна копия [] с множеством ссылок на нее. Для первого foo () список [] изменяется на 1 методом добавления. Но обратите внимание, что существует только одна копия объекта списка, и этот объект теперь становится 1 . При запуске второй функции foo () неверно то, что говорит веб-страница effbot (элементы больше не оцениваются). a оценивается как объект списка, хотя теперь содержимое объекта равно 1 . Это эффект передачи по ссылке! Результат foo (3) может быть легко получен таким же образом.

Для дальнейшей проверки моего ответа давайте рассмотрим два дополнительных кода.

====== № 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[] - это объект, также как и None (первый изменчив, а второй неизменен. Но изменчивость не имеет ничего общего с вопросом). Никого нет где-то в космосе, но мы знаем, что это там, и там есть только одна копия Никого. Таким образом, каждый раз, когда вызывается foo, элементы оцениваются (в отличие от некоторого ответа, что он оценивается только один раз), чтобы быть None, чтобы быть ясным, ссылка (или адрес) None. Затем в foo элемент изменяется на [], то есть указывает на другой объект с другим адресом.

====== № 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

При вызове foo (1) элементы указывают на объект списка [] с адресом, скажем, 11111111. содержимое списка изменяется на 1 в функции foo в дальнейшем, но адрес не изменился, все равно 11111111. Затем приходит foo (2, []). Хотя [] в foo (2, []) имеет то же содержимое, что и параметр по умолчанию [] при вызове foo (1), их адреса разные! Поскольку мы предоставляем параметр явно, items должен взять адрес этого нового [], скажем, 2222222, и вернуть его после внесения некоторых изменений. Теперь foo (3) выполняется. поскольку предоставляется только x, элементам снова нужно принять значение по умолчанию. Какое значение по умолчанию? Он устанавливается при определении функции foo: объекта списка, расположенного в 11111111. Таким образом, элементы оцениваются как адрес 11111111, имеющий элемент 1. Список, расположенный в 2222222, также содержит один элемент 2, но он не указывается элементами Больше. Следовательно, приложение 3 составит items [1,3].

Из приведенных выше объяснений видно, что веб-страница effbot , рекомендованная в принятом ответе, не дала соответствующего ответа на этот вопрос. Более того, я думаю, что точка на веб-странице effbot неверна. Я думаю, что код относительно UI.Button правильный:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

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

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Если мы выполним x[7](), мы получим 7, как ожидалось, а x[9]() даст 9, еще одно значение i.

7 голосов
/ 26 мая 2015

Просто измените функцию на:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
4 голосов
/ 15 декабря 2018

TLDR: значения по умолчанию для времени согласованы и строго более выразительны.


Определение функции влияет на две области действия: область определения , содержащая функцию, и область выполнения , содержащаяся в функции. Хотя довольно ясно, как блоки отображаются в области действия, вопрос в том, где def <name>(<args=defaults>): принадлежит:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

def name part должен оценить в определяющей области действия - мы хотим, чтобы name был там доступен, в конце концов. Оценка функции только внутри себя сделает ее недоступной.

Поскольку parameter является именем константы, мы можем "оценить" его одновременно с def name. Это также имеет то преимущество, что создает функцию с известной сигнатурой name(parameter=...): вместо name(...):.

Теперь, когда оценивать default?

Согласованность уже говорит «при определении»: все остальное из def <name>(<args=defaults>): лучше всего оценивать и при определении. Задержка его частей была бы удивительным выбором.

Оба варианта не эквивалентны: если default вычисляется во время определения, может все же влиять на время выполнения. Если default вычисляется во время выполнения, оно не может влиять на время определения. Выбор «при определении» позволяет выразить оба случая, а при выборе «при исполнении» можно выразить только один:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=None):     # delay default until execution time
    parameter = [] if parameter is None else parameter
    ...
1 голос
/ 03 января 2019

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

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

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Теперь давайте переопределим нашу функцию с помощью этого декоратора:

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

foo() # '[5]'
foo() # '[5]' -- as desired

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

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

с

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Важно отметить, что вышеприведенное решение не работает, если вы попытаетесь использовать аргументы ключевых слов, например:

foo(a=[4])

Декоратор может быть настроен, чтобы учесть это, но мы оставляем это как упражнение для читателя;)

...