Область аргументов функции [дубликат]

Любой, владеющий 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 без размножения ошибок?)

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

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

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

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

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

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

2086
задан Matt 29 January 2017 в 01:29
поделиться

30 ответов

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

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

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

1358
ответ дан Roberto Liffredo 18 August 2018 в 00:56
поделиться
  • 1
    Для всех, кто читает вышеприведенный ответ, я настоятельно рекомендую вам потратить время на ознакомление с связанной статьей Effbot. Как и вся другая полезная информация, очень важно знать, как эта языковая функция может использоваться для кэширования результатов / memoisation! – Cam Jackson 14 October 2011 в 01:05
  • 2
    Даже если это объект первого класса, можно все же представить конструкцию, в которой code для каждого значения по умолчанию сохраняется вместе с объектом и переоценивается каждый раз при вызове функции. Я не говорю, что было бы лучше, просто функции, являющиеся первоклассными объектами, не полностью исключают его. – gerrit 11 January 2013 в 12:55
  • 3
    Извините, но все, что считается «Самый большой WTF в Python», наиболее определенно является дефектом дизайна . Это источник ошибок для each в какой-то момент, потому что никто не ожидает этого поведения сначала, а это значит, что он не должен был быть разработан таким образом. Меня не волнует, какие обручи им пришлось перепрыгнуть, они должны спроектировать Python, так что аргументы по умолчанию не статичны. – BlueRaja - Danny Pflughoeft 7 June 2013 в 22:28
  • 4
    Независимо от того, является ли это дефектом дизайна, ваш ответ, по-видимому, подразумевает, что такое поведение является каким-то необходимым, естественным и очевидным, учитывая, что функции являются первоклассными объектами, и это просто не так. У Python есть замыкания. Если вы замените аргумент по умолчанию назначением в первой строке функции, он вычисляет выражение каждого вызова (потенциально используя имена, объявленные в охватывающей области). Нет никакой причины, что было бы невозможно или разумно, чтобы аргументы по умолчанию оценивались каждый раз, когда функция вызывается точно так же. – Mark Amery 9 January 2014 в 00:16
  • 5
    Дизайн не следует непосредственно из functions are objects. В вашей парадигме предложение будет состоять в том, чтобы реализовать значения функций по умолчанию как свойства, а не атрибуты. – bukzor 3 May 2014 в 21:46

Я ничего не знаю о внутренних интерпретаторах 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):
97
ответ дан 101 18 August 2018 в 00:56
поделиться
  • 1
    Вы можете сделать: def x (a = None): И тогда, если a есть None, установите a = datetime.datetime.now () – Anon 16 July 2009 в 01:18
  • 2
    Я знаю, это был просто пример, объясняющий, почему я предпочитаю привязку времени исполнения. – Utaal 16 July 2009 в 10:01
  • 3
    Спасибо тебе за это. Я не мог сказать, почему это раздражает меня до конца. Вы сделали это красиво с минимальным пухом и путаницей. Как кто-то из системного программирования на C ++, а иногда и наивно "перевод" языковые особенности, этот ложный друг пинал меня в мягкой голове большой раз, точно так же, как атрибуты класса. Я понимаю, почему все так, но я не могу не любить, независимо от того, что из этого может получиться. По крайней мере, это настолько противоречит моему опыту, что я, вероятно (надеюсь) никогда не забуду это ... – AndreasT 22 April 2011 в 10:33
  • 4
    @Andreas, как только вы используете Python достаточно долго, вы начинаете видеть, насколько логично, чтобы Python интерпретировал вещи как атрибуты класса так, как это делается, - только из-за особых особенностей и ограничений таких языков, как C ++ (и Java, и C # ...), что смысл содержимого блока class {} интерпретироваться как принадлежащий экземплярам :). Но когда классы являются первоклассными объектами, очевидно, что естественным является их содержимое (в памяти), чтобы отразить их содержимое (в коде). – Karl Knechtel 22 July 2011 в 20:55
  • 5
    Нормативная структура - это не причуда или ограничение в моей книге. Я знаю, что это может быть неуклюжим и уродливым, но вы можете назвать это «определением». чего-либо. Динамические языки кажутся мне анархистами: Конечно, все свободны, но вам нужна структура, чтобы заставить кого-то опорожнить мусор и проложить дорогу. Думаю, я стар ... :) – AndreasT 26 July 2011 в 09:54

Python: Mutable Default Argument

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

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

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

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

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

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 singleton как объект-дозор, чтобы сообщить функции, есть ли у нас аргумент, отличный от значения по умолчанию. Если мы не получаем никакого аргумента, то мы действительно хотим использовать новый пустой список [] в качестве значения по умолчанию.

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

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

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
17
ответ дан Aaron Hall 18 August 2018 в 00:56
поделиться

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

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

Неверный метод (возможно ...):

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

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

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

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

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

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]

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

16
ответ дан Alexander 18 August 2018 в 00:56
поделиться

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

def a(): return []

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

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

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

17
ответ дан Ashraf.Shk786 18 August 2018 в 00:56
поделиться

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

>>> 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 или нет, и есть ли задействованные аргументы по умолчанию.

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

25
ответ дан Ben 18 August 2018 в 00:56
поделиться
  • 1
    Хотя это связано, я думаю, что это отличное поведение (как мы ожидаем, append изменится a «на месте»). То, что измененный по умолчанию не восстанавливается повторно при каждом вызове , является «неожиданным». бит ... по крайней мере для меня. :) – Andy Hayden 24 August 2012 в 13:27
  • 2
    @AndyHayden, если функция ожидается , чтобы изменить аргумент, почему имеет смысл иметь значение по умолчанию? – Mark Ransom 17 October 2017 в 13:31
  • 3
    @MarkRansom единственным примером, о котором я могу думать, является cache={}. Однако я подозреваю, что это «наименьшее удивление» появляется, когда вы не ожидаете (или хотите) функцию, которую вы вызываете, для изменения аргумента. – Andy Hayden 17 October 2017 в 17:56
  • 4
    @AndyHayden Я оставил свой собственный ответ здесь с расширением этого чувства. Дайте мне знать, что вы думаете. Я мог бы добавить ваш пример cache={} в него для полноты. – Mark Ransom 17 October 2017 в 18:02
  • 5
    @AndyHayden Точка моего ответа заключается в том, что если вы когда-либо удивляетесь случайному изменению значения аргумента по умолчанию, то у вас есть еще одна ошибка, которая заключается в том, что ваш код может случайно изменить значение вызывающего абонента, когда значение по умолчанию не было . И обратите внимание, что использование None и присвоение реального значения по умолчанию, если arg является None , не разрешает эту проблему (я считаю, что это анти-шаблон по этой причине). Если вы исправите другую ошибку, избегая изменения значений аргументов независимо от того, имеют ли они значения по умолчанию, вы никогда не заметите или не позаботитесь об этом «удивительном». поведение. – Ben 17 October 2017 в 21:44

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

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

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

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

16
ответ дан bgreen-litl 18 August 2018 в 00:56
поделиться
  • 1
    Я предпочитаю добавить декоратор для memoization и поместить кеш memoization на сам объект функции. – Stefano Borini 6 February 2015 в 08:34
  • 2
    Этот пример не заменяет более сложный шаблон, который вы показываете, потому что вы вызываете _make_singleton во время разбора в примере аргумента по умолчанию, но во время вызова в глобальном примере. Истинная подстановка будет использовать какой-то изменяемый поле для значения аргумента по умолчанию, но добавление аргумента дает возможность передавать альтернативные значения. – Yann Vernier 19 November 2017 в 09:29

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

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

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

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

2. Принудительные связанные параметры

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

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. Introspection

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

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=[]

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

51
ответ дан Brian 18 August 2018 в 00:56
поделиться
  • 1
    вы могли бы достичь интроспекции также, если для каждой была функция для создания аргумента по умолчанию вместо значения. модуль проверки просто вызовет эту функцию. – yairchu 16 July 2009 в 11:24
  • 2
    @SilentGhost: Я говорю о том, было ли поведение изменено для его воссоздания - создание его однажды является текущим поведением и почему существует изменяемая проблема по умолчанию. – Brian 16 July 2009 в 11:59
  • 3
    @yairchu: Это предполагает, что конструкция безопасна для этого (т.е. не имеет побочных эффектов). Интроспекция args не должна делать что-либо, но оценка произвольного кода вполне может привести к эффекту. – Brian 16 July 2009 в 12:02
  • 4
    Другой дизайн языка часто означает, что писать вещи по-разному. Ваш первый пример может быть легко написан как: _expensive = dear (); def foo (arg = _expensive), если вы специально не хотите переоценить. – Glenn Maynard 16 July 2009 в 19:23
  • 5
    @Glenn - вот что я имел в виду с "кэшированием переменной извне" - он немного более подробный, но в конечном итоге вы получаете дополнительные переменные в своем пространстве имен. – Brian 16 July 2009 в 20:04

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

Пример:

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

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

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

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

29
ответ дан Community 18 August 2018 в 00:56
поделиться
  • 1
    Надеюсь, вы знаете, что Python - это not функциональный язык программирования. – Veky 8 May 2014 в 14:18
  • 2
    Да, Python - это язык с несколькими парамиггами с некоторыми функциональными возможностями. («Не делайте каждую проблему похожим на гвоздь только потому, что у вас есть молот».) Многие из них находятся в лучших практиках Python. Python имеет интересное функциональное программирование HOWTO . Другие функции - это закрытие и каррирование, не упомянутые здесь. – hynekcer 8 May 2014 в 16:54
  • 3
    На этом позднем этапе я также добавлю, что семантика назначения Python была разработана явно, чтобы избежать необходимости копирования данных там, где это необходимо, поэтому создание копий (и особенно глубоких копий) будет отрицательно сказаться на использовании времени выполнения и памяти. Поэтому их следует использовать только в случае необходимости, но новичкам часто бывает трудно понять, когда это происходит. – holdenweb 16 January 2018 в 15:27
  • 4
    @holdenweb Я согласен. Временная копия является наиболее обычным способом, а иногда и единственным возможным способом защиты исходных изменяемых данных от посторонней функции, которая их потенциально изменяет. К счастью, функция, которая необоснованно изменяет данные, считается ошибкой и поэтому необычна. – hynekcer 17 January 2018 в 22:41
  • 5
    Я согласен с этим ответом. И я не понимаю, почему конструкция def f( a = None ) рекомендуется, когда вы действительно имеете в виду что-то еще. Копирование в порядке, потому что вы не должны мутировать аргументы. И когда вы делаете if a is None: a = [1, 2, 3], вы все равно копируете список. – koddo 16 February 2018 в 17:15

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

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

def eat(food=fruits):
    ...

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

231
ответ дан das-g 18 August 2018 в 00:56
поделиться
  • 1
    Я думаю, что это вопрос дебатов. Вы действуете на глобальной переменной. Любая оценка, выполняемая в любом месте вашего кода, включающего вашу глобальную переменную, теперь (правильно) относится к («черника», «манго»). параметр по умолчанию может быть похож на любой другой случай. – Stefano Borini 15 July 2009 в 19:16
  • 2
    На самом деле, я не думаю, что согласен с вашим первым примером. Я не уверен, что мне нравится идея изменить инициализатор, как это в первую очередь, но если бы я это сделал, я бы ожидал, что он будет вести себя точно так, как вы описали, - изменив значение по умолчанию на ("blueberries", "mangos"). – Ben Blank 15 July 2009 в 19:26
  • 3
    Параметр по умолчанию равен , как и любой другой случай. Неожиданным является то, что параметр является глобальной переменной, а не локальной. Это, в свою очередь, связано с тем, что код выполняется при определении функции, а не в вызове. Как только вы получите это, и что то же самое касается классов, это совершенно ясно. – Lennart Regebro 15 July 2009 в 19:59
  • 4
    Я нахожу пример вводящим в заблуждение, а не блестящим. Если some_random_function() присоединяется к fruits вместо назначения ему, поведение eat() будет изменяться. Так много для настоящего замечательного дизайна. Если вы используете аргумент по умолчанию, который упоминается в другом месте, а затем изменить ссылку из-за пределов функции, вы просите о проблемах. Настоящий WTF - это когда люди определяют новый аргумент по умолчанию (литерал списка или вызов конструктора), а still получают бит. – alexis 9 October 2014 в 16:37
  • 5
    Вы просто явно объявили global и переназначили кортеж - нет ничего удивительного, если eat работает по-другому после этого. – user3467349 26 January 2015 в 17:07

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

  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 меняют 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], поскольку этот атрибут доступен только для чтения.
  • Per (1), новая локальная переменная с именем L создается в локальной области и используется для остальной части вызова функции. foo.func_defs[0], таким образом, остается неизменным для будущих вызовов foo.
18
ответ дан Dmitry Minkovsky 18 August 2018 в 00:56
поделиться

Архитектура

Назначение значений по умолчанию в вызове функции - это запах кода.

def a(b=[]):
    pass

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

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

Функции, которые делают две вещи, являются плохими функциями, поскольку мы учимся на чистых практиках кода.

Атака на эту проблему с полиморфизмом мы будем расширять список python или переносить его в класс, а затем выполнять свою функцию на нем.

Но подождите, пока вы скажете, мне нравятся мои однострочные.

Ну , Угадай, что. Код - это не просто способ управления поведением оборудования. Это способ:

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

Не оставляйте бомбы замедленного действия для себя

Отделяя эту функцию от двух вещей, которые она делает, нам нужен класс

class ListNeedsFives(object):
    def __init__(self, b=None):
        if b is None:
            b = []
        self.b = b

    def foo():
        self.b.append(5)

Выполнено

a = ListNeedsFives()
a.foo()
a.b

И почему это лучше, чем слияние всего вышеописанного кода с одной функцией.

def dontdothis(b=None):
    if b is None:
        b = []
    b.append(5)
    return b

Почему бы не сделать это?

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

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

Метод foo() не возвращает список, почему бы и нет?

При возврате отдельного списка вы можете предположить, что безопасно делать то, что вам хочется. Но это может быть не так, поскольку он также разделяется объектом a. Заставляя пользователя ссылаться на него как a.b, он напоминает, где находится список. Любой новый код, который хочет изменить a.b, естественно, будет помещен в класс, где он принадлежит.

Функция подписи def dontdothis(b=None): не имеет ни одного из этих преимуществ.

-2
ответ дан firelynx 18 August 2018 в 00:56
поделиться
  • 1
    Я думаю, что ваш комментарий & quot; Code - это больше, чем просто способ управления поведением аппаратного обеспечения & quot; - это отлично, но для того, чтобы сказать, что аргументы по умолчанию представляют собой запах кода, кажется слишком резким. Есть прекрасные причины для аргументов по умолчанию. – Bryan Oakley 14 November 2017 в 02:29
  • 2
    @BryanOakley Ну ... я честно написал этот ответ, когда я был в каком-то темном настроении ... понял, что, черт возьми, я могу просто выговорить о моем мнении. В любом случае вопрос довольно упрям ​​... Так ... да, спасибо за похвалу, я тоже принимаю вину близко к сердцу, хотя – firelynx 14 November 2017 в 20:05

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

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), которые мы будем игнорировать.

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

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

31
ответ дан Glenn Maynard 18 August 2018 в 00:56
поделиться
  • 1
    Это не было бы закрытием - лучший способ подумать об этом просто заставил бы байт-код создавать по умолчанию первую строку кода - ведь вы все равно компилируете тело в этой точке - нет никакой реальной разницы между кодом в аргументах и ​​коде в теле. – Brian 16 July 2009 в 10:39
  • 2
    Правда, но это все равно замедлит Python, и это будет действительно удивительно, если вы не сделаете то же самое для определений классов, что сделало бы его глупо медленным, так как вам пришлось бы повторно запускать все определение класса каждый раз, когда вы создаете экземпляр класс. Как уже упоминалось, исправление будет более удивительным, чем проблема. – Lennart Regebro 16 July 2009 в 12:49
  • 3
    Согласился с Леннартом. Поскольку Гвидо любит говорить, для каждой языковой функции или стандартной библиотеки есть кто-то , используя ее. – Jason Baker 16 July 2009 в 14:21
  • 4
    Теперь изменить его было бы безумием - мы просто изучаем, почему так оно и есть. Если раньше начиналась оценка по умолчанию, это не обязательно было бы удивительно. Совершенно верно, что в таком ядре разница в синтаксическом анализе будет иметь широкий и, вероятно, много неясных последствий для языка в целом. – Glenn Maynard 16 July 2009 в 19:10

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

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

196
ответ дан glglgl 18 August 2018 в 00:56
поделиться
  • 1
    Фразы «это обычно не то, что было предназначено». и "путь вокруг этого" запах, как будто они документируют недостаток дизайна. – bukzor 3 May 2014 в 21:53
  • 2
    @Matthew: Я хорошо знаю, но это не стоит ловушки. По этой причине вы, как правило, видите руководства по стилям и линтам, безусловно, ошибочно меняете изменчивые значения по умолчанию. Явный способ сделать то же самое - добавить атрибут в функцию (function.data = []) или еще лучше, создать объект. – bukzor 19 June 2014 в 04:59
  • 3
    @bukzor: Ловушки должны быть отмечены и задокументированы, поэтому этот вопрос хорош и получил так много upvotes. В то же время ловушки необязательно должны быть удалены. Сколько начинающих Python передали список функции, которая его модифицировала, и были потрясены, увидев, что изменения отображаются в исходной переменной? Однако изменчивые типы объектов замечательны, когда вы понимаете, как их использовать. Я думаю, это просто сводится к мнению об этой конкретной ловушке. – Matthew 19 June 2014 в 18:54
  • 4
    Фраза «это обычно не то, что было предназначено». означает "не то, что действительно хотел программист", & quot; не «не то, что должен делать Python». & quot; – holdenweb 19 December 2014 в 12:48
  • 5
    @oriadam Возможно, вы захотите задать вопрос об этом. Может быть, вы делаете что-то другое, чем предполагалось ... – glglgl 7 September 2015 в 07:28

Простейшее обходное решение, использующее 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]
18
ответ дан hugo24 18 August 2018 в 00:56
поделиться

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

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 у вас нет этого преимущества.

24
ответ дан Jason Baker 18 August 2018 в 00:56
поделиться

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

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

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

15
ответ дан jdborg 18 August 2018 в 00:56
поделиться
  • 1
    Это хорошее решение в тех случаях, когда вы используете программное обеспечение для создания автоматической документации для документирования типов аргументов, ожидаемых функцией. Помещение a = None, а затем установка a на [], если a None, не помогает читателю понять с первого взгляда, что ожидается. – Michael Scott Cuthbert 20 January 2013 в 08:55
  • 2
    Лучше использовать docstring. – Aaron Hall♦ 26 November 2014 в 00:23
  • 3
    Прохладная идея: повторное связывание этого имени гарантирует, что он никогда не будет изменен. Мне это очень нравится. – holdenweb 16 January 2018 в 15:29
  • 4
    Это именно то, как это сделать. Python не делает копию параметра, поэтому вам нужно сделать копию явно. После того, как у вас есть копия, она будет изменяться, как вам будет угодно, без каких-либо неожиданных побочных эффектов. – Mark Ransom 25 May 2018 в 16:56

Это легко объясняется:

  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 изменения - операция выполняется на одном и том же объекте - и печатается
44
ответ дан Jim Fasarakis Hilliard 18 August 2018 в 00:56
поделиться
  • 1
    Ваш №4 может сбивать с толку людей, поскольку целые числа являются неизменными и поэтому "if & quot; неправда. Например, с d, установленным в 0, d .__ add __ (1) вернет 1, но d все равно будет 0. – Anon 16 July 2009 в 00:45
  • 2
    (На самом деле, add - плохой пример, но целые числа неизменяемы по-прежнему являются моей основной точкой.) – Anon 16 July 2009 в 00:54
  • 3
    да, это не был хороший пример – ymv 16 July 2009 в 00:57
  • 4
    Понял это до моего огорчения после проверки, чтобы убедиться, что с b установлен в [], b .__ add __ ([1]) возвращает [1], но также оставляет b еще [], даже если списки изменяемы. Виноват. – Anon 16 July 2009 в 01:03
  • 5
    @ANon: есть __iadd__, но он не работает с int. Конечно. :-) – Veky 8 May 2014 в 14:16
  • 6
    Требуется ли id(...) для этой последней проверки или оператор is отвечает на тот же вопрос? – das-g 9 March 2016 в 09:09
  • 7
    @ das-g is будет отлично, я просто использовал id(val), потому что думаю, что это может быть более интуитивно понятным. – Jim Fasarakis Hilliard 9 March 2016 в 09:20
  • 8
    @JimFasarakisHilliard Я бы добавил приглашение на input. как input('Did you just see me without calling me?'). Это делает его более понятным. – Ev. Kounis 1 September 2017 в 09:22
  • 9
    @ Ev.Kounis Мне это нравится! Спасибо что подметил это. – Jim Fasarakis Hilliard 1 September 2017 в 09:53

Это может быть правдой, что:

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

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

  1. Это запутанная функция, и это несчастливо в Python.

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

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

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

11
ответ дан JonathanHayward 18 August 2018 в 00:56
поделиться
  • 1
    -1 Хотя оправданная перспектива, это не ответ, и я не согласен с этим. Слишком много особых исключений порождают собственные угловые случаи. – Marcin 7 July 2012 в 20:24
  • 2
    Таким образом, это «удивительно невежественное». сказать, что в Python было бы разумнее, если аргумент по умолчанию [] останется [] при каждом вызове функции? – JonathanHayward 28 December 2012 в 00:09
  • 3
    И невежественно рассматривать в качестве неудачной идиомы значение аргумента по умолчанию для None, а затем в теле тела функции, если аргумент == None: argument = []? Невероятно ли считать эту идиому неудачной, так как часто люди хотят того, чего ожидал наивный новичок, если вы назначили f (аргумент = []), аргумент автоматически по умолчанию будет иметь значение []? – JonathanHayward 28 December 2012 в 00:11
  • 4
    Но в Python часть духа языка состоит в том, что вам не нужно брать слишком много глубоких погружений; array.sort () работает и работает независимо от того, насколько мало вы понимаете сортировку, big-O и константы. Красота Python в механизме сортировки массивов, чтобы дать один из бесчисленных примеров, заключается в том, что вам не нужно глубоко погружаться во внутренние детали. И, говоря иначе, красота Python заключается в том, что обычно не требуется глубоко погружаться в реализацию, чтобы получить то, что Just Works. И есть обходное решение (... if argument == None: argument = []), FAIL. – JonathanHayward 28 December 2012 в 00:41
  • 5
    В качестве автономного оператора x=[] означает «создать пустой объект списка» и привязать к нему имя «x». & Quot; Таким образом, в def f(x=[]) также создается пустой список. Он не всегда связан с x, поэтому вместо этого он привязывается к суррогату по умолчанию. Позже, когда вызывается f (), значение по умолчанию выгружается и привязывается к x. Поскольку это был пустой список, который был безвозвратно, тот же список - единственное, что доступно для привязки к x, независимо от того, что в нем было застряло или нет. Как же может быть иначе? – Jerry B 5 October 2013 в 07:18

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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

72
ответ дан Lennart Regebro 18 August 2018 в 00:56
поделиться
  • 1
    Если для каждого экземпляра все иначе, это не атрибут класса. Атрибуты класса являются атрибутами CLASS. Отсюда и название. Следовательно, они одинаковы для всех случаев. – Lennart Regebro 15 July 2009 в 20:17
  • 2
    Он не просил описания поведения Питона, он просил объяснения. Ничто в Python не просто «Как это работает» ;; все это делает то, что делает по какой-то причине. – Glenn Maynard 15 July 2009 в 21:20
  • 3
    И я объяснил это. – Lennart Regebro 15 July 2009 в 22:56
  • 4
    Я бы не сказал, что это «хорошее учебное пособие», потому что это не так. – Geo 16 July 2009 в 14:20
  • 5
    @Geo: За исключением того, что это так. Это помогает вам понять многое на Python. – Lennart Regebro 16 July 2009 в 15:12

5 баллов в защиту Python

  1. Простота: поведение прост в следующем смысле: большинство людей попадают в эту ловушку только один раз, а не несколько раз.
  2. Согласованность: Python всегда передает объекты, а не имена. Параметр по умолчанию, очевидно, является частью заголовка функции (а не тела функции). Поэтому он должен оцениваться при времени загрузки модуля (и только при времени загрузки модуля, если не вложен), а не во время вызова функции.
  3. Полезность: Как указывает Фредерик Лунд в своем объяснении Значения параметров по умолчанию в Python ", текущее поведение может быть весьма полезно для расширенного программирования. (Используйте экономно.)
  4. Достаточная документация: в самой базовой документации Python, в руководстве, проблема громко объявляется как «Важное предупреждение» в первом разделе раздела раздела «Подробнее о определении функций» . Предупреждение даже использует жирный шрифт, который редко применяется за пределами заголовков. RTFM: прочитайте тонкое руководство.
  5. Мета-обучение: падение в ловушку на самом деле является очень полезным моментом (по крайней мере, если вы являетесь рефлексивным учеником), потому что впоследствии вы лучше поймете пункт «согласованность», выше, и это научит вас много о Python.
48
ответ дан Lutz Prechelt 18 August 2018 в 00:56
поделиться
  • 1
    Мне потребовался год, чтобы найти, что такое поведение испортило мой код на производстве, в итоге удалило полную функцию, пока я случайно не столкнулся с этим недостатком дизайна. Я использую Django. Поскольку в промежуточной среде не было много запросов, эта ошибка никогда не оказывала влияния на качество. Когда мы поехали вживую и получили много одновременных запросов - некоторые служебные функции начали переписывать параметры друг друга! Делать дыры в безопасности, ошибки, а что нет. – oriadam 5 September 2015 в 13:09
  • 2
    @oriadam, не обижайтесь, но мне интересно, как вы научились Python, не сталкиваясь с этим раньше. Я просто изучаю Python сейчас, и эта возможная ловушка упоминается в официальном учебнике Python рядом с первым упоминанием аргументов по умолчанию. (Как упоминалось в пункте 4 этого ответа.) Я полагаю, что мораль - скорее несимпатично - читать официальные документы языка, который вы используете для создания производственного программного обеспечения. – Wildcard 30 August 2016 в 02:26
  • 3
    Кроме того, было бы удивительно (для меня), если бы вызвала функцию неизвестной сложности в дополнение к вызову функции, которую я делаю. – Vatine 2 September 2016 в 13:26

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

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

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

17
ответ дан Marcin 18 August 2018 в 00:56
поделиться

Это не ошибка дизайна . Любой, кто совершает это, делает что-то неправильно.

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

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

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

6
ответ дан Mark Ransom 18 August 2018 в 00:56
поделиться
  • 1
    & quot; Делать что-то неправильно & quot; это диагноз. Тем не менее, я думаю, что времена были = Нет шаблон полезен, но обычно вы не хотите изменять, если в этом случае передается mutable (2). Шаблон cache={} - это действительно единственное решение для интервью, в реальном коде вы, вероятно, захотите @lru_cache ! – Andy Hayden 17 October 2017 в 18:19
>>> def a():
>>>    print "a executed"
>>>    return []
>>> x =a()
a executed
>>> def b(m=[]):
>>>    m.append(5)
>>>    print m
>>> b(x)
[5]
>>> b(x)
[5, 5]
5
ответ дан Martijn Pieters 18 August 2018 в 00:56
поделиться

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

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

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
9
ответ дан Norfeldt 18 August 2018 в 00:56
поделиться

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

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

... мы присваиваем аргумент a списку unnamed , если вызывающий объект не передает значение 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]
14
ответ дан Saish 18 August 2018 в 00:56
поделиться

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

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
22
ответ дан Stéphane 18 August 2018 в 00:56
поделиться
  • 1
    на самом деле это может быть немного запутанным для новичков как a = a + [1] перегрузки a ... рассмотреть возможность изменения его на b = a + [1] ; print id(b) и добавить строку a.append(2). Это сделает более очевидным, что + в двух списках всегда создает новый список (назначается b), а модифицированный a может по-прежнему иметь тот же id(a). – Jörn Hees 8 April 2017 в 13:47

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

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

Признавая вышеуказанные два момента, давайте объясним, что произошло с кодом python. Это происходит только из-за передачи по ссылке для объектов, но не имеет ничего общего с mutable / immutable или, возможно, факта, что оператор «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, но он не указывается элементами any Больше. Следовательно, добавление 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.

8
ответ дан user2384994 18 August 2018 в 00:56
поделиться
  • 1
    Ваш последний пункт неверен. Попробуйте, и вы увидите, что x[7]() - 9. – Duncan 2 October 2013 в 14:29
  • 2
    «элементарный тип данных прохода python по значению, т. е. делает локальную копию значения локальной переменной», совершенно неверно. Я удивлен, что кто-то может, очевидно, хорошо знать Python, но у него такое ужасное непонимание основ. :-( – Veky 19 November 2014 в 10:07
  • 3
    Был ли из-за тона, или вы думаете, что я говорю неправильно? – Veky 29 November 2014 в 06:38

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

def notastonishinganymore(a = []): '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
3
ответ дан ytpillai 18 August 2018 в 00:56
поделиться
  • 1
    Это даже не законный Python. Почему 6 человек будут выдвигать этот ответ за пределы меня. – Aaron Hall♦ 14 August 2016 в 00:14
  • 2
    Пожалуйста, определите, почему это не законный Python, если он работает, и мы не находимся в корпоративной ситуации с помощью своих правил – ytpillai 15 August 2016 в 14:21
  • 3
    Docstring помещалась в строку, определяющую функцию, поэтому есть синтаксическая ошибка. – Benjamin Engwall 7 August 2018 в 14:53
45
ответ дан Jim Fasarakis Hilliard 6 September 2018 в 16:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: