"Найменше здивування" та змінна аргументація за замовчуванням


2593

Кожен, хто поводився з 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()

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

Редагувати :

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

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

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

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

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



4
Я не сумніваюся, що мінливі аргументи порушують принцип найменшого здивування для пересічної людини, і я бачив, як початківці крокують туди, а потім героїчно замінюють списки розсилки поштовими картками. Тим не менш, змінні аргументи все ще узгоджуються з Python Zen (Pep 20) і потрапляє в пункт "очевидний для голландців" (зрозумілий / експлуатований жорсткими ядрами програмістів python). Рекомендований спосіб вирішення doc-рядка є найкращим, проте опір до doc-струн та будь-яких (письмових) документів не так вже й рідкість. Особисто я віддаю перевагу декоратору (скажімо @fixed_defaults).
Серж

5
Мій аргумент, коли я стикаюсь з цим, такий: "Чому вам потрібно створити функцію, яка повертає мутант, який, за бажанням, може бути мутаційним, ви переходите до функції? Або він змінює мутабельний або створює новий. Для чого вам потрібен робити обидві з однією функцією? І чому перекладач повинен бути переписаний, щоб дозволити це робити, не додаючи до коду три рядки? " Тому що ми говоримо про переписання способу обробки перекладача функціонування тут. Це дуже багато зробити для ледве необхідного випадку використання.
Алан Лейтхард

12
"Початківці Python очікують, що ця функція завжди поверне список лише з одним елементом: [5]" Я новачок Python, і я цього не очікував, бо, очевидно foo([1]), повернеться [1, 5], ні [5]. Що ви хотіли сказати, це те, що новачок очікує, що функція, яка називається без параметра , завжди повернеться [5].
symplectomorphic

2
Це запитання задає "Чому це [неправильний шлях] реалізувався так?" Він не запитує "Що це за правильний шлях?" , на яку поширюється [ Чому використання arg = None не виправляє змінний аргумент за замовчуванням Python? ] * ( stackoverflow.com/questions/10676729/… ). Нові користувачі майже завжди менш зацікавлені в першому і набагато більше в другому, тому іноді це дуже корисне посилання / дуп.
smci

Відповіді:


1612

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

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

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


80
Кому-небудь, хто читає вищезазначену відповідь, я настійно рекомендую витратити час, щоб прочитати пов'язану статтю Effbot. Як і вся інша корисна інформація, детально про те, як цю функцію мови можна використовувати для кешування результатів / запам'ятовування, дуже зручно знати!
Cam Jackson

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

312
Вибачте, але все, що вважається "Найбільшим WTF в Python" - це, безумовно, недолік дизайну . Це джерело помилок для всіх в якийсь момент, тому що спочатку ніхто не очікує такої поведінки - це означає, що вона не повинна була бути спроектована саме так. Мені байдуже, через які обручі вони повинні були перестрибнути, вони повинні були розробити Python так, що аргументи за замовчуванням нестатичні.
BlueRaja - Danny Pflughoeft

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

24
Дизайн безпосередньо не випливає з цього functions are objects. У вашій парадигмі пропозиція полягає в тому, щоб реалізувати значення за замовчуванням функцій як властивості, а не атрибути.
bukzor

273

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

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

def eat(food=fruits):
    ...

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

Однак, як передбачається згодом у коді, я роблю щось подібне

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

то якби параметри за замовчуванням були пов'язані при виконанні функції, а не в оголошенні функції, я би здивувався (дуже погано), щоб виявити, що фрукти були змінені. Це було б більш дивовижним IMO, ніж виявлення того, що ваша 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: аргументи функції за замовчуванням оцінюються, коли функція визначена, і цей об'єкт завжди є за замовчуванням. Я гадаю, що вони могли використовувати особливий випадок, використовуючи порожній список, але такий вид спеціального корпусу викликав би ще більше подив, не кажучи вже про назад несумісні.


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

47
Власне, я не думаю, що я згоден з вашим першим прикладом. Я не впевнений, що мені подобається в першу чергу ідея зміни ініціалізатора на зразок такої, але якби я це зробив, я би сподівався, що він буде вести себе так, як ви описуєте - змінивши значення за замовчуванням на ("blueberries", "mangos").
Бен Бланк

12
Параметр за замовчуванням , як і будь-який інший випадок. Що несподівано - це те, що параметр є глобальною змінною, а не локальною. Що в свою чергу тому, що код виконується при визначенні функції, а не при виклику. Як тільки ви це зрозумієте, і те саме стосується занять, це абсолютно зрозуміло.
Леннарт Регебро

17
Я вважаю приклад оманливим, а не геніальним. Якщо some_random_function()дописує fruitsзамість присвоєння йому поведінку eat() буде змінюватися. Стільки для нинішнього чудового дизайну. Якщо ви використовуєте аргумент за замовчуванням, на який посилається в іншому місці, а потім змінюєте посилання поза функцією, ви задаєте проблеми. Справжній WTF - це коли люди визначають свіжий аргумент за замовчуванням (буквальний список або виклик конструктору) і все-таки отримують біт.
алексис

13
Ви просто явно декларували globalі перепризначали кортеж - нічого дивного немає, якщо він eatпрацює інакше.
користувач3467349

241

Відповідна частина документації :

Значення параметрів за замовчуванням оцінюються зліва направо, коли виконується визначення функції. Це означає, що вираз оцінюється один раз, коли функція визначена, і що для кожного виклику використовується те саме «попередньо обчислене» значення. Це особливо важливо для розуміння, коли параметр за замовчуванням є об'єктом, що змінюється, таким як список або словник: якщо функція змінює об'єкт (наприклад, додаючи елемент до списку), значення за замовчуванням фактично змінюється. Це взагалі не те, що було призначено. Шляхом цього є використання Noneв якості за замовчуванням та явне тестування на нього в тілі функції, наприклад:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
Фрази "це взагалі не те, що було призначено" і "шлях цього" є запахом, ніби вони документують ваду дизайну.
bukzor

4
@Matthew: Я добре знаю, але цього не варто. Як правило, з цього приводу ви побачите, що посібники стилів та підводки беззастережно позначають значення, що змінюються за замовчуванням, як змінні. Явний спосіб зробити те ж саме - набити атрибут у функцію ( function.data = []), а ще краще зробити об’єкт.
bukzor

6
@bukzor: Підводні камені потрібно помітити та задокументувати, тому це питання добре і отримало так багато відгуків. У той же час, підводні камені не обов’язково видаляти. Скільки початківців Python передали список функції, яка модифікувала його, і були шоковані тим, що зміни відображаються в оригінальній змінній? Але типи об'єктів, що змінюються, чудові, коли ти розумієш, як ними користуватися. Я здогадуюсь, що це просто зводиться до думки щодо цієї конкретної проблеми.
Метью

33
Словосполучення "це взагалі не те, що було призначено" означає "не те, що програміст насправді хотів статися", а не "не те, що повинен робити Python".
holdenweb

4
@holdenweb Вау, я запізнююсь на вечірку. Зважаючи на контекст, bukzor абсолютно прав: вони документують поведінку / наслідок, який не був "призначений", коли вони вирішили, що мова повинна виконувати визначення функції. Оскільки це ненавмисний наслідок їх вибору дизайну, це недолік дизайну. Якби не вада дизайну, не було б потреби навіть пропонувати "шлях цього".
code_dredd

118

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

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

a = []

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

Чому a=[]в

def x(a=[]):

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

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

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

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

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

def x(static a=b):

11
Ви можете зробити: def x (a = None): І тоді, якщо a є None, встановіть a = datetime.datetime.now ()
Anon

20
Дякую за це. Я не міг покласти пальця на те, чому це дратує мене без кінця. Ви зробили це красиво з мінімумом нечіткості та розгубленості. Оскільки хтось виходив із систем, які програмували на C ++, а іноді і наївно "перекладали" мовні особливості, цей неправдивий друг великого часу мене виганяв у м'якість голови, як і атрибути класу. Я розумію, чому все так, але я не можу не любити це, не дивлячись на те, що з цього виходить позитиву. Принаймні, це так суперечить моєму досвіду, що я, мабуть, сподіваюся, ніколи його не забуду ...
AndreasT

5
@Andreas, коли ви досить довго використовуєте Python, ви починаєте бачити, наскільки логічно Python інтерпретувати речі як атрибути класу так, як це робиться - це лише через певні химерності та обмеження таких мов, як C ++ (і Java, і C # ...), що має сенс class {}інтерпретувати вміст блоку як належність до екземплярів :) Але коли класи - це об'єкти першого класу, очевидно, що природним є їх вміст (у пам'яті), щоб відображати їхній вміст (у коді).
Карл Кнечтел

6
Нормативна структура не є вигадкою чи обмеженням у моїй книзі. Я знаю, що це може бути незграбно і потворно, але ви можете назвати це "визначення" чогось. Динамічні мови мені здаються схожими на анархістів: Впевнені, що всі вільні, але вам потрібна структура, щоб хтось спорожнів сміття та проклав дорогу. Здогадайтесь, я старий ... :)
AndreasT

4
Функція визначення виконується в модулі часу завантаження. Тіло функції виконується під час виклику функції. Аргумент за замовчуванням є частиною визначення функції, а не тіла функції. (Це ускладнюється для вкладених функцій.)
Lutz Prechelt

84

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

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

class BananaBunch:
    bananas = []

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

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

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

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

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


Як визначити атрибут класу, який відрізняється для кожного примірника класу?
Kieveli

19
Якщо він різний для кожного примірника, це не атрибут класу. Атрибути класу - це атрибути на КЛАСІ. Звідси і назва. Отже, вони однакові для всіх примірників.
Леннарт Регебро

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

@Kievieli: Ви говорите про звичайні змінні класу класу. :-) Ви визначаєте атрибути екземпляра, кажучи self.attribute = значення у будь-якому методі. Наприклад __init __ ().
Леннарт Регебро

@Kieveli: дві відповіді: ви не можете, тому що будь-яка річ, яку ви визначаєте на рівні класу, буде атрибутом класу, а будь-який екземпляр, який має доступ до цього атрибута, матиме доступ до того ж атрибута класу; ви можете / сортувати /, використовуючи propertys - які насправді є функціями рівня класу, які діють як звичайні атрибути, але зберігають атрибут в екземплярі замість класу (використовуючи, self.attribute = valueяк сказав Леннарт).
Етан Фурман

66

Чому ви не задумаєтесь?

Я дуже здивований, що ніхто не виконував глибокої самоаналізу, запропонованої 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__атрибут для об'єкта функції (відповідний розділ: Callables):

>>> func.__defaults__
([],)

Гаразд, такий порожній список, як єдиний запис __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 .


1
Чи id(...)потрібна ця остання перевірка, чи isоператор відповів би на те саме запитання?
дас-г

1
@ das-g isзробив би добре, я просто використовував, id(val)тому що я думаю, що це може бути більш інтуїтивно зрозумілим.
Дімітріс Фасаракіс Хілліард

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

58

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

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-меж, тобто:

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=[]

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


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

@SilentGhost: Я говорю про те, чи змінили поведінку, щоб її відтворити - створити її один раз - це поточна поведінка, і чому існує змінна проблема за замовчуванням.
Брайан

1
@yairchu: Це передбачає, що конструкція безпечна для цього (тобто не має побічних ефектів). Огляд аргументів не повинен нічого робити , але оцінка довільного коду може призвести до ефекту.
Брайан

1
Інший мовний дизайн часто просто означає писати речі по-різному. Ваш перший приклад легко можна записати як: _expensive = скъпо (); def foo (arg = _expensive), якщо ви спеціально не хочете, щоб його переоцінювали.
Гленн Мейнард

@Glenn - це те, про що я мав на увазі "кешування змінної зовнішньо" - це трохи більш багатослівно, і ви в кінцевому підсумку маєте додаткові змінні у вашому просторі імен.
Брайан

55

5 балів на захист Пітона

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

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

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

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

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


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

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

Крім того, було б дивно (для мене), якби виклик функції, який я роблю, був викликаний функцією невідомої складності.
Ватін

52

Така поведінка легко пояснюється:

  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 зміни - операція виконується на одному об’єкті - і вона друкується

(Насправді, додавання - це поганий приклад, але головне моє значення залишається незмінним цілим числом.)
Anon,

Зреагувавши на це, коли я перевірив, що з b, встановленим на [], b .__ додати __ ([1]) повертає [1], але також залишає b ще [], хоча списки змінюються. Моє ліжко.
Anon

@ANon: є __iadd__, але він не працює з int. Звичайно. :-)
Veky

35

Що ви запитуєте, чому це:

def func(a=[], b = 2):
    pass

не є рівнозначним цьому:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

за винятком явного виклику func (None, None), який ми ігноруємо.

Іншими словами, замість того, щоб оцінювати параметри за замовчуванням, чому б не зберігати кожен з них, а не оцінювати їх, коли функція викликається?

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


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

10
Щоправда, але це все-таки уповільнить Python, і це насправді буде досить дивно, якщо ви не зробите те саме для визначення класів, що зробить це дурно повільним, оскільки вам доведеться повторно виконувати визначення цілого класу щоразу, коли інстанціювати клас. Як було сказано, виправлення було б більш дивовижним, ніж проблема.
Леннарт Регебро

Домовились з Леннартом. Як любить говорити Гвідо, для кожної мовної функції або стандартної бібліотеки хтось там використовує її.
Джейсон Бейкер

6
Змінити його зараз було б божевіллям - ми просто вивчаємо, чому саме так воно і є. Якщо б це почалося з пізньої оцінки за замовчуванням для початку, це не обов'язково дивуватиметься. Безумовно, правда, що така суттєва різниця в синтаксичному розборі мала б великі та, мабуть, багато незрозумілі наслідки для мови в цілому.
Гленн Мейнард

35

1) Так звана проблема "змінного аргументу за замовчуванням" взагалі є спеціальним прикладом, що демонструє, що:
"Усі функції з цією проблемою також страждають від подібної проблеми побічного ефекту на фактичний параметр ".
Це суперечить правилам функціонального програмування, як правило, небажано і їх слід фіксувати разом.

Приклад:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Рішення : копія
Абсолютно безпечне рішення спочатку - copyабо deepcopyвхідного об'єкта, а потім робити все, що потрібно з копією.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Багато вбудованих змінних типів мають такий метод копіювання, як some_dict.copy()або some_set.copy()або його можна легко скопіювати, як somelist[:]або list(some_list). Кожен об'єкт також може бути скопійований copy.copy(any_object)або більш ретельним copy.deepcopy()(останній корисний, якщо об'єкт, що змінюється, складається із об'єктів, що змінюються). Деякі об'єкти принципово базуються на таких побічних ефектах, як об'єкт "файл", і їх неможливо відтворити копією.копіювання

Приклад проблеми для аналогічного запитання SO

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

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

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

2)
Тільки якщо потрібний побічний ефект на фактичний параметр, але небажаний для параметра за замовчуванням, тоді корисне рішення - def ...(var1=None): if var1 is None: var1 = [] Більше ..

3) У деяких випадках корисна зміна параметрів за замовчуванням .


5
Сподіваюся, ви знаєте, що Python не є функціональною мовою програмування.
Векі

6
Так, Python - це багатопарагігмна мова з деякими функціональними особливостями. ("Не робіть будь-яку проблему схожою на цвях лише тому, що у вас є молоток.") Багато з них є найкращими практиками Python. У Python є цікаве функціональне програмування HOWTO. Інші особливості - це закриття та витримка, тут не згадується.
hynekcer

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

1
@holdenweb Я згоден. Тимчасова копія є найбільш звичайним способом, а іноді і єдиним можливим способом захисту вихідних даних, що змінюються, від сторонніх функцій, які потенційно змінюють їх. На щастя, функція, яка необґрунтовано модифікує дані, вважається помилкою і тому є рідкісною.
hynekcer

Я згоден з цією відповіддю. І я не розумію, чому def f( a = None )конструкція рекомендується, коли ти справді маєш на увазі щось інше. Копіювання нормально, тому що ви не повинні мутувати аргументи. І коли ви це робите if a is None: a = [1, 2, 3], ви все одно копіюєте список.
кодо

30

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

>>> def foo(a):
    a.append(5)
    print a

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

У цьому коді немає видимих ​​значень за замовчуванням, але ви отримаєте абсолютно таку ж проблему.

Проблема полягає в тому, що fooє зміна змінної змінної, переданої від абонента, коли абонент цього не очікує. Код такого, як це було б добре, якби функція називалася чимось подібнимappend_5 ; тоді абонент буде викликати функцію, щоб змінити значення, яке вони передають, і поведінку було б очікувати. Але така функція навряд чи візьме аргумент за замовчуванням і, ймовірно, не поверне список (оскільки абонент вже має посилання на цей список; той, який він щойно передав).

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

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


7
Хоча це пов'язано, я думаю, що це виразна поведінка (оскільки ми очікуємо appendзмінити a"на місці"). Те, що мутація за замовчуванням не повторюється при кожному виклику, є "несподіваним" бітом ... принаймні для мене. :)
Енді Хейден

2
@AndyHayden, якщо очікується, що функція може змінити аргумент, чому було б сенс мати за замовчуванням?
Марк Рансом

@MarkRansom - єдиний приклад, про який я можу придумати cache={}. Однак я підозрюю, що таке "найменше здивування" виникає, коли ви не очікуєте (або хочете) функції, яку ви закликаєте мутувати аргумент.
Енді Хейден

1
@AndyHayden Я залишив власну відповідь тут, розширивши ці настрої. Дайте мені знати, що ви думаєте. Я можу додати ваш приклад для cache={}цього для повноти.
Марк Рансом

1
@AndyHayden Суть моєї відповіді полягає в тому, що якщо ви коли-небудь здивуєтесь випадковому вимкненню значення аргументу за замовчуванням, то у вас є ще одна помилка, тобто ваш код може випадково змінити значення абонента, коли типовим не було використано. І зауважте, що використання Noneта призначення справжнього за замовчуванням, якщо аргумент None не вирішує цю проблему (я вважаю, що це антитіла з цієї причини). Якщо ви виправите іншу помилку, уникаючи мутуючих значень аргументу, чи не мають вони за замовчуванням, ви ніколи не помітите і не піклуєтесь про цю "дивовижну" поведінку.
Бен

27

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

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

2
насправді це може бути трохи заплутано для новачків, оскільки a = a + [1]перевантаження a... подумайте про зміну b = a + [1] ; print id(b)та додавання рядка a.append(2). Це зробить більш очевидним, що +у двох списках завжди створюється новий список (призначений b), тоді як модифікований aвсе ще може мати той самий id(a).
Йорн Хес

25

Це оптимізація продуктивності. Внаслідок цієї функціональності, який із цих двох викликів функцій, на вашу думку, є швидшим?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Я дам вам підказку. Ось розбирання (див. Http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

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

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


24

Python: Аргумент, що змінюється за замовчуванням

Аргументи за замовчуванням оцінюються під час компіляції функції в об'єкт функції. При використанні функції, багаторазово виконуючи цю функцію, вони є і залишаються тим самим об'єктом.

Коли вони змінюються, коли їх мутують (наприклад, додаючи до неї елемент), вони залишаються мутованими під час послідовних дзвінків.

Вони залишаються мутованими, оскільки кожен раз є одним і тим же об'єктом.

Еквівалентний код:

Оскільки список пов'язаний з функцією, коли об'єкт функції компілюється та інстанціюється, це:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

майже рівнозначний цьому:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Демонстрація

Ось демонстрація - ви можете перевірити, що вони є тим самим об'єктом щоразу, коли на них посилаються

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

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

і працює з ним python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Чи це порушує принцип "Найменшого здивування"?

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

Звичайна інструкція для нових користувачів Python:

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

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Це використовує None singleton як дозорний об'єкт для того, щоб повідомити функцію, чи отримали ми аргумент, відмінний від за замовчуванням. Якщо у нас немає аргументів, ми фактично хочемо використовувати новий порожній список [], як за замовчуванням.

Як говорить підручник з потоку управління :

Якщо ви не хочете, щоб за замовчуванням ділилися між наступними дзвінками, ви можете замість цього записати функцію:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

24

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

def a(): return []

def b(x=a()):
    print x

Сподіваємось, досить показати, що не виконувати вирази аргументів за замовчуванням під час виконання defоператора не просто, або не має сенсу, або обох.

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


20

Просте вирішення методу None

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

19

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

  1. Поведінка атрибутів класу лише для читання при спробах призначення, і це
  2. Функції - це об'єкти (добре пояснено у прийнятій відповіді).

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

(1) описано в навчальному посібнику Python про класи . У спробі призначити значення атрибуту класу лише для читання:

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

Погляньте на початковий приклад і врахуйте вищезазначені моменти:

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

Ось fooоб’єкт і aє атрибутом foo(доступний на foo.func_defs[0]). Оскільки aце список, aє змінним і, таким чином, є атрибутом читання-запису foo. Він ініціалізується до порожнього списку, визначеного підписом, коли функція створена, і доступна для читання та запису до тих пір, поки існує об'єкт функції.

Для виклику fooбез зміни суми за замовчуванням використовується це значення за замовчуванням з foo.func_defs. У цьому випадку foo.func_defs[0]використовується в aмежах коду функціонального об'єкта. Зміни для aзміни foo.func_defs[0], що є частиною fooоб'єкта і зберігаються між виконанням коду в foo.

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

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Враховуючи (1) і (2) , можна побачити, чому це досягає бажаної поведінки:

  • Коли fooоб'єкт функції інстанціюється, foo.func_defs[0]встановлюється Noneнезмінний об'єкт.
  • Коли функція виконується за замовчуванням (без параметра, вказаного Lу виклику функції), foo.func_defs[0]( None) доступний у локальній області як L.
  • Після L = []цього призначення не може досягти успіху foo.func_defs[0], оскільки цей атрибут є лише для читання.
  • Згідно (1) , нова локальна змінна, також названа L, створюється в локальній області і використовується для залишку виклику функції. foo.func_defs[0]таким чином, залишається незмінним для майбутніх викликів foo.

19

Я продемонструю альтернативну структуру для передачі функції списку за замовчуванням функції (вона однаково добре працює зі словниками).

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

Неправильний метод (напевно ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Ви можете перевірити, що вони є одним і тим же об'єктом, скориставшись id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Перфорет Бретта Слаткіна "Ефективний пітон: 59 конкретних способів написання кращого пітона", пункт 20: Використання Noneта Документи для визначення динамічних аргументів за замовчуванням (стор. 48)

Умовою досягнення бажаного результату в Python є надання значення за замовчуванням Noneта документальне підтвердження фактичної поведінки в docstring.

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

Кращий спосіб :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

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


17

Тут є такі рішення:

  1. Використовуйте Noneяк значення за замовчуванням (або не має значення object) і вмикайте це, щоб створювати свої значення під час виконання; або
  2. Використовуйте параметр a lambdaза замовчуванням і зателефонуйте до нього в блоці спробу, щоб отримати значення за замовчуванням (це те, для чого потрібна абстракція лямбда).

Другий варіант хороший тим, що користувачі функції можуть передавати дзвінки, які вже можуть існувати (наприклад, a type)


16

Коли ми це робимо:

def foo(a=[]):
    ...

... ми відносимо аргумент aдо безіменному списку, якщо викликає абонент не бажає надсилати значення а.

Щоб зробити це простішим для цієї дискусії, давайте тимчасово дамо ім’я списку неназваних. Як щодо pavlo?

def foo(a=pavlo):
   ...

У будь-який час, якщо абонент не каже нам, що aтаке, ми повторно використовуємо pavlo.

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

Отже, це те, що ви бачите (Пам'ятайте, pavloініціалізовано до []):

 >>> foo()
 [5]

Тепер, pavloє [5].

foo()Знову дзвінок знову змінюється pavlo:

>>> foo()
[5, 5]

Вказівка aпід час дзвінка foo()гарантує pavlo, що не торкається.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Отже, pavloдосі [5, 5].

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

16

Я іноді використовую таку поведінку як альтернативу наступній схемі:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Якщо singletonвін використовується лише use_singletonмені, мені подобається такий шаблон як заміна:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Я використовував це для інстанціювання клієнтських класів, які мають доступ до зовнішніх ресурсів, а також для створення диктовок або списків для запам'ятовування.

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


2
Я вважаю за краще додати декоратор для запам'ятовування та помістити кеш пам’яті на сам об’єкт функції.
Стефано Борині

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

15

Ви можете подолати це, замінивши об'єкт (і, отже, краватку на область):

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

Некрасиво, але це працює.


3
Це хороше рішення у випадках, коли ви використовуєте програмне забезпечення для автоматичного створення документації для документування типів аргументів, очікуваних функцією. Якщо поставити a = None, а потім встановити значення [], якщо a є None, не допоможе читачеві зрозуміти, що очікується.
Майкл Скотт Катберт

Класна ідея: перезапис цього імені гарантує, що він ніколи не може бути змінений. Мені це дуже подобається.
holdenweb

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

13

Може бути правдою, що:

  1. Хтось використовує кожну функцію мови / бібліотеки та
  2. Переключення поведінки тут було б необдумано, але

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

  1. Це заплутана особливість, і в Python це прикро.

Інші відповіді, або принаймні деякі з них або складають пункти 1 і 2, але не 3, або роблять пункт 3 і зменшують точки 1 і 2. Але всі три вірні.

Може бути правдою, що перемикання коней у середині течії вимагатиме значного поломки, і що може виникнути більше проблем, змінивши Python, щоб інтуїтивно обробити відкриваючий фрагмент Стефано. І може бути правдою, що хтось, хто добре знав внутрішню програму Python, міг пояснити мінне поле наслідків. Однак,

Існуюча поведінка не є пітонічною, і Python є успішним, тому що дуже мало про мову порушує принцип найменшого здивування будь-де поблизуце погано. Це справжня проблема, було б розумно викорінити її чи ні. Це вада дизайну. Якщо ви розумієте мову набагато краще, намагаючись простежити поведінку, я можу сказати, що C ++ робить все це та інше; Ви багато дізнаєтесь, навігації, наприклад, тонких помилок вказівника. Але це не Pythonic: люди, які піклуються про Python достатньо, щоб наполегливо постати перед такою поведінкою, - це люди, яких тягне до мови, тому що Python має набагато менше сюрпризів, ніж інша мова. Даблєри та допитливі стають піфоністами, коли вони дивуються тому, як мало часу потрібно, щоб щось запрацювало - не через дизайнерський fl - я маю на увазі, приховану логічну головоломку - що перешкоджає інтуїції програмістів, які тягнуться до Python тому що це просто працює .


6
-1 Хоча перспектива, яку можна захистити, це не відповідь, і я не згоден з цим. Занадто багато особливих винятків породжують власні кутові справи.
Марцін

3
Отже, "дивовижно невідомо" говорити, що в Python було б більше сенсу, щоб аргумент за замовчуванням [] залишався [] щоразу, коли функція викликається?
Крістос Хейвард

3
І невідомо розглядати як нещасну ідіому, яка встановлює аргумент за замовчуванням на None, а потім в тілі тіла налаштування функції, якщо аргумент == None: argument = []? Чи невідомо вважати цю ідіому нещасною, оскільки люди часто хочуть того, чого очікував би наївний новачок, що якщо ви призначите f (argument = []), аргумент автоматично за замовчуванням буде значення []?
Крістос Хейвард

3
Але в Python частина мови мови полягає в тому, що вам не доведеться робити занадто багато глибоких занурень; array.sort () працює і працює незалежно від того, наскільки мало ви розумієте сортування, big-O та константи. Краса Python в механізмі сортування масиву, щоб навести один із незліченних прикладів, полягає в тому, що від вас не потрібно глибоко занурюватися у внутрішні місця. Інакше кажучи, краса Python полягає в тому, що зазвичай не потрібно глибоко занурюватися в реалізацію, щоб отримати щось, що просто працює. І існує рішення (... якщо аргумент == None: аргумент = []), FAIL.
Крістос Хейвард

3
Як окремий вислів, вислів x=[]означає "створити порожній список об'єкта та прив’язати до нього ім'я" x "." Так, у def f(x=[]), також створюється порожній список. Він не завжди прив'язується до x, тому замість цього він прив'язується до сурогату за замовчуванням. Пізніше, коли викликається f (), за замовчуванням виводиться і прив'язується до x. Оскільки саме порожній список вивернувся, той самий список є єдиним, що можна прив’язати до x, чи все в ньому застрягло чи ні. Як могло бути інакше?
Джері Б

10

Це не вада дизайну . Кожен, хто подорожує цим, робить щось не так.

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

  1. Ви маєте намір змінити аргумент як побічний ефект функції. У цьому випадку аргумент за замовчуванням ніколи не має сенсу . Єдиним винятком є ​​те, коли ви зловживаєте списком аргументів, щоб мати атрибути функції, наприклад cache={}, і від вас не очікується взагалі викликати функцію фактичним аргументом.
  2. Ви маєте намір залишити аргумент немодифікованим, але ви випадково його змінили. Ось помилка, виправте.
  3. Ви маєте намір змінити аргумент для використання всередині функції, але не очікували, що модифікація буде видно за межами функції. У такому випадку вам потрібно зробити копію аргументу, був він за замовчуванням чи ні! Python - це не дзвінка за значенням, тому вона не робить копію для вас, вам потрібно мати чітку інформацію про неї.

Приклад у запитанні може потрапити до категорії 1 чи 3. Не дивно, що він і змінює список, що передається, і повертає його; вам слід вибрати те чи інше.


"Робити щось не так" - це діагноз. З цього приводу я думаю, що були випадки = Жодна модель не корисна, але, як правило, ви не хочете змінювати, якщо в цьому випадку передавали мутант (2). cache={}Картина дійсно інтерв'ю-єдине рішення, в реальному коді ви , ймовірно , хочете @lru_cache!
Енді Хейден

9

Цей «жучок» дав мені багато понаднормових робочих годин! Але я починаю бачити потенційне використання цього (але мені б хотілося, щоб це було на час виконання)

Я дам вам те, що я бачу як корисний приклад.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

друкує наступне

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Просто змініть функцію:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Я думаю, що відповідь на це питання полягає в тому, як python передає дані в параметр (передавати за значенням або за посиланням), а не змінювати або як python обробляє оператор "def".

Короткий вступ. По-перше, у python є два типи даних, один - простий елементарний тип даних, як числа, а інший тип даних - об'єкти. По-друге, при передачі даних до параметрів python передає елементарний тип даних за значенням, тобто робить локальну копію значення в локальну змінну, але передає об'єкт за посиланням, тобто вказівники на об'єкт.

Приймаючи вищевказані два моменти, давайте пояснимо, що сталося з кодом python. Це лише через проходження посилання на об'єкти, але не має нічого спільного з змінним / незмінним, або, мабуть, тим, що оператор "def" виконується лише один раз, коли він визначений.

[] є об'єктом, тому python передає посилання [] на a, тобто aє лише вказівником на [], який лежить в пам'яті як об'єкт. Є лише одна копія [], однак на неї багато посилань. Для першого foo () список [] змінюється на 1 методом додавання. Але зауважте, що існує лише одна копія об'єкта списку, і цей об'єкт зараз стає 1 . Під час запуску другого foo (), те, що йдеться на веб-сторінці effbot (елементи більше не оцінюються), є неправильним. aоцінюється як об'єкт списку, хоча зараз вміст об'єкта дорівнює 1 . Це ефект проходження посилання! Результат foo (3) можна легко отримати таким же чином.

Для подальшого підтвердження моєї відповіді давайте розглянемо два додаткові коди.

====== № 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]є об'єктом, так і є None(перший є змінним, а другий - незмінним. Але змінність не має нічого спільного з питанням). Жоден не знаходиться десь у просторі, але ми знаємо, що він є, і там є лише одна копія None. Тож кожен раз, коли викликається foo, елементи оцінюються (на відміну від деяких відповідей, що він оцінюється лише один раз), щоб бути None, щоб було зрозуміло, посилання (або адреса) None. Потім у foo пункт змінюється на [], тобто вказує на інший об'єкт, який має іншу адресу.

====== № 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

Викликання foo (1) змушує елементи вказувати на об'єкт списку [] з адресою, скажімо, 11111111. Вміст списку змінюється на 1 у функції foo у подальшому, але адреса не змінюється, все ще 11111111 Тоді йде foo (2, []). Хоча [] у foo (2, []) має той самий вміст, що і параметр за замовчуванням [] при виклику foo (1), їх адреса відрізняється! Оскільки ми надаємо параметр явно, itemsповинен приймати адресу цього нового[] , скажімо, 2222222, і повернути його після внесення деяких змін. Тепер foo (3) виконується. оскільки тількиxнадано, елементи повинні знову приймати значення за замовчуванням. Яке значення за замовчуванням? Він встановлюється під час визначення функції foo: об'єкт списку, розташований у 11111111. Отже, елементи оцінюються як адреса 11111111, що має елемент 1. Список, розташований у 2222222, також містить один елемент 2, але його не вказують жодні елементи більше. Отже, додаток 3 складе items[1,3].

З наведених пояснень ми бачимо, що веб-сторінка effbot, рекомендована у прийнятій відповіді, не дала відповідної відповіді на це питання. Більше того, я вважаю, що точка веб-сторінки effbot неправильна. Я думаю, що код щодо UI.Button є правильним:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

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

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Якщо ми виконаємо, x[7]()ми отримаємо 7, як очікувалося, і x[9]()дамо 9, інше значення i.


5
Ваш останній пункт неправильний. Спробуйте, і ви побачите, що x[7]()це 9.
Дункан

2
"елемента елемента передачі python за значенням, тобто зробити локальну копію значення в локальну змінну" є абсолютно невірним. Я здивований, що хтось, очевидно, може дуже добре знати Python, але все ж має таке жахливе нерозуміння основ. :-(
Veky

6

TLDR: Значення за замовчуванням за певним часом є послідовними та суворо виразнішими.


Визначення функції впливає на два діапазони: визначальну область, що містить функцію, та область виконання, яку містить функція. Хоча цілком зрозуміло, як блокується карта на область застосування, питання полягає в тому, куди def <name>(<args=defaults>):належить:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

def nameЧастина повинна оцінити в визначальною сфері - ми хочемо nameбути доступні там, в кінці кінців. Оцінка функції лише всередині себе зробить її недоступною.

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

Тепер, коли оцінювати default?

Послідовність вже говорить "при визначенні": все інше def <name>(<args=defaults>):найкраще оцінюється також при визначенні. Затримка його частини була б дивним вибором.

Два варіанти також не є рівнозначними: якщо defaultоцінюється під час визначення, він все одно може впливати на час виконання. Якщо defaultоцінюється під час виконання, це не може впливати на час визначення. Вибір "за визначенням" дозволяє виражати обидва випадки, а вибір "при виконанні" може виражати лише один:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

"Послідовність уже говорить" при визначенні ": все інше def <name>(<args=defaults>):найкраще оцінюється і при визначенні". Я не думаю, що висновок випливає з передумови. Тільки тому, що дві речі знаходяться в одній лінії, не означає, що їх слід оцінювати в одній області. default- різна річ, ніж решта рядка: це вираз. Оцінка виразу - це зовсім інший процес, ніж визначення функції.
LarsH

Визначення функцій @LarsH які будуть оцінені в Python. Незалежно від того, що це з заяви ( def) чи виразу ( lambda), це не змінює те, що створення функції означає оцінку, особливо її підпис. А значення за замовчуванням - частина підпису функції. Це не означає, що за замовчуванням потрібно негайно оцінювати, наприклад, підказки типу не можуть. Але це, безумовно, говорить, що вони повинні, якщо немає вагомих причин цього не робити.
MisterMiyagi

Добре, створення функції означає оцінку в певному сенсі, але, очевидно, не в тому сенсі, що кожне вираження всередині неї оцінюється в момент визначення. Більшість - ні. Мені незрозуміло, у якому сенсі підпис особливо "оцінюється" в момент визначення більше, ніж тіло функції "оцінюється" (розбирається на відповідне подання); тоді як вирази в тілі функції явно не оцінюються в повному розумінні. З цієї точки зору, послідовність говорить про те, що вирази в підписі також не повинні оцінюватися "повною мірою".
LarsH

Я не маю на увазі, що ви помиляєтесь, лише те, що ваш висновок не випливає з послідовності.
LarsH

@LarsH За замовчуванням не є частиною тіла, і я не стверджую, що послідовність - єдиний критерій. Чи можете ви зробити пропозицію, як уточнити відповідь?
MisterMiyagi

3

Кожна інша відповідь пояснює, чому це насправді приємна та бажана поведінка, або чому ви цього не потребуєте. Моє для тих впертих, хто хоче скористатися своїм правом нахиляти мову до своєї волі, а не навпаки.

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

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Тепер давайте переглянемо нашу функцію за допомогою цього декоратора:

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

foo() # '[5]'
foo() # '[5]' -- as desired

Це особливо акуратно для функцій, які беруть кілька аргументів. Порівняйте:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

з

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Важливо зазначити, що вищезазначене рішення порушується, якщо ви намагаєтесь використовувати аргументи ключових слів, наприклад:

foo(a=[4])

Декоратор може бути налаштований так, щоб це допустити, але ми залишаємо це як вправу для читача;)

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