Почему аргументы по умолчанию оцениваются во время определения в Python? - PullRequest
34 голосов
/ 30 октября 2009

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

Ответы [ 8 ]

38 голосов
/ 30 октября 2009

Альтернатива была бы довольно тяжелой - хранение «значений аргументов по умолчанию» в объекте функции в виде «фрагментов» кода, который будет выполняться снова и снова при каждом вызове функции без указания значения для этого аргумента - и было бы намного труднее получить раннее связывание (связывание во время определения времени), что часто является тем, чего вы хотите. Например, в 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]: «Хотя этот путь поначалу может быть неочевидным, если вы не голландец».

10 голосов
/ 30 октября 2009

Проблема в следующем.

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

  • 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, что требует значения eval another_func. К сожалению.

7 голосов
/ 30 октября 2009

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

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

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children
7 голосов
/ 30 октября 2009

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

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

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

5 голосов
/ 31 октября 2009

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

Это происходит из-за того, что 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

0 голосов
/ 31 октября 2009

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

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

0 голосов
/ 30 октября 2009

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

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

...