"Least Astonishment" and the Mutable Default Argument

Least Astonishment and the Mutable Default Argument

Actually, this is not a design flaw, and it is not because of internals or performance. It comes simply from the fact that functions in Python are first-class objects, and not only a piece of code.

As soon as you think of it this way, then it completely makes sense: a function is an object being evaluated on its definition; default parameters are kind of "member data" and therefore their state may change from one call to the other - exactly as in any other object.

In any case, the effbot (Fredrik Lundh) has a very nice explanation of the reasons for this behavior in Default Parameter Values in Python.
I found it very clear, and I really suggest reading it for a better knowledge of how function objects work.

Question is different with '“Least Astonishment” and the Mutable Default Argument'

default arguments come in picture when you don't pass any arguments to the function,

here you are passing arguments to the function namely [2], [3] and [4] and hence default value is not being used

in your question you mention that for first print output should be [2, [...]] if you are able to understand that , then next ones are exactly same as default argument never comes in the picture.. just focus on how [2, [...]] came

Least Astonishment and the Mutable Default Argument

Actually, this is not a design flaw, and it is not because of internals or performance. It comes simply from the fact that functions in Python are first-class objects, and not only a piece of code.

As soon as you think of it this way, then it completely makes sense: a function is an object being evaluated on its definition; default parameters are kind of "member data" and therefore their state may change from one call to the other - exactly as in any other object.

In any case, the effbot (Fredrik Lundh) has a very nice explanation of the reasons for this behavior in Default Parameter Values in Python.
I found it very clear, and I really suggest reading it for a better knowledge of how function objects work.

Avoiding Default Argument Value is Mutable Warning (PyCharm)

If your only requirement is to bundle several elements together into one object, and that object doesn't need to be mutable, use a tuple instead of a list. Tuples provide all the same behavior of lists except for modification of their elements. To be exact, mutable elements can still be mutated, but elements of the tuple cannot be assigned, added, or removed, and the tuple itself is considered an immutable data structure. The syntax for constructing a tuple is the same as a list, just with parentheses instead of square brackets:

# Immutable container with immutable elements, perfectly fine to use as a default argument
tuple_o_fruits = ("Apple", "Banana", "Orange")

Python's lack of generic enforceable const-correctness is one of the few things that really irks me about the language. The standard solution is to use builtin immutable datatypes like tuple in place of mutable ones like list. The immutable form of set is frozenset, for example. Many common builtin types such as str, int, and float are only available in immutable form. bytes is an interesting case because the immutable form is better known than its mutable alternative, bytearray. The most prominent builtin mutable type that lacks an immutable alternative is dict (but it's not hard to make your own).

So, the real answer to "how do I avoid warnings about mutable default arguments" is, 9 times out of 10, to not use a mutable object, because immutable alternative are generally readily available. This should be sufficient for your case.

Concatenation of the result of a function with a mutable default argument

That's actually pretty interesting!

As we know, the list l in the function definition is initialized only once at the definition of this function, and for all invocations of this function, there will be exactly one copy of this list. Now, the function modifies this list, which means that multiple calls to this function will modify the exact same object multiple times. This is the first important part.

Now, consider the expression that adds these lists:

f()+f()+f()

According to the laws of operator precedence, this is equivalent to the following:

(f() + f()) + f()

...which is exactly the same as this:

temp1 = f() + f() # (1)
temp2 = temp1 + f() # (2)

This is the second important part.

Addition of lists produces a new object, without modifying any of its arguments. This is the third important part.

Now let's combine what we know together.

In line 1 above, the first call returns [0], as you'd expect. The second call returns [0, 1], as you'd expect. Oh, wait! The function will return the exact same object (not its copy!) over and over again, after modifying it! This means that the object that the first call returned has now changed to become [0, 1] as well! And that's why temp1 == [0, 1] + [0, 1].

The result of addition, however, is a completely new object, so [0, 1, 0, 1] + f() is the same as [0, 1, 0, 1] + [0, 1, 2]. Note that the second list is, again, exactly what you'd expect your function to return. The same thing happens when you add f() + ["-"]: this creates a new list object, so that any other calls to f won't interfere with it.

You can reproduce this by concatenating the results of two function calls:

>>> f() + f()
[0, 1, 0, 1]
>>> f() + f()
[0, 1, 2, 3, 0, 1, 2, 3]
>>> f() + f()
[0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]

Again, you can do all that because you're concatenating references to the same object.

Why the mutable default argument fix syntax is so ugly, asks python newbie

Default arguments are evaluated at the time the def statement is executed, which is the probably the most reasonable approach: it is often what is wanted. If it wasn't the case, it could cause confusing results when the environment changes a little.

Differentiating with a magic local method or something like that is far from ideal. Python tries to make things pretty plain and there is no obvious, clear replacement for the current boilerplate that doesn't resort to messing with the rather consistent semantics Python currently has.

How to type mutable default arguments

None is not the only sentinel available. You can choose your own list value to use as a sentinel, replacing it (rather than None) with a new empty list at run time.

_sentinel = []

def foo(bar: List[int]=_sentinel):
bar = [] if bar is _sentinel else bar
return sorted(bar)

As long as no one calls foo using _sentinel as an explicit argument, bar will always get a fresh empty list. In a call like foo([]), bar is _sentinel will be false: the two empty lists are not the same object, as the mutability of lists means that you cannot have a single empty list that always gets referenced by [].



Related Topics



Leave a reply



Submit