← Оглавление · 🏠 Домой · Truthy and Falsy →
Коротко. Изменяемые (mutable) можно поменять на месте, не создавая новый
объект: list, dict, set, bytearray. Неизменяемые (immutable) — нельзя:
int, float, str, bool, tuple, bytes, frozenset, complex,
range, NoneType.
Разница не в том, «меняется ли значение переменной», а в том, меняется ли сам
объект в памяти. Присваивание x = x + 1 для int не меняет объект — оно
создаёт новый объект и перепривязывает имя. А lst.append(1) меняет тот самый
объект, на который смотрят все ссылающиеся на него имена.
a = [1, 2, 3]
b = a # b и a — одно и то же
a.append(4)
# b -> [1, 2, 3, 4] изменился «тоже», потому что это один объект
s = "abc"
t = s
s += "d" # создан НОВЫЙ объект, t остался на старом
# t -> 'abc'Подвох. «tuple неизменяемый — значит, его содержимое не поменяется?» Нет.
Неизменяем сам кортеж (набор ссылок), но если внутри лежит список, его менять
можно — см. вопрос про += ниже.
Глубже. frozenset — неизменяемый и хешируемый аналог set. range —
тоже неизменяемая последовательность, а не генератор: у неё есть len(),
индексация и срезы. namedtuple часто упоминают рядом, но это не встроенный
тип, а фабрика классов из collections — каждый вызов создаёт новый подкласс
tuple.
Коротко. Ни то, ни другое в классическом смысле — Python передаёт ссылку на объект (call by sharing). Копирования не происходит никогда, ни для каких типов.
Это самый частый вопрос по теме, и на нём же чаще всего ошибаются: кажется, что неизменяемые типы «передаются копией», раз функция не может их поменять. На деле внутрь всегда попадает ссылка на тот же объект. Просто изменить неизменяемый объект нельзя в принципе, поэтому любое присваивание внутри функции создаёт новый объект и перепривязывает локальное имя — снаружи ничего не меняется.
flowchart TB
subgraph mut["Мутация изменяемого объекта видна снаружи"]
direction LR
a["имя a (снаружи)"] --> obj1["список [1, 2, 3, 4]"]
lst["имя lst (внутри функции)"] --> obj1
end
flowchart TB
subgraph immut["Присваивание перепривязывает только локальное имя"]
direction LR
b["имя b (снаружи)"] --> obj2["int 10"]
n["имя n (внутри)"] -.->|"было"| obj2
n -->|"после n += 1"| obj3["новый int 11"]
end
def change_list(lst):
lst.append(4) # мутация объекта -> видна снаружи
def change_int(n):
n += 1 # новый объект + перепривязка локального имени
a, b = [1, 2, 3], 10
change_list(a)
change_int(b)
# a -> [1, 2, 3, 4]
# b -> 10Что объект тот же самый, легко проверить по id(): внутри функции он совпадает
с внешним даже для immutable-аргумента.
Подвох. «А как тогда вернуть изменённое число?» Только return — или
завернуть значение в изменяемый контейнер. Попытка «поменять аргумент» работает
исключительно через мутацию изменяемого объекта.
Глубже. Ловушка отсюда же — изменяемое значение по умолчанию. Оно вычисляется один раз при определении функции, а не при каждом вызове:
def f(x, acc=[]):
acc.append(x)
return acc
f(1) # [1]
f(2) # [1, 2] — тот же список!Правильно — def f(x, acc=None): acc = [] if acc is None else acc.
Коротко. is сравнивает идентичность объектов, а не значения. Совпадение
для литералов — результат оптимизаций CPython (кеширование и интернирование), а
не гарантия языка. Для сравнения значений всегда ==.
Два разных механизма дают похожий эффект:
a, b = 10, 400
a is (a + 1 - 1) # True — маленькие int закешированы
b is (b + 1 - 1) # False — 400 в кеш не попадаетЦелые числа от −5 до 256 создаются один раз при старте интерпретатора, и на них просто раздаются ссылки. Для строк работает интернирование: строки, похожие на идентификаторы, компилятор складывает в общую таблицу.
Подвох. Классический пример с "Hello, World!" is "Hello, World!" даёт
разный результат в зависимости от того, где его запустить, — и это важнее
самого примера. В REPL каждая строка компилируется отдельно, поэтому получится
False. В .py-файле обе константы попадают в один code object и
дедуплицируются компилятором — будет True. Поэтому такие проверки не показатель
ничего: правило простое — is только для None и синглтонов, == для значений.
Глубже. Явно положить строку в таблицу интернирования можно через
sys.intern() — это иногда используют для экономии памяти на множестве
одинаковых ключей.
Коротко. Потому что += для списка — это две операции: сначала мутация на
месте (__iadd__), потом попытка присвоить результат обратно в кортеж. Первая
успевает пройти, вторая падает.
t = ([],)
t[0] += [1, 2, 3]
# TypeError: 'tuple' object does not support item assignment
# но:
t # ([1, 2, 3],) — список УЖЕ изменёнПодвох. Отсюда следует, что кортеж с изменяемым элементом нельзя положить в
dict или set: hash() по нему упадёт, потому что хеш кортежа считается по
хешам его элементов, а у списка хеша нет.
Глубже. Порядок в байткоде: __iadd__ мутирует список и возвращает его же,
затем выполняется STORE_SUBSCR, который и выбрасывает TypeError. Если
заменить += на t[0].extend([1, 2, 3]), присваивания не будет — и ошибки
тоже.
Коротко. Только хешируемые: те, у которых есть __hash__ и хеш не меняется
за время жизни объекта. Изменяемые встроенные типы (list, dict, set)
не хешируемы намеренно.
Обычные пользовательские классы хешируемы по умолчанию: они наследуют от
object реализации, основанные на идентичности объекта.
class A:
def __eq__(self, other): return True
hash(A())
# TypeError: unhashable type: 'A'Подвох. Как только вы определяете __eq__, Python убирает унаследованный
__hash__ — объект становится нехешируемым. Если равные объекты должны
попадать в set/dict, нужно определить и __hash__ так, чтобы равные
объекты давали одинаковый хеш.
Глубже. Реализация по умолчанию у object: __eq__ сравнивает
идентичность и возвращает NotImplemented, если объекты разные (дальше
интерпретатор сам сводит это к False); __hash__ строится из адреса объекта
через циклический сдвиг указателя. Это деталь реализации CPython, а не правило
языка, — полагаться на конкретную формулу нельзя.
python 3.10+ — вместо Union[int, str] пишется int | str, в том числе
в isinstance (раньше там требовался именно кортеж):
# python 3.10+
def foo(x: int | str) -> None: ...
isinstance(1, int | str) # True