Почему аргументы по умолчанию вычисляются во время определения в Python?

StackOverflow https://stackoverflow.com/questions/1651154

  •  22-07-2019
  •  | 
  •  

Вопрос

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

Код выглядит следующим образом:

class Node(object):
    def __init__(self, children = []):
        self.children = children

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

>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176

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

Это было полезно?

Решение

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

def ack(m, n, _memo={}):
  key = m, n
  if key not in _memo:
    if m==0: v = n + 1
    elif n==0: v = ack(m-1, 1)
    else: v = ack(m-1, ack(m, n-1))
    _memo[key] = v
  return _memo[key]

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

for i in range(len(buttons)):
  buttons[i].onclick(lambda i=i: say('button %s', i))

... простой i = i , опирающийся на раннее связывание (время определения) значений arg по умолчанию, является тривиально простым способом раннего связывания. Итак, текущее правило простое, прямое и позволяет вам делать все, что вы хотите, способом, который чрезвычайно легко объяснить и понять: если вы хотите позднее связывание значения выражения, оцените это выражение в теле функции; если вы хотите раннее связывание, оцените его как значение по умолчанию для аргумента.

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

Другими словами, "должен существовать один, а предпочтительно только один, очевидный способ сделать это [1]": если вы хотите позднее связывание, уже есть совершенно очевидный способ достичь этого (поскольку все код функции выполняется только во время вызова, очевидно, все, что оценивается там , имеет позднюю привязку); оценка по умолчанию-arg приводит к раннему связыванию и дает очевидный способ достижения раннего связывания (плюс! -), а не ДВА очевидных способа позднего связывания и никакого очевидного способа раннего связывания (минус! -).

[1]: " Поначалу этот путь может быть неочевидным, если вы не голландец. "

Другие советы

Проблема заключается вот в чем.

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

  • 0 это простой литерал.Оцените это один раз, используйте навсегда.

  • int это функция (например, list), которая должна была бы оцениваться каждый раз, когда она требуется в качестве инициализатора.

Конструкция [] является буквальным, как 0, что означает "именно этот объект".

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

Было бы непосильным бременем добавлять необходимые if заявление о необходимости проводить эту оценку постоянно.Лучше принимать все аргументы как литералы и не выполнять никакой дополнительной оценки функции как части попытки выполнить оценку функции.

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

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

[Это было бы параллельным путем collections.defaultdict работает.]

def aFunc( a=another_func ):
    return a*2

def another_func( b=aFunc ):
    return b*3

В чем ценность another_func()?Чтобы получить значение по умолчанию для b, он должен оценить aFunc, для чего требуется оценка another_func.Упс.

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

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

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children

Обходной путь для этого, обсуждается здесь (и очень твердый), есть:

class Node(object):
    def __init__(self, children = None):
        self.children = [] if children is None else children

Что касается того, зачем искать ответ от von L & # 246; wis, но, скорее всего, потому, что определение функции создает объект кода из-за архитектуры Python, и не может быть средства для работы с ссылочными типами, подобными этому в аргументах по умолчанию.

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

Функция - это объект. Во время загрузки Python создает объект функции, оценивает значения по умолчанию в операторе def , помещает их в кортеж и добавляет этот кортеж в качестве атрибута функции с именем func_defaults , Затем, когда вызывается функция, если в вызове не указано значение, Python получает значение по умолчанию из func_defaults .

Например:

>>> class C():
        pass

>>> def f(x=C()):
        pass

>>> f.func_defaults
(<__main__.C instance at 0x0298D4B8>,)

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

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

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

def f(x = None):
   if x == None:
      x = g()

Это связано с тем, что Python делает упор на синтаксис и простоту выполнения. Оператор def происходит в определенный момент во время выполнения. Когда интерпретатор python достигает этой точки, он оценивает код в этой строке, а затем создает объект кода из тела функции, который будет запущен позже при вызове функции.

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

funcs = []
for x in xrange(5):
    def foo(x=x, lst=[]):
        lst.append(x)
        return lst
    funcs.append(foo)
for func in funcs:
    print "1: ", func()
    print "2: ", func()

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

1:  [0]
2:  [0, 0]
1:  [1]
2:  [1, 1]
1:  [2]
2:  [2, 2]
1:  [3]
2:  [3, 3]
1:  [4]
2:  [4, 4]

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

Отредактировано для добавления: Подробнее об этом см. статью effbot здесь: http: // effbot.org/zone/default-values.htm и описание языка здесь: http://docs.python.org/reference/compound_stmts.html#function

Определения функций Python - это просто код, как и весь другой код; они не "волшебные" таким образом, что некоторые языки. Например, в Java вы можете сослаться на «сейчас» к чему-то определенному «позже»:

public static void foo() { bar(); }
public static void main(String[] args) { foo(); }
public static void bar() {}

но в Python

def foo(): bar()
foo()   # boom! "bar" has no binding yet
def bar(): pass
foo()   # ok

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

Потому что, если бы они имели, то кто-то мог бы написать вопрос, спрашивающий, почему это не было наоборот :-p

Предположим теперь, что они имели. Как бы вы реализовали текущее поведение в случае необходимости? Легко создавать новые объекты внутри функции, но вы не можете " создать " их (вы можете удалить их, но это не одно и то же).

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

  

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

Мне трудно в это поверить. Если назначения аргументов по умолчанию, такие как foo = 'some_string' , действительно добавляют неприемлемое количество служебных данных, я уверен, что было бы возможно определить назначения неизменяемым литералам и предварительно вычислить их.

  

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

Хотя в отдельных случаях это может быть проблематично, в качестве шаблона проектирования он не очень элегантен. Он добавляет стандартный код и скрывает значения аргументов по умолчанию. Такие шаблоны, как foo = foo или ... не работают, если foo может быть объектом, похожим на массив с неопределенным значением истинности. А в ситуациях, когда None является значимым значением аргумента, которое может быть передано преднамеренно, его нельзя использовать в качестве дозорного, и этот обходной путь становится действительно уродливым.

  

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top