Изменяемый аргумент Python по умолчанию: почему?


20

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

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

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


Это уже обсуждалось в удивительной детализации переполнения стека: stackoverflow.com/q/1132941/5419599
Подстановочный

Ответы:


14

Я думаю, что причина в простоте реализации. Позвольте мне уточнить.

Значением функции по умолчанию является выражение, которое необходимо оценить. В вашем случае это простое выражение, которое не зависит от замыкания, но может быть чем-то, что содержит свободные переменные - def ook(item, lst = something.defaultList()). Если вы хотите разработать Python, у вас будет выбор - оценивать ли вы его один раз, когда функция определена, или каждый раз, когда функция вызывается. Python выбирает первый (в отличие от Ruby, который идет со вторым вариантом).

Есть некоторые преимущества для этого.

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

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


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

1
Самое удивительное в этом заключается в следующем: если у вас есть семантика оценки при каждом вызове, вы можете получить ошибку, если замыкание меняется между двумя вызовами функций (что вполне возможно). Это может быть более удивительным, чем знать, что оно оценивается один раз. Конечно, можно утверждать, что когда вы приехали из других языков, вы, кроме семантики оценки по каждому вызову, и это является сюрпризом, но вы можете видеть, как это происходит в обоих направлениях :)
Стефан Канев

Хороший вопрос о сфере применения
0xc0de

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

5

Один из способов выразить это в том, что параметр lst.append(item) не изменяет lstпараметр. lstвсе еще ссылается на тот же список. Просто содержимое этого списка было видоизменено.

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

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

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


+1 Точно. Важно понимать уровень косвенности - переменная не является значением; вместо этого он ссылается на значение. Чтобы изменить переменную, значение может быть заменен , или мутировали (если это изменяемые).
Joonas Pulakka

Когда я сталкиваюсь с чем-то хитрым, связанным с переменными в python, я считаю полезным рассматривать «=» как «оператор привязки имени»; имя всегда восстанавливается независимо от того, является ли объект, с которым мы его связываем, новым (новый объект или экземпляр неизменяемого типа) или нет.
StarWeaver

4

Какие преимущества дает такое поведение по сравнению с инициализацией при каждом вызове?

Это позволяет вам выбрать поведение, которое вы хотите, как вы продемонстрировали в своем примере. Поэтому, если вы хотите, чтобы аргумент по умолчанию был неизменным, вы должны использовать неизменяемое значение , такое как Noneили 1. Если вы хотите сделать изменяемый аргумент по умолчанию, вы используете что-то изменяемое, например []. Это просто гибкость, хотя и по общему признанию, она может укусить, если вы этого не знаете.


2

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

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

что дает, [2, 2, 2]где вы ожидаете, что настоящее закрытие производства [0, 1, 2].

Мне бы очень хотелось, чтобы в Python была возможность обернуть параметр по умолчанию в замыканиях. Например:

def foo(a, b=a.b):
    ...

Было бы очень удобно, но «a» не входит в область действия во время определения функции, так что вы не можете сделать это, а вместо этого должны сделать неуклюжий:

def foo(a, b=None):
    if b is None:
        b = a.b

Что почти то же самое ... почти.



1

Огромная выгода - это запоминание. Это стандартный пример:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

и для сравнения:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Время измерения в ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s

0

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

Если один сказал

def f(x = {}):
    ....

было бы довольно ясно, что вы каждый раз хотели новый массив.

Но что, если я скажу:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

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

Но как компилятор будет догадываться об этом? Так зачем пытаться? Мы могли бы положиться на то, было ли это названо или нет, и это могло бы иногда помочь, но на самом деле это было бы просто предположение. В то же время, есть веская причина, чтобы не пытаться - последовательность.

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

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


-5

Это происходит потому, что функции в Python являются объектами первого класса :

Значения параметров по умолчанию оцениваются при выполнении определения функции. Это означает, что выражение вычисляется один раз , когда функция определена, и что для каждого вызова используется одно и то же «предварительно вычисленное» значение .

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

Это означает, def foo(l=[])что при вызове становится экземпляром этой функции и повторно используется для дальнейших вызовов. Думайте о параметрах функции как об отделении от атрибутов объекта.

Pro может включать использование этого, чтобы классы имели C-подобные статические переменные. Поэтому лучше всего объявить значения по умолчанию None и инициализировать их при необходимости:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

выходы:

[5] [5] [5] [5]

вместо неожиданного

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]


5
Нет. Вы можете определять функции (первоклассные или нет) по-разному, чтобы снова вычислять выражение аргумента по умолчанию для каждого вызова. И все после этого, то есть примерно 90% ответа, совершенно не относится к вопросу. -1

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

2
На уровне языкового дизайна я имею в виду. В определении языка Python в настоящее время говорится, что аргументы по умолчанию обрабатываются так, как они есть; с таким же успехом можно утверждать, что аргументы по умолчанию обрабатываются другим способом. Теперь вы отвечаете «так обстоят дела» на вопрос «почему вещи такие, какие они есть».

Python мог бы реализовать параметры по умолчанию, аналогично тому, как это делает Coffeescript. Он будет вставлять байт-код для проверки отсутствующих параметров, а если они будут отсутствовать, будет вычислять выражение.
Уинстон Эверт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.