"Наименьшее удивление" и параметр Mutable Default Argument

Любой, владеющий Python достаточно долго, был укушен (или разорван на куски) по следующей проблеме:

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

Новички Python ожидают, что эта функция всегда вернет список только с одним элементом: [5]. В результате вместо этого очень разные и очень удивительные (для новичков):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Один мой менеджер когда-то впервые встретился с этой функцией и назвал его "драматическим недостатком дизайна" языка. Я ответил, что поведение имеет основополагающее объяснение, и оно действительно очень озадачивает и неожиданно, если вы не понимаете внутренности. Тем не менее, я не смог ответить (себе) на следующий вопрос: в чем причина привязки аргумента по умолчанию при определении функции, а не при выполнении функции? Я сомневаюсь, что опытное поведение имеет практическое применение (кто действительно использовал статические переменные в C, без размножения ошибок?)

Изменить:

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

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

+2425
источник поделиться
34 ответа
  • 1
  • 2

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

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

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

+1537
источник

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

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

def eat(food=fruits):
    ...

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

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

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

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

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

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

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

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

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

+263
источник

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

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

+229
источник

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

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

a = []

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

Почему a=[] в

def x(a=[]):

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

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

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

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

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

def x(static a=b):
+109
источник

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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

+81
источник

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

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

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

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

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

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

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

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

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

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

3. Самоанализ

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

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

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

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

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

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

_undefined = object()  # sentinel value

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

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

+57
источник

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

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

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

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

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

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

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

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

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

>>> func.__defaults__
([],)

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

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

Позвольте теперь выполнить эту функцию:

>>> func()

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

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

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

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

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

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

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

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


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

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

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


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

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

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

+57
источник

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

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

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

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

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

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

+53
источник

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

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

Итак:

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

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

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

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

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

+33
источник

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]

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

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

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

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

+31
источник

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

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

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

+28
источник

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

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
+26
источник

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

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 у вас нет этой выгоды.

+25
источник

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

def a(): return []

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

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

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

+23
источник

Python: аргумент Mutable Default

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

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

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

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

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

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
+23
источник

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

  • Поведение атрибутов класса только для чтения при попытках назначения и
  • Функции - это объекты (хорошо объясненные в принятом ответе).

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

Простейшее обходное решение, использующее 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]
+19
источник

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

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

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

+17
источник

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

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

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

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 "Эффективный 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]

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

+17
источник

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

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

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

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

def foo(a=pavlo):
   ...

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

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

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

 >>> foo()
 [5]

Теперь pavlo - [5].

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

>>> foo()
[5, 5]

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

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

Итак, pavlo все еще [5, 5].

>>> foo()
[5, 5, 5]
+16
источник

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

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
источник

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

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

Уродливый, но он работает.

+15
источник

Может быть, что:

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

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

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

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

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

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

+12
источник

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

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

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

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

+10
источник

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

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

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let 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
источник

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

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
+8
источник

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

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

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

[] - это объект, поэтому python передает ссылку от [] на a, т.е. a является только указателем на [], который находится в памяти как объект. Существует только одна копия [] с, однако, многими ссылками на нее. Для первого foo() список [] изменен на 1 методом append. Но учтите, что существует только одна копия объекта списка, и этот объект теперь становится 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.

+7
источник

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


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

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

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

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

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

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

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

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

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...
+5
источник

Здесь очень тонкий вопрос. Спасибо за все понимание.

Я столкнулся с аналогичной проблемой и нашел исправление для этого.

Всегда безопасно очищать сосуд до начала приготовления

безопасная версия:

def foo(bird=[]):  
    del bird[:]  # <--- clean it
    bird.append('parrot')  
    return bird  

>>>> foo()       #1st call
    ['parrot']
>>>> foo()       #2nd call
    ['parrot']
>>>> foo()       #3rd call
    ['parrot']

>>>> foo()       #nth call
    ['parrot']

Теперь, независимо от количества вызов, это будет работать как "ожидаемый"

+4
  • 1
  • 2

Посмотрите другие вопросы по меткам или Задайте вопрос