"Наименьшее удивление" и изменяемый аргумент по умолчанию

любой, кто возится с 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()

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

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

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

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

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

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

30 ответов


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

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

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


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

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

def eat(food=fruits):
    ...

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


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

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


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

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

a = []

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

почему a=[] в

def x(a=[]):

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

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

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

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

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

def x(static a=b):

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

3. Самоанализ!--13-->

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

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

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

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

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


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

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

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

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

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

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


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

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

Дана простенькая функция func определено как:

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

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

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

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

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

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

>>> func.__defaults__
([],)

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

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

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

>>> func()

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

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

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

>>> 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 сделано.


это поведение легко объяснить:

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

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

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

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

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


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

пример:

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]

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

пример проблемы для подобный так вопрос

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]

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

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

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

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


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

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

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

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


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

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


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

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

Python: Изменяемый Аргумент По Умолчанию

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

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

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

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

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

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

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

_a_list = [] # create a list in the globals

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

del _a_list # remove globals name binding

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

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

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

example.py

print('1. Global scope being evaluated')

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

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

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

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


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

и с python example.py:

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

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

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

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

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

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

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

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

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

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

простой обходной путь с использованием 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]

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

  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 в функции call),foo.func_defs[0] (None) доступен в локальной области как L.
  • на L = [], задание не может быть выполнено при foo.func_defs[0], потому что этот атрибут предназначен только для чтения.
  • Per (1), новая локальная переменная с именем L создается в локальной области и используется для остальной части вызова функции. foo.func_defs[0] таким образом, остается неизменным для будущих вызовов foo.

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

def a(): return []

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

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

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


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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

>>> id(a)
5347866528

>>> id(b)
5347866528

за Бретта Слаткина "эффективный Python: 59 конкретных способов написать лучший Python",Пункт 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]

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


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

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

некрасиво, но это работает.


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

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

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

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

def foo(a=pavlo):
   ...

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

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

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

 >>> foo()
 [5]

теперь pavlo is [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]

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

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

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

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

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

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

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


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

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

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

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

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

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

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

чтобы подтвердить мой ответ, давайте посмотрим на два дополнительных кода.

====== No. 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 элемент изменяется на [], т. е. указывает на другой объект, который имеет другой адрес.

====== No. 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. Тогда фу(2,[]) идет. Хотя [] в foo(2,[]) имеет тот же контент, что и параметр по умолчанию [] при вызове foo (1), их адрес отличается! Поскольку мы предоставляем параметр явно,items взять адрес этого нового [], скажем 2222222, и верните его после внесения некоторых изменений. Теперь выполняется foo(3). так как только x предоставляется, элементы должны снова принять значение по умолчанию. Что по умолчанию ценность? Он устанавливается при определении функции foo: объект списка, расположенный в 11111111. Поэтому детали оценивается адресу 11111111 с элементом 1. Список, расположенный по адресу 2222222, также содержит один элемент 2,но он больше не указывается элементами. Следовательно, добавление 3 сделает items [1,3].

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

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.


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

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

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

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


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

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

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

архитектура

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

def a(b=[]):
    pass

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

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

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

атакуя эту проблему с полиморфизмом, мы расширим список 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 естественно, будет помещен в класс, где он принадлежит.

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