Подтвердить что ты не робот

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

Мне очень трудно было понять основную причину проблемы в алгоритме. Затем, упростив функции шаг за шагом, я обнаружил, что оценка аргументов по умолчанию в 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 решили, что аргументы по умолчанию должны оцениваться во время определения? Это кажется мне очень противоречивым.

4b9b3361

Ответ 1

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

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

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

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

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

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

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

Ответ 2

Проблема в этом.

Слишком дорого оценить функцию как инициализатор при каждом вызове функции.

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

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

Конструкция [] является литералом, например 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. К сожалению.

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

Я думал, что это тоже противоречиво, пока я не узнал, как 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()

Ответ 6

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

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

Ответ 7

Определения функций 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

Итак, аргумент по умолчанию оценивается в момент оценки этой строки кода!

Ответ 8

Потому что, если бы они были, то кто-то задавал вопрос, почему это не так: -p

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

Ответ 9

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

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

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

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

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

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

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

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

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