Mutable Default Arguments in Python
Objects of built-in types like (int, float, bool, str, tuple, Unicode) are immutable. Objects of built-in types like (list, set, dict) are mutable.
A mutable object can change its state or contents and immutable objects cannot. This is a very simple definition of a Mutable and Immutable object in Python. Now coming to the interesting part which is Mutable Default Object in function definitions. I have written a very simple function as below —
def foobar(element, data=[]): data.append(element) return data
In the above function, I set data to a list(Mutable object) as a default argument. Now let’s execute this function —
>>> print(foobar(22)) [12, 22] >>> print(foobar(32)) [12, 22, 32]
What is going on here? As you can see, the first time, the function returns exactly what we expected. On the second and all subsequent passes the same list is used in each call.
Why is this happening
After function executes this definition, You can check the default attribute for this function. by using __defaults__ .
About __defaults__
A tuple containing default argument values for those arguments that have defaults, or None if no arguments have a default value.
The result an empty list as the only entry in __defaults__
Let’s now execute this function and check the defaults.
Astonished? The value inside the object changes, Consecutive calls to the function will now simply append to that embedded list object:
Let’s execute the function multiple times:
>>> foo() >>> foo() >>> foo() >>> foo.__defaults__ ([10, 10, 10, 10],)
The reason why this is happening because default argument values are stored in the function object, not in the code object (because they represent values calculated at run-time).
The Solution
Now, the question is how to code foobar in order to get the behavior that we expected.
Fortunately, the solution is straightforward. The mutable objects used as defaults are replaced by None, and then the arguments are tested for None.
def foobar(element, data=None): if data is None: data = [] data.append(element) return data >>> foobar(12) [12]
So, by replacing mutable default arguments with None solved our problem.
Good Use of Mutable Default Argument
However, Mutable Default Argument have some good use-case also as following-
1.Binding local variable to the current value of the outer variable in a callback
2.Cache/Memoization
I hope you like the explanation of Mutable Default Arguments in Python. Still, if any doubt or improvement regarding it, ask in the comment section.
Top comments (8)
Such software as dreams are made on. I mostly rant about performance, unnecessary complexity, privacy and data collection.
Great question! I had to refresh my knowledge a bit about the why
Nice, I see this as a great time to ask though, is there an Object.freeze() equivalent in Python that will automatically let us make mutable objects immutable?
Not really, there are immutable objects like numbers, strings, tuples and frozen sets but a generic object can’t be automatically frozen.
You can play with some builtin methods like setattr (which is called when a new attribute is added to an object) and its specular getattribute to create a custom immutable object
And if not, why wouldn’t something like that be added?
Probably because the language has immutable data types that are used daily: numbers, strings and tuples.
>>> a = "abc" >>> a[0] = "x" Traceback (most recent call last): File "", line 1, in module> TypeError: 'str' object does not support item assignment
2.7.0 :001 > a = "abc" 2.7.0 :002 > a[0] = "x" 2.7.0 :003 > a => "xbc"
if you look at many Ruby code bases, you’ll see .freeze used beside many strings that are defined as constant because by default they are mutable. In Python they aren’t. I personally prefer the latter design choice.
In 2005 there was a PEP (the name of Python community proposals) to add a freeze() method, called freeze protocol. It was rejected and among the reasons there are the following:
- the concept of frozen objects in Python is related to them being able to be keys in a dictionary (all immutable objects can be), but why would a dev need an entire object to be a key in a dictionary?
- sending objects around both in their mutable version and their immutable one can lead to errors. Ruby has a .frozen? method to help you distinguish them, but still, that would mean that each receiver of that object would need to check if the object is frozen or not. It’s easier to assume that all objects aside from those immutable builtins are mutable and live with that.
TLDR; language design choices. 15 years have passed since that proposal and Python doesn’t seem to be in need of a freeze() method after all
[Python] Mutable default arguments
Пол дня выяснял причину, почему у меня массив растёт.
Такие дела значит.
>>> def function( data =[]):
. data.append(1)
. return data
.
>>> function()
[1]
>>> function()
[1, 1]
>>> function()
[1, 1, 1]
Добро пожаловать в мир оптимизаций питона))
vater
> Пол дня выяснял причину, почему у меня массив растёт.
Не понял, что здесь не так ?
innuendo
Изменеяется дефолтный параметр, который по идее должен был быть всегда пустым массивом
Разве не понятно было сразу, что по ссылке(указателю) передаётся ?
innuendo
> Разве не понятно было сразу, что по ссылке(указателю) передаётся ?
Я бы подумал, что это какое-то специальное значение, которое каждый раз генерируется, а не глобальная штука.
PANDA
> а не глобальная штука.
innuendo
> data виден глобально ?
Нет, это же аргумент у функции.
innuendo
Окей, не глобальная, но статическая.
Вообще, забавно.
Как раз сейчас учу python, прохожу онлайн курсы. Первый был совсем по основам, второй более продвинутый. В обоих рассказывалось о аргументах функций, в том числе про передачу наборов аргументов в виде кортежа и словаря и про значения по умолчанию. Но про такую подставу никто не рассказал.
Вообще, тот же PyCharm подчеркивает мьютабл дефолтные значения.
Вообще при изучении любого нового языка можно взять за правило гуглить такой запрос. Особенно полезно для C++ и JavaScript 🙂
D.Lans
> Про простому запросу «python gotchas»
По простому запросу «python Default Argument Values» получаем 4.7.1. Default Argument Values официального мануала, НЕ читаем ни в коем случае, не делаем никаких выводов, продолжаем тратить по полдня на три строчки, ходить на какие-то помойки с готчами, удивляться.
innuendo
> Разве не понятно было сразу, что по ссылке(указателю) передаётся ?
А в питоне можно передать список в функцию как-то по другому?
beejah
> python Default Argument Values
почему я должен это гуглить первым же делом?
Изменяемый аргумент по умолчанию
Изменяемый аргумент по умолчанию (англ. mutable default argument) — распространенная, но легко решаемая проблема при работе с функциями и классами на Python.
В чем проблема?
Представьте себе, что мы написали функцию, которая добавляет к списку значение no_zeros , если список не содержит числа 0 . Функция отлично работала, но затем мы решили, добавить пустой список по умолчанию:
>>> def no_zeros(lst=[]): if 0 not in lst: lst.append('no zeros') return lst print(no_zeros([])) print(no_zeros()) print(no_zeros([0, 1, 2])) print(no_zeros()) print(no_zeros())
>>> print(no_zeros([])) . print(no_zeros()) . print(no_zeros([0, 1, 2])) . print(no_zeros()) . print(no_zeros()) ['no zeros'] ['no zeros'] [0, 1, 2] ['no zeros', 'no zeros'] ['no zeros', 'no zeros', 'no zeros']
Пока мы передаем в функцию список, все работает отлично, но каждый раз, когда мы используем аргумент по умолчанию, в него добавляется ‘no zeros’ . А это значит, что мы имеем дело с изменяемым аргументом по умолчанию.
Что случилось?
Когда интерпретатор доходит до нашей функции он создает значение по умолчанию и использует его при вызове функции без соответствующего аргумента. Если это значение изменяется, то в дальнейшем оно используется уже в измененном виде. Неизменяемые типы (кортежи, строки и т.д.) защищены от таких действий, но списки, словари и другие изменяемые типы и классы могут создать проблемы.
Что делать?
Эта проблема решается множеством способов, но стандартным решением является следующее. Значением по умолчанию назначается None , а в теле функции пишется простая проверка. Если соответствующим аргументом выступает None, то вместо него создается наше изменяемое значение.
def no_zeros(lst=None): if lst is None: lst = [] if 0 not in lst: lst.append(None) return lst
>>> print(no_zeros([])) . print(no_zeros()) . print(no_zeros([0, 1, 2])) . print(no_zeros()) . print(no_zeros()) [None] [None] [0, 1, 2] [None] [None]
В этом случае каждый раз создается новый список. Такое решение является распространенным паттерном, и часто используется в программах на Python, чтобы избежать изменяемых аргументов по умолчанию.
Заключение
Подводя итог, можно с уверенностью сказать, что не стоит использовать изменяемые аргументы по умолчанию без очень, очень веской причины. Вместо этого используйте значение None по умолчанию, и присваивайте изменяемое значение в теле функции.
Практический Python для начинающих
Станьте junior Python программистом за 7 месяцев