Зачем нужно разделение типов данных на изменяемые и неизменяемые
Что проверяют
Понимаете ли вы, какие ошибки в Python и в пайплайнах данных растут из изменяемости, а не просто можете перечислить типы. Следом обычно дают код с аргументом по умолчанию def f(x=[]) или просят объяснить, почему список нельзя сделать ключом словаря.
Как отвечать
Неизменяемые: int, float, str, bytes, tuple, frozenset. Изменяемые: list, dict, set, bytearray, а ещё массивы NumPy, DataFrame и тензоры. Переменная в Python это ссылка на объект, поэтому изменяемость решает, увидят ли изменение все, кто держит ту же ссылку.
Зачем разделение нужно:
- хешируемость: ключ
dictи элементsetобязаны иметь стабильный хеш. Поэтому ключом может быть кортеж, но не список, а кортеж со списком внутри тоже не хешируется; - безопасная передача: неизменяемый объект можно отдать в функцию или другой поток без страха, что его поменяют. Отсюда
@dataclass(frozen=True)для конфигов экспериментов; - оптимизации интерпретатора: интернирование строк и кеш маленьких целых возможны только потому, что объекты не меняются;
- дешёвые изменения: дополнить список дешевле, чем каждый раз собирать новый кортеж. Строки в цикле склеивают через
"".join(...), а не+=.
В данных это важнее всего. Срез массива NumPy возвращает view, и запись в него меняет исходный массив. В pandas из-за этого годами жило предупреждение SettingWithCopyWarning, а в pandas 3.0 по умолчанию работает Copy-on-Write: изменение производного объекта не трогает исходный. В PyTorch операции с подчёркиванием вроде add_() меняют тензор на месте и могут сломать вычисление градиентов.
Где спотыкаются
Пишут изменяемый аргумент по умолчанию. Список создаётся один раз при определении функции и копит значения между вызовами. Правильно x=None и создание списка внутри.
Путают b = a с копированием. Для вложенных структур и copy.copy мало, нужен copy.deepcopy.
Считают кортеж полностью неизменяемым. Неизменен только набор ссылок, список внутри кортежа по-прежнему можно дополнить.