[python] «Наименьшее удивление» и параметр Mutable Default Argument



14 Answers

Предположим, у вас есть следующий код

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

Question

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

Фактическое поведение более последовательное: все из этой строки оценивается, когда эта строка выполняется, что означает определение функции.




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

  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 изменения - операция выполняется на одном объекте - и печатается



Архитектура

Assigning default values in a function call is a code smell.

def a(b=[]):
    pass

This is a signature of a function that is up to no good. Not just because of the problems described by other answers. I won't go in to that here.

This function aims to do two things. Create a new list, and execute a functionality, most likely on said list.

Functions that do two things are bad functions, as we learn from clean code practices.

Attacking this problem with polymorphism, we would extend the python list or wrap one in a class, then perform our function upon it.

But wait you say, I like my one-liners.

Well, guess what. Code is more than just a way to control the behavior of hardware. It's a way of:

  • communicating with other developers, working on the same code.

  • being able to change the behavior of the hardware when new requirements arises.

  • being able to understand the flow of the program after you pick up the code again after two years to make the change mentioned above.

Don't leave time-bombs for yourself to pick up later.

Separating this function into the two things it does, we need a class

class ListNeedsFives(object):
    def __init__(self, b=None):
        if b is None:
            b = []
        self.b = b

    def foo():
        self.b.append(5)

Executed by

a = ListNeedsFives()
a.foo()
a.b

And why is this better than mashing all the above code into a single function.

def dontdothis(b=None):
    if b is None:
        b = []
    b.append(5)
    return b

Why not do this?

Unless you fail in your project, your code will live on. Most likely your function will be doing more than this. The proper way of making maintainable code is to separate code into atomic parts with a properly limited scope.

The constructor of a class is a very commonly recognized component to anyone who has done Object Oriented Programming. Placing the logic that handles the list instantiation in the constructor makes the cognitive load of understanding what the code does smaller.

The method foo() does not return the list, why not?

In returning a stand alone list, you could assume that it's safe to do what ever you feel like to it. But it may not be, since it is also shared by the object a . Forcing the user to refer to it as ab reminds them where the list belongs. Any new code that wants to modify ab will naturally be placed in the class, where it belongs.

the def dontdothis(b=None): signature function has none of these advantages.




I sometimes exploit this behavior as an alternative to the following pattern:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

If singleton is only used by use_singleton , I like the following pattern as a replacement:

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

I've used this for instantiating client classes that access external resources, and also for creating dicts or lists for memoization.

Since I don't think this pattern is well known, I do put a short comment in to guard against future misunderstandings.




Python: The Mutable Default Argument

Default arguments get evaluated at the time the function is compiled into a function object. When used by the function, multiple times by that function, they are and remain the same object.

When they are mutable, when mutated (for example, by adding an element to it) they remain mutated on consecutive calls.

They stay mutated because they are the same object each time.

Equivalent code:

Since the list is bound to the function when the function object is compiled and instantiated, this:

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

is almost exactly equivalent to this:

_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

демонстрация

Here's a demonstration - you can verify that they are the same object each time they are referenced by

  • seeing that the list is created before the function has finished compiling to a function object,
  • observing that the id is the same each time the list is referenced,
  • observing that the list stays changed when the function that uses it is called a second time,
  • observing the order in which the output is printed from the source (which I conveniently numbered for you):

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()

and running it with 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']

Does this violate the principle of "Least Astonishment"?

This order of execution is frequently confusing to new users of Python. If you understand the Python execution model, then it becomes quite expected.

The usual instruction to new Python users:

But this is why the usual instruction to new users is to create their default arguments like this instead:

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

This uses the None singleton as a sentinel object to tell the function whether or not we've gotten an argument other than the default. If we get no argument, then we actually want to use a new empty list, [] , as the default.

As the tutorial section on control flow says:

If you don't want the default to be shared between subsequent calls, you can write the function like this instead:

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



It may be true that:

  1. Someone is using every language/library feature, and
  2. Switching the behavior here would be ill-advised, but

it is entirely consistent to hold to both of the features above and still make another point:

  1. It is a confusing feature and it is unfortunate in Python.

The other answers, or at least some of them either make points 1 and 2 but not 3, or make point 3 and downplay points 1 and 2. But all three are true.

It may be true that switching horses in midstream here would be asking for significant breakage, and that there could be more problems created by changing Python to intuitively handle Stefano's opening snippet. And it may be true that someone who knew Python internals well could explain a minefield of consequences. Однако,

The existing behavior is not Pythonic, and Python is successful because very little about the language violates the principle of least astonishment anywhere near this badly. It is a real problem, whether or not it would be wise to uproot it. It is a design flaw. If you understand the language much better by trying to trace out the behavior, I can say that C++ does all of this and more; you learn a lot by navigating, for instance, subtle pointer errors. But this is not Pythonic: people who care about Python enough to persevere in the face of this behavior are people who are drawn to the language because Python has far fewer surprises than other language. Dabblers and the curious become Pythonistas when they are astonished at how little time it takes to get something working--not because of a design fl--I mean, hidden logic puzzle--that cuts against the intuitions of programmers who are drawn to Python because it Just Works .




This behavior is not surprising if you take the following into consideration:

  1. The behavior of read-only class attributes upon assignment attempts, and that
  2. Functions are objects (explained well in the accepted answer).

The role of (2) has been covered extensively in this thread. (1) is likely the astonishment causing factor, as this behavior is not "intuitive" when coming from other languages.

(1) is described in the Python tutorial on classes . In an attempt to assign a value to a read-only class attribute:

...all variables found outside of the innermost scope are read-only ( an attempt to write to such a variable will simply create a new local variable in the innermost scope, leaving the identically named outer variable unchanged ).

Look back to the original example and consider the above points:

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

Here foo is an object and a is an attribute of foo (available at foo.func_defs[0] ). Since a is a list, a is mutable and is thus a read-write attribute of foo . It is initialized to the empty list as specified by the signature when the function is instantiated, and is available for reading and writing as long as the function object exists.

Calling foo without overriding a default uses that default's value from foo.func_defs . In this case, foo.func_defs[0] is used for a within function object's code scope. Changes to a change foo.func_defs[0] , which is part of the foo object and persists between execution of the code in foo .

Now, compare this to the example from the documentation on emulating the default argument behavior of other languages , such that the function signature defaults are used every time the function is executed:

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

Taking (1) and (2) into account, one can see why this accomplishes the the desired behavior:

  • When the foo function object is instantiated, foo.func_defs[0] is set to None , an immutable object.
  • When the function is executed with defaults (with no parameter specified for L in the function call), foo.func_defs[0] ( None ) is available in the local scope as L .
  • Upon L = [] , the assignment cannot succeed at foo.func_defs[0] , because that attribute is read-only.
  • Per (1) , a new local variable also named L is created in the local scope and used for the remainder of the function call. foo.func_defs[0] thus remains unchanged for future invocations of foo .



Что вы спрашиваете, почему это:

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), который мы будем игнорировать.

Другими словами, вместо оценки параметров по умолчанию, почему бы не сохранить каждый из них и не оценить их при вызове функции?

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




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



You can get round this by replacing the object (and therefore the tie with the scope):

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

Ugly, but it works.




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

Если объекты python изменяемы, я думаю, что это следует учитывать при разработке аргументов аргументов по умолчанию. Когда вы создаете экземпляр списка:

a = []

вы ожидаете получить новый список, на который ссылается a .

Почему a = [] в

def x(a=[]):

создать новый список по определению функции, а не по вызову? Это похоже на то, что вы спрашиваете: «Если пользователь не предоставляет аргумент, то создайте новый список и используйте его, как если бы он был вызван вызывающим». Я думаю, что это двусмысленно:

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

пользователь, хотите ли вы по умолчанию использовать дату-время, соответствующее тому, когда вы определяете или выполняете x ? В этом случае, как и в предыдущем, я буду придерживаться такого же поведения, как если бы аргумент по умолчанию «назначение» был первой инструкцией функции (datetime.now (), вызванной вызовом функции). С другой стороны, если пользователь хотел отобразить время-отображение, он мог бы написать:

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

Я знаю, я знаю: это закрытие. В качестве альтернативы Python может предоставить ключевое слово для привязки определения времени:

def x(static a=b):



This actually has nothing to do with default values, other than that it often comes up as an unexpected behaviour when you write functions with mutable default values.

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

No default values in sight in this code, but you get exactly the same problem.

The problem is that foo is modifying a mutable variable passed in from the caller, when the caller doesn't expect this. Code like this would be fine if the function was called something like append_5 ; then the caller would be calling the function in order to modify the value they pass in, and the behaviour would be expected. But such a function would be very unlikely to take a default argument, and probably wouldn't return the list (since the caller already has a reference to that list; the one it just passed in).

Your original foo , with a default argument, shouldn't be modifying a whether it was explicitly passed in or got the default value. Your code should leave mutable arguments alone unless it is clear from the context/name/documentation that the arguments are supposed to be modified. Using mutable values passed in as arguments as local temporaries is an extremely bad idea, whether we're in Python or not and whether there are default arguments involved or not.

If you need to destructively manipulate a local temporary in the course of computing something, and you need to start your manipulation from an argument value, you need to make a copy.




Я думал, что создание объектов во время выполнения будет лучшим подходом. Теперь я менее уверен, так как вы теряете некоторые полезные функции, хотя это может стоить того, что было бы просто для предотвращения путаницы новичков. Недостатки этого:

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

Однако мы потеряли способность интроспекции и посмотрим, что представляют собой аргументы по умолчанию. Поскольку объекты не были построены, мы никогда не сможем их захватить, не называя функцию. Самое лучшее, что мы могли бы сделать, это сохранить исходный код и вернуть его в виде строки.




I think the answer to this question lies in how python pass data to parameter (pass by value or by reference), not mutability or how python handle the "def" statement.

A brief introduction. First, there are two type of data types in python, one is simple elementary data type, like numbers, and another data type is objects. Second, when passing data to parameters, python pass elementary data type by value, ie, make a local copy of the value to a local variable, but pass object by reference, ie, pointers to the object.

Admitting the above two points, let's explain what happened to the python code. It's only because of passing by reference for objects, but has nothing to do with mutable/immutable, or arguably the fact that "def" statement is executed only once when it is defined.

[] is an object, so python pass the reference of [] to a , ie, a is only a pointer to [] which lies in memory as an object. There is only one copy of [] with, however, many references to it. For the first foo(), the list [] is changed to 1 by append method. But Note that there is only one copy of the list object and this object now becomes 1 . When running the second foo(), what effbot webpage says (items is not evaluated any more) is wrong. a is evaluated to be the list object, although now the content of the object is 1 . This is the effect of passing by reference! The result of foo(3) can be easily derived in the same way.

To further validate my answer, let's take a look at two additional codes.

====== No. 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]

[] is an object, so is None (the former is mutable while the latter is immutable. But the mutability has nothing to do with the question). None is somewhere in the space but we know it's there and there is only one copy of None there. So every time foo is invoked, items is evaluated (as opposed to some answer that it is only evaluated once) to be None, to be clear, the reference (or the address) of None. Then in the foo, item is changed to [], ie, points to another object which has a different address.

====== No. 3 =======

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

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

The invocation of foo(1) make items point to a list object [] with an address, say, 11111111. the content of the list is changed to 1 in the foo function in the sequel, but the address is not changed, still 11111111. Then foo(2,[]) is coming. Although the [] in foo(2,[]) has the same content as the default parameter [] when calling foo(1), their address are different! Since we provide the parameter explicitly, items has to take the address of this new [] , say 2222222, and return it after making some change. Now foo(3) is executed. since only x is provided, items has to take its default value again. What's the default value? It is set when defining the foo function: the list object located in 11111111. So the items is evaluated to be the address 11111111 having an element 1. The list located at 2222222 also contains one element 2, but it is not pointed by items any more. Consequently, An append of 3 will make items [1,3].

From the above explanations, we can see that the effbot webpage recommended in the accepted answer failed to give a relevant answer to this question. What is more, I think a point in the effbot webpage is wrong. I think the code regarding the UI.Button is correct:

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

Each button can hold a distinct callback function which will display different value of i . I can provide an example to show this:

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

If we execute x[7]() we'll get 7 as expected, and x[9]() will gives 9, another value of i .




Already busy topic, but from what I read here, the following helped me realizing how it's working internally:

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





Related