python value function - «Наименьшее удивление» и параметр Mutable Default Argument





15 Answers

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

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

arguments predefined params

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

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

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

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

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

Изменить :

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

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

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

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

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




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

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

a = []

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

Почему a = [] в

def x(a=[]):

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

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

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

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

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

def x(static a=b):



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

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

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

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

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

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

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

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




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

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




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

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

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

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




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

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

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




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

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



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



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

def a(): return []

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

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

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




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

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.

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




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

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

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




Верно, что:

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

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

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

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

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

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




Я думаю, что ответ на этот вопрос заключается в том, как 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.




>>> 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 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. Принуждение пользователя ссылаться на него, abнапоминающее им, где находится список. Любой новый код, который хочет изменить ab, естественно будет помещен в класс, где он принадлежит.

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






Related