Аргумент за замовчуванням, що змінюється 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
Wildcard

Відповіді:


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 Pulakaka

Зіткнувшись із чим-небудь складним із залученням змінних у 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=[])стає екземпляром цієї функції при виклику і повторно використовується для подальших дзвінків. Розгляньте параметри функції як розрізнення атрибутів об'єкта.

Професійні програми можуть включати використання цього класу, щоб мати класи-статичні змінні у 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 в даний час зазначає, що аргументи за замовчуванням трактуються таким, яким вони є; це однаково добре може стверджувати, що аргументи за замовчуванням трактуються інакше. IOW ви відповідаєте "ось так і є справи" на питання "чому такі речі є такими, якими вони є".

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