ПИТОН ОБЪЕКТЫ: ИЗМЕНЧИВЫЙ 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. НЕИЗМЕННЫЙ