ПИТОН ОБЪЕКТЫ: ИЗМЕНЧИВЫЙ VS. НЕИЗМЕННЫЙ

Note: this is a Russian language translation of the following English post: Python Objects: Mutable vs. Immutable. Thank you to my friends Andrei Rybin and Dimitri Kozlenko for their help in translating!

Не все объекты в Питон обрабатывают изменения одинаково. Некоторые объекты являются изменяемыми, то есть они могут быть изменены. Другие неизменны; они не могут быть изменены, а возвращают новые объекты при попытке обновить. Что это значит при написании кода в Питон?

Этот пост будет об (а) мутабильность общих типов данных и (б) случаях, когда вопрос переменчивости важен.

Изменчивость по типовым разновидностям

Ниже приведены некоторые неизменяемые объекты:

  • int
  • float
  • decimal
  • complex
  • bool
  • string
  • tuple
  • range
  • frozenset
  • bytes

Ниже приведены некоторые изменяемые объекты:

  • list
  • dict
  • set
  • bytearray
  • определяемые пользователем классы (если это специально не сделано неизменяемым)

То, что помогает мне помнить, какие типы изменчивы а какие нет, это что контейнеры и определяемые пользователем типы, как правило, изменяемые, в то время, как скалярные типы почти всегда неизменны. Потом я вспоминаю некоторые заметные исключения: tuple является неизменяемым контейнером, frozenset неизменяемая версия set. Строки неизменны; что если вы хотите, чтобы можно было изменять chars в определенном индексе? Используйте bytearray.

КОГДА мутабильность ВАЖНА

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

string_build = ""
for data in container:
    string_build += str(data)

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

Ниже приводится более эффективный код в стиле Питона::

builder_list = []
for data in container:
    builder_list.append(str(data))
"".join(builder_list)

### Another way is to use a list comprehension
"".join([str(data) for data in container])

### or use the map function
"".join(map(str, container))

Этот код использует преимущества изменяемости одного объекта LIST, чтобы собрать свои данные вместе, а затем выделить результат в одну строку.. Это сокращает общее число выделяемых объектов почти вдвое.

Еще один подводный камень, связанные с изменчивостью является следующий сценарий:

def my_function(param=[]):
    param.append("thing")
    return param

my_function() # ["thing"]
my_function() # ["thing", "thing"]

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

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

def my_function2(param=None):
    if param is None:
        param = []
    param.append("thing")
    return param

ВЫВОД

Изменчивость важна. Знайте ее Выучите ее. Примитивные типы, скорее всего, неизменны. Контейнерные типы, скорее всего, изменчивые.

References

ПИТОН ОБЪЕКТЫ: ИЗМЕНЧИВЫЙ VS. НЕИЗМЕННЫЙ

Python Objects: Mutable vs. Immutable

For other languages see here:

Not all python objects handle changes the same way. Some objects are mutable, meaning they can be altered.  Others are immutable; they cannot be changed but rather return new objects when attempting to update. What does this mean when writing python code?

This post will talk about (a) the mutability of common data types and (b) instances where mutability matters.

Mutability of Common Types

The following are some immutable objects:

  • int
  • float
  • decimal
  • complex
  • bool
  • string
  • tuple
  • range
  • frozenset
  • bytes

The following are some mutable objects:

  • list
  • dict
  • set
  • bytearray
  • user-defined classes (unless specifically made immutable)

The way I like to remember which types are mutable and which are not is that containers and user-defined types tend to be mutable while scalar types are almost always immutable. Then remember some notable exceptions: tuple is an immutable container, frozenset is an immutable version of set. Strings are immutable; what if you want to do some in-place modifications like character swapping? Use a bytearray.

When Mutability Matters

Mutability might seem like an innocuous topic, but when writing an efficient program it is essential to understand. For instance, the following code is a straightforward solution to concatenate a string together:

string_build = ""
for data in container:
    string_build += str(data)

In reality, this is very inefficient. Because strings are immutable, concatenating two strings together actually creates a third string which is the combination of the previous two. If you are iterating a lot and building a large string, you will waste a lot of memory creating and throwing away objects. Also, at the end of the iteration you will be allocating and throwing away very large string objects which is even more costly.

The following is a more efficient and pythonic way:

builder_list = []
for data in container:
    builder_list.append(str(data))
"".join(builder_list)

### Another way is to use a list comprehension
"".join([str(data) for data in container])

### or use the map function
"".join(map(str, container))

This code takes advantage of the mutability of a single list object to gather your data together and then allocate a single result string to put your data in. That cuts down on the total number of objects allocated by almost half.

Another pitfall related to mutability is the following scenario:

def my_function(param=[]):
    param.append("thing")
    return param

my_function() # returns ["thing"]
my_function() # returns ["thing", "thing"]

What you might think would happen is that by giving an empty list as a default value to param, a new empty list is allocated each time the function is called and no list is passed in. But what actually happens is that every call that uses the default list will be using the same list.  This is because Python (a) only evaluates functions definitions once, (b) evaluates default arguments as part of the function definition, and (c) allocates one mutable list for every call of that function.

Do not put a mutable object as the default value of a function parameter. Immutable types are perfectly safe. If you want to get the intended effect, do this instead:

def my_function2(param=None):
    if param is None:
        param = []
    param.append("thing")
    return param

Conclusion

Mutability matters. Learn it. Primitive-like types are probably immutable. Container-like types are probably mutable.

References

Python Objects: Mutable vs. Immutable