Питання "Найменше здивування" та аргумент, що змінюється за замовчуванням


Будь-хто, хто тривалий час маскується з Python, був укушений (або розірваний) наступним випуском:

def foo(a=[]):
    a.append(5)
    return a

Початківці Python очікували, що ця функція завжди поверне список лише одним елементом: [5]. Результат, навпаки, дуже різний і дуже вражаючий (для початківця):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Мій менеджер колись зустрівся з цією функцією і назвав його "драматичним дизайнерським недоліком" мови. Я відповів, що поведінка має основне пояснення, і це дійсно дуже загадкове та несподіване, якщо ви не розумієте внутрішні органи. Однак я не зміг відповісти (на себе) на таке запитання: якою є причина прив'язки аргументу за замовчуванням при визначенні функції, а не при виконанні функції? Я сумніваюся, що досвідчена поведінка практично використовується (хто дійсно використовував статичні змінні в C, без розмноження помилок?)

Редагувати:

Бачек зробив цікавий приклад. Разом з більшістю ваших коментарів і, зокрема, Утала, я докладніше розробив:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Мені здається, що дизайнерське рішення стосовно того, куди поставити сферу параметрів: всередині функції або "разом" з ним?

Включення зв'язування усередині функції означатиме це x ефективно прив'язується до заданого за замовчуванням, коли функція називається, не визначено, щось, що могло б представляти глибокий недолік: def лінія буде "гібридною" в тому сенсі, що частина зв'язування (об'єкта функції) відбудеться при визначенні, а частина (присвоєння параметрів за замовчуванням) при використанні часу виклику функції.

Фактична поведінка є більш послідовною: вся ця лінія стає оцінена, коли виконується така лінія, що означає визначення функції.


2057
2017-07-15 18:00


походження


Додаткове питання - Хороше використання для змінних аргументів за умовчанням - Jonathan
Я не сумніваюся, що мінливі аргументи порушують принцип найменшого здивування для середньої людини, і я бачив, як початківці сходили туди, а потім героїчно замінювали списки розсилки кореспонденціями. Тим не менше, змінні аргументи все ще відповідають Python Zen (Pep 20) і потрапляють в "очевидний для голландців" (зрозумілий / експлуатований твердими основними програмами пітона). Рекомендований шлях обходу з рядком doc є найкращим, однак стійкість до рядків документа та будь-які (написані) документи є настільки незвичайними в наші дні. Особисто я б вважав за краще декоратора (скажімо @fixed_defaults). - Serge
Мій аргумент, коли я натрапив на це, полягає в наступному: "Чому вам потрібно створити функцію, яка повертає змінну, яка, можливо, може бути змінною, яку ви перейдете до функції? Або це змінює мінливий або створює новий. Чому вам потрібно робити одночасно одну функцію? І чому перекладач слід переписати, щоб дозволити вам це зробити, не додаючи трьох рядків до вашого коду? " Тому що мова йде про переписування того, як інтерпретатор обробляє визначення функцій та виявлення тут. Це багато чого зробити для ледь необхідного випадку користування. - Alan Leuthard
"Початківці Python очікували, що ця функція завжди поверне список лише одним елементом: [5]"Я новачок у Python, і я не очікував цього, тому що очевидно foo([1]) повернеться [1, 5], ні [5]. Що ви мали на увазі сказати, що новачок очікував би функції викликається без параметрів завжди повернеться [5]. - symplectomorphic
Для Наприклад, підручник з Python, чому це if L is None: потрібно? Я зняв цей тест, і це не змінилося - sdaffa23fdsf


Відповіді:


Фактично, це не конструктивний недолік, і це не через внутрішні характеристики або продуктивність.
Це відбувається просто з того факту, що функції в Python - це об'єкти першого класу, а не лише фрагмент коду.

Як тільки ви подумаєте про це, це цілком має сенс: функція - це об'єкт, що оцінюється за його визначенням; Параметри за замовчуванням - це "дані учасника", тому їх стан може змінюватися від одного дзвінка до іншого - точно так само, як і в будь-якому іншому об'єкті.

У будь-якому випадку, Effbot має дуже хороше пояснення причин такої поведінки в Значення параметрів за замовчуванням у Python.
Я виявив це дуже чітко, і я дійсно рекомендую прочитати його для кращого знання того, як функціонують об'єкти роботи.


1353
2017-07-17 21:29



Якщо хтось прочитає наведену вище відповідь, я настійно рекомендую вам взяти час, щоб прочитати через пов'язану статтю Effbot. Як і вся інша корисна інформація, частина того, як ця функція мови може бути використана для кешування результатів / запам'ятовування, дуже зручна для знання! - Cam Jackson
Навіть якщо це першокласний об'єкт, він може все ще передбачати дизайн, де код для кожного значення за замовчуванням зберігається разом з об'єктом і повторно оцінюється кожного разу, коли виклику функції викликає. Я не кажу, що це буде краще, адже функції, які є об'єктами першого класу, не повністю перешкоджають цьому. - gerrit
Вибачте, але щось вважається "найбільшим WTF в Python" є найбільш виразно - конструктивний недолік. Це джерело помилок для кожен в той чи інший момент, тому що ніхто не очікує такої поведінки спочатку - а це означає, що він не повинен був бути розроблений таким чином з самого початку. Мені все одно, що обручі вони повинні були перестрибнути, вони повинен розробили Python так, щоб аргументи за замовчуванням були нестатичними. - BlueRaja - Danny Pflughoeft
Незалежно від того, чи є це дизайнерським недоліком, ваша відповідь, мабуть, передбачає, що ця поведінка є якось необхідною, природною і очевидною, з огляду на те, що функції є першокласними об'єктами, і це просто не так. Python має закриття. Якщо ви замінюєте аргумент за замовчуванням з призначенням у першому рядку функції, він оцінює вираз кожного виклику (потенційно використовуючи імена, оголошені в межах, що належать). Немає жодної причини, що неможливо або розумно оцінити аргументи за замовчуванням кожного разу, коли функція викликається точно таким же чином. - Mark Amery
Дизайн безпосередньо не випливає з functions are objects. У вашій парадигмі пропозиція полягає в тому, щоб застосувати значення за замовчуванням функцій як властивості, а не атрибути. - bukzor


Припустимо, що ви маєте наступний код

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Коли я бачу декларування їжі, найменш дивним є думка, що якщо перший параметр не буде вказано, він буде рівним кортежу ("apples", "bananas", "loganberries")

Проте, як передбачалося пізніше в коді, я роблю щось на зразок

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

тоді, якщо параметри за замовчуванням були пов'язані з виконанням функції, а не з функцією декларації, то я був би здивований (дуже поганим чином), щоб виявити, що фрукти були змінені. Це буде більш дивовижним ІМО, ніж виявити, що ваш fooФункція вище міняла список.

Реальна проблема полягає в змінних змінних, і всі мови мають певну міру цю проблему. Ось питання: припустимо, у Java я маю такий код:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Тепер моя карта використовує значення StringBuffer ключ, коли він був поміщений на карту, чи він зберігає ключ за посиланням? Так чи інакше, хтось здивований; або людина, яка намагалася вивести об'єкт з Map використовуючи значення, ідентичне тому, з яким вони вводять, або людину, яка, здається, не може отримати їх об'єкт, навіть якщо ключ, який вони використовують, є буквально тим самим об'єктом, який використовувався для його введення в карту (це насправді чому Python не дозволяє своїм змінним вбудованим типом даних використовуватись як словникові ключі).

Ваш приклад є гарним з тих випадків, коли новачки Python будуть здивовані та вкушені. Але я б стверджував, що якщо б ми "закріпили" це, то це лише створило б іншу ситуацію, коли їх закупорюватиме, і що це буде ще менш інтуїтивно. Крім того, це завжди трапляється при роботі з змінними змінними; ви завжди стикаєтесь із випадками, коли хтось інтуїтивно може очікувати одну чи іншу поведінку в залежності від того, який код вони пишуть.

Я особисто схожий на поточний підхід Python: аргументи функції за замовчуванням оцінюються, коли функція визначена, і цей об'єкт завжди є стандартним. Я думаю, вони могли б використовувати спеціальний футляр, використовуючи порожній список, але такий спеціальний корпус викликав би ще більше здивування, не кажучи вже про зворотне несумісність.


231
2017-07-15 18:11



Я думаю, це питання дебатів. Ви дієте на глобальну змінну. Будь-яка оцінка, виконана де-небудь у своєму коді, включаючи вашу глобальну змінну, тепер (правильно) посилається на ("чорниця", "манго"). параметр за умовчанням може бути як і будь-який інший випадок. - Stefano Borini
Насправді, я не думаю, що я згоден з вашим першим прикладом. Я не впевнений, що мені сподобалася ідея модифікації подібного ініціалізатора, але якщо я це зробив, я очікую, що вона поводиться точно так, як ви описали - зміна значення за умовчанням до ("blueberries", "mangos"). - Ben Blank
Параметр за замовчуванням є як і будь-який інший випадок. Що несподівано, це параметр глобальна змінна, а не локальна. Це, в свою чергу, пов'язано з тим, що код виконується при визначенні функції, а не викликати. Як тільки ви це отримаєте, і те ж саме стосується класів, це абсолютно зрозуміло. - Lennart Regebro
Я вважаю приклад не так блискучим, а оманливим. Якщо some_random_function() додається до fruits замість того, щоб присвоїти йому поведінку eat()  воля змінити Так багато для поточного чудового дизайну. Якщо ви використовуєте аргумент за промовчанням, на який посилається в іншому місці, а потім змінюєте посилання поза межами функції, ви питаєте проблеми. Реальний WTF - це коли люди визначають новий аргумент за промовчанням (список буквені або дзвінок до конструктора), і досі отримати трохи - alexis
Ви просто явно заявили global і перепризначив кортеж - абсолютно нічого не дивно, якщо eat після цього працює по-іншому. - user3467349


AFAICS ніхто ще не опублікував відповідну частину документація:

Значення параметрів за замовчуванням оцінюються, коли виконується визначення функції. Це означає, що вираз оцінюється один раз, коли визначається функція, і що для кожного виклику використовується однакове "попередньо обчислене" значення. Це особливо важливо зрозуміти, коли параметр за умовчанням є змінним об'єктом, таким як список або словник: якщо функція модифікує об'єкт (наприклад, додавши елемент до списку), значення за замовчуванням фактично змінюється. Це, як правило, не те, що малося на увазі. Спосіб усунення цього полягає в тому, щоб використовувати як параметр "немає" за замовчуванням і явно перевірити його в тілі функції [...]


195
2017-07-10 14:50



Фрази "це зовсім не те, що малося на увазі", а "шлях навколо цього" - запах, як вони документують недоліки дизайну. - bukzor
@ Маттеу: Я добре знаю, але це не варто. Як правило, ви бачите стилі довідників і шрифтів, що безумовно позначають змінні значення за замовчуванням як неправильні з цієї причини. Явним способом зробити те ж саме є накладання атрибуту на функцію (function.data = []) або ще краще, зробіть об'єкт. - bukzor
@bukzor: Необхідно відзначити та документувати підводні камені, і тому це питання добре, і він отримав стільки попереджень. У той же час, підводні камені не обов'язково потрібно видаляти. Скільки вже початківців Python пройшли список до функції, яка змінила його, і були шоковані, щоб побачити зміни, відображені у вихідній змінній? Проте змінні типи об'єктів чудові, коли ви розумієте, як їх використовувати. Я здогадуюсь, це просто зводиться до думки про цю конкретну пастку. - Matthew
Фраза "це зовсім не те, що мала на увазі" означає "не те, що програміст дійсно хотів трапитись", а "не те, що повинен робити Python". - holdenweb
@oriaadam Можливо, вам захочеться опублікувати питання про це тоді. Може бути, ви робите щось інше, ніж було задумано ... - glglgl


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

За умови, що об'єкти пітона мінливі Я думаю, що це слід враховувати при розробці стандартних аргументів. Коли ви створюєте список:

a = []

ти очікуєш отримати новий Список посилається на a.

Чому б a = [] в

def x(a=[]):

Екзаменувати новий список на визначення функції, а не на виклик? Це просто так, як ви запитуєте "якщо користувач не надає аргумент тоді екземпляр новий список і використовувати його так, ніби це було зроблено абонентом ". Я думаю, що це неоднозначно, а не:

def x(a=datetime.datetime.now()):

користувач, ти хочеш a за замовчуванням, до дати, що відповідає коли ви визначаєте чи виконуєте х? У цьому випадку, як і в попередньому, я буду зберігати таку саму поведінку, як якщо аргумент за замовчуванням "assignment" був першою інструкцією функції (datetime.now (), викликаною викликом функції). З іншого боку, якщо користувач хоче визначити часові відображення, він може написати:

b = datetime.datetime.now()
def x(a=b):

Я знаю, я знаю: це закриття. Як альтернатива, Python може надати ключове слово, щоб примусити час визначення визначення часу:

def x(static a=b):

97
2017-07-15 23:21



Ви могли б виконати: def x (a = None): а потім, якщо a є None, встановіть a = datetime.datetime.now () - Anon
Я знаю, це був лише приклад, щоб пояснити, чому я вважаю за краще прив'язувати час виконання. - Utaal
Дякую за це. Я не міг поставити палець на те, чому це не заважає мені закінчитись. Ви зробили це красиво з мінімальною пухкістю та плутаниною. Оскільки хтось виходить з системного програмування на C ++, а іноді і наївно "перекладає" мовні функції, цей фальшивий друг побив мене в м'якій частині голови великого часу, точно так само, як атрибути класу. Я розумію, чому все таке, але я не можу не любити його, незалежно від того, наскільки позитивне це може статися. Принаймні це настільки суперечить моєму досвіду, що я, імовірно, (сподіваюся) ніколи не забуду його ... - AndreasT
@Andreas, як тільки ви використовуєте Python досить довго, ви починаєте бачити, наскільки логічним є те, щоб Python тлумачив речі як класові атрибути так, як він це робить - це лише з-за особливих примх і обмежень таких мов, як C ++ (і Java і C # ...), що має сенс для вмісту class {} блок, що інтерпретується як приналежність до випадки:) Але коли класи є об'єктами першого класу, очевидно, природним є їх вміст (у пам'яті), щоб відобразити їх вміст (в коді). - Karl Knechtel
Нормативна структура не є примхом або обмеженням у моїй книзі. Я знаю, що це може бути незграбним і потворним, але можна назвати це "визначенням" чогось. Динамічні мови здаються мені анархістами: звичайно, кожен є вільним, але вам потрібна структура, щоб кожен звільнив сміття та прокладав дорогу. Вгадаю, я старий ... :) - AndreasT


Що ж, причина проста - прив'язки виконуються, коли виконується код, і виконується визначення функції, добре ... коли функції визначені.

Порівняйте це:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Цей код страждає від точно такої ж несподіваної випадковості. банани - це атрибут класу, а отже, коли ви додаєте речі до нього, він доданий до всіх екземплярів цього класу. Причина точно така ж.

Просто "як це працює", і зробити його роботою по-різному у випадку функцій, ймовірно, буде складно, а у випадку класу, ймовірно, неможливо або принаймні сповільнити екземпляр об'єкта, тому що вам доведеться тримати код класу навколо і виконувати його, коли створюються об'єкти.

Так, це несподівано. Але коли копія падає, вона чудово вписується в те, як працює Python взагалі. Насправді, це хороша навчальна допомога, і як тільки ви зрозумієте, чому це трапляється, ви будете краще пітона.

Це говорить про те, що воно повинно бути чітко виражено в будь-якому хорошому підручнику Python. Тому що, як ви згадуєте, всі рано чи пізно справляються з цією проблемою.


72
2017-07-15 18:54



Якщо для кожного прикладу це не так, це не атрибут класу. Атрибути класу є атрибутами класу. Звідси і назва Звідси вони однакові для всіх випадків. - Lennart Regebro
Він не просив опису поведінки Пітона, він запитував обгрунтування. Нічого в Python просто "як це працює"; все це робить те, що робить з причини. - Glenn Maynard
І я даю обгрунтування. - Lennart Regebro
Я б не сказав, що це "це хороша навчальна допомога", тому що це не так. - Geo
@Geo: окрім того, що це таке. Це допомагає вам зрозуміти багато речей у Python. - Lennart Regebro


Я думав, що створення об'єктів під час виконання буде кращим підходом. Мені вже невідомо, оскільки ви втрачаєте деякі корисні функції, хоча це може бути варто, незалежно просто, щоб запобігти плутанини новонароджених. Недоліками цього є:

1. Продуктивність

def foo(arg=something_expensive_to_compute())):
    ...

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

2. Примушування пов'язаних параметрів

Корисний трюк полягає в тому, щоб пов'язати параметри лямбда з струм прив'язка змінної, коли створюється лямбда. Наприклад:

funcs = [ lambda i=i: i for i in range(10)]

Це повертає список функцій, які повертають 0,1,2,3 ... відповідно. Якщо поведінка зміниться, вони замість цього зв'язуються i до час виклику значення i, тож ви отримаєте список всіх функцій, що повернулися 9.

Єдиний спосіб здійснити це іншим способом полягає в тому, щоб створити додаткове закриття з i bound, тобто:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Інтроспекція

Розглянемо код:

def foo(a='test', b=100, c=[]):
   print a,b,c

Ми можемо отримати інформацію про аргументи та за замовчуванням за допомогою inspect модуль, який

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Ця інформація дуже корисна для таких речей, як створення документів, метапрограмування, декоратори тощо.

Тепер припустимо, що поведінка за замовчуванням може бути змінена так, щоб це було еквівалентом:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

Проте ми втратили можливість самоаналізувати і подивитися, які аргументи за замовчуванням є. Оскільки об'єкти не були побудовані, ми не можемо їх затримати, не називаючи функцію. Найкраще, що ми можемо зробити, це зберегти вихідний код і повернути його як рядок.


50
2017-07-16 10:05



ви можете досягти самоаналізу, також, якщо для кожної існує функція створення аргументу за замовчуванням замість значення. модуль перевірки буде просто викликати цю функцію. - yairchu
@SilentGhost: Я говорю про те, чи було змінено поведінку, щоб відтворити його - одноразове створення - це поточна поведінка і чому існує помилка під час помилок. - Brian
@yairchu: Це припускає, що конструкція безпечна для цього (тобто не має побічних ефектів). Інтроспекція арг не повинна робити нічого, крім оцінки довільного коду, цілком може призвести до ефекту. - Brian
Інший дизайн мови часто означає лише написання речей по-різному. Ваш перший приклад можна легко записати як: _подорожній = дорогий (); def foo (аргумент = дешевий), якщо ви конкретно ні хочете переоцінити його. - Glenn Maynard
@Glenn - ось що я мав на увазі з "cache змінної зовнішньо" - це трохи докладніше, і в кінцевому підсумку з додатковими змінними у вашому просторі імен хоча. - Brian


5 очок у захисті Python

  1. Простота: Поведінка проста в такому сенсі: Більшість людей потрапляє в цю пастку лише один раз, а не кілька разів.

  2. Послідовність: Python завжди передає об'єкти, а не імена. Параметр за замовчуванням, очевидно, є частиною функції заголовок (не тіло функції). Тому його слід оцінювати при завантаженні модуля часу (і тільки при завантаженні модуля, якщо не вкладено), немає на час виклику функції

  3. Корисність: Як зазначив Фредерік Лундх у своєму поясненні від "Значення параметрів за замовчуванням в Python", the поточна поведінка може бути досить корисною для просунутого програмування. (Використовуйте економно.)

  4. Достатна документація: У найбільш основній документації Python підручник, проблема голосно оголошена як ан "Важливе попередження" в перший підрозділ розділу "Більше про визначення функцій". Попередження навіть використовує жирний шрифт, який рідко застосовується поза заголовками. RTFM: прочитайте правильний посібник.

  5. Мета-навчання: Падіння в пастку насправді дуже корисний момент (принаймні, якщо ви рефлексивний учень), тому що ви в подальшому краще зрозумієте цю точку "Послідовність" вище, що буде Навчіть вас багато чого про Python.


47
2018-03-30 11:18



Мені знадобилося рік, щоб виявити, що така поведінка перекриває мої коди на виробництві, і в кінцевому підсумку знімають повну функцію, поки я не випадково наткнувся на цей недолік проекту. Я використовую Джанго. Оскільки середовище створення не мав багато запитів, ця помилка ніколи не вплинула на QA. Коли ми вийшли жити і отримували багато одночасних запитів - деякі службові функції почали перезаписувати параметри один одного! Зробіть дірки з безпеки, помилки і що ні. - oriadam
@Ioradam, ніякого образу, але я цікаво, як ви дізналися Python, не вдаючись до цього раніше. Я просто вивчаю Python зараз, і це можлива пастка згадані в офіційному підручнику Python поряд з першою згадкою про аргументи за умовчанням. (Як згадувалося в пункті 4 цієї відповіді.) Я вважаю, що мораль, скоріше, несимпатично - читати офіційні документи мови, яку ви використовуєте для створення виробничого програмного забезпечення. - Wildcard
Крім того, було б дивно (для мене), якщо функція невідомої складності називалася додатково до виклику функції, який я роблю. - Vatine


Чому ти не бачиш себе?

Я є дійсно Здивований, ніхто не виконав виразну самоаналіз, запропонований Python (2 і 3 застосувати) на дзвінки.

Дана проста маленька функція func визначається як:

>>> def func(a = []):
...    a.append(5)

Коли Python стикається з ним, перше, що він зробить - це скомпілювати його, щоб створити code об'єкт для цієї функції. Поки цей етап компіляції завершено, Python оцінює* і потім магазини аргументи за умовчанням (порожній список [] тут) в самому об'єкті функції. Як зазначила головна відповідь: список a тепер можна вважати а член функції func.

Отже, давайте зробимо деяку інтроспекцію, а до і після, щоб дізнатися, як розширюється список всередині об'єкт функції Я використовую Python 3.x для цього для Python 2 те ж саме застосовується (використовуйте __defaults__ або func_defaults в Python 2; так, дві назви для однієї і тієї ж речі).

Функція перед виконанням:

>>> def func(a = []):
...     a.append(5)
...     

Після того, як Python виконує це визначення, він буде приймати будь-які параметри за замовчуванням (a = [] тут) і напишіть їх у __defaults__ атрибут для об'єкта функції (відповідний розділ: виклики):

>>> func.__defaults__
([],)

O.k, так що порожній список як єдиний запис у __defaults__, як і очікувалося.

Функція після виконання:

Тепер виконаємо цю функцію:

>>> func()

Тепер давайте подивимось на них __defaults__ знову:

>>> func.__defaults__
([5],)

Здивований Значення всередині об'єкта змінюється! Послідовні виклики функції тепер будуть просто додаватися до вбудованого list об'єкт:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Отже, у вас є це, причина, чому це "дефект" буває, тому що аргументи за замовчуванням є частиною об'єкта функції. Тут нічого дивного не відбувається, це все лише трохи дивно.

Загальне рішення для боротьби з цим є звичайним None як за замовчуванням, а потім ініціалізувати в тілі функції:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Оскільки тіло функції виконується заново кожен раз, ви завжди отримуєте новий порожній список, якщо для аргументу немає a.


Щоб ще більше підтвердити, що список в __defaults__ такий самий, як і у функції func ви можете просто змінити свою функцію, щоб повернути id зі списку a використовується всередині тіла функції. Потім порівняйте його зі списком в __defaults__ (позиція [0] в __defaults__), і ви побачите, як це насправді стосується того ж самого списку:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Все з силою самоаналізу!


* Щоб переконатися, що Python оцінює аргументи за замовчуванням під час складання функції, спробуйте виконати наступне:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

як ви помітите input() викликається перед процесом побудови функції та зв'язування його з іменем bar зроблено


43
2017-12-09 07:13



Є id(...) потрібна для цієї останньої перевірки, чи буде вона is Оператор відповідає на одне й те ж питання? - das-g
@ das-g is зробив би просто добре, я просто використовував id(val) тому що я думаю, що це може бути більш інтуїтивно зрозумілим. - Jim Fasarakis Hilliard
@JimFasarakisHilliard Я хотів би додати підказку до input. люблю input('Did you just see me without calling me?'). Це робить це більш чітким. - Ev. Kounis
@ Ev.Kounis мені це подобається! Дякую, що вказали на це. - Jim Fasarakis Hilliard


Таку поведінку легко пояснити:

  1. Декларація функції (клас тощо) виконується лише один раз, створюючи всі об'єкти за умовчанням
  2. все проходить за посиланням

Так:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a не змінюється - кожен виклик призначення виконує створення нового int-об'єкта - друкується новий об'єкт
  2. b не змінюється - новий масив побудований за замовчуванням і друкується
  3. c зміни - операція виконується на тому ж об'єкті - і вона друкується

40
2017-07-15 19:15



Ваша №4 може бути заплутаною людям, оскільки цілі числа незмінними і тому, що "якщо" не відповідає дійсності. Наприклад, коли d встановлено на 0, d .__ додати __ (1) поверне 1, але d все одно буде 0. - Anon
(Фактично, додати це поганий приклад, але цілі числа залишаються незмінними, все-таки мій основний момент.) - Anon
так, це не був хорошим прикладом - ymv
Реалізував це на мій шахрайство після перевірки, щоб побачити, що, коли b встановлено на [], b .__ додати __ ([1]) повертає [1], але також залишає b ще [], навіть якщо списки змінюються. Моє ліжко. - Anon
@ Анон: є __iadd__, але це не працює з int. Звичайно. :-) - Veky