Skip to content

Latest commit

 

History

History
212 lines (159 loc) · 10.8 KB

File metadata and controls

212 lines (159 loc) · 10.8 KB

Типы данных

← Оглавление · 🏠 Домой · Truthy and Falsy →


Какие типы в Python изменяемые, а какие нет?

Коротко. Изменяемые (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 передаёт аргументы по значению или по ссылке?

Коротко. Ни то, ни другое в классическом смысле — Python передаёт ссылку на объект (call by sharing). Копирования не происходит никогда, ни для каких типов.

Это самый частый вопрос по теме, и на нём же чаще всего ошибаются: кажется, что неизменяемые типы «передаются копией», раз функция не может их поменять. На деле внутрь всегда попадает ссылка на тот же объект. Просто изменить неизменяемый объект нельзя в принципе, поэтому любое присваивание внутри функции создаёт новый объект и перепривязывает локальное имя — снаружи ничего не меняется.

flowchart TB
    subgraph mut["Мутация изменяемого объекта видна снаружи"]
        direction LR
        a["имя a (снаружи)"] --> obj1["список [1, 2, 3, 4]"]
        lst["имя lst (внутри функции)"] --> obj1
    end
Loading
flowchart TB
    subgraph immut["Присваивание перепривязывает только локальное имя"]
        direction LR
        b["имя b (снаружи)"] --> obj2["int 10"]
        n["имя n (внутри)"] -.->|"было"| obj2
        n -->|"после n += 1"| obj3["новый int 11"]
    end
Loading
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.


Почему a is b для одинаковых строк то True, то False?

Коротко. 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() — это иногда используют для экономии памяти на множестве одинаковых ключей.


Почему t[0] += [1, 2] для кортежа падает с ошибкой и всё равно меняет список?

Коротко. Потому что += для списка — это две операции: сначала мутация на месте (__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]), присваивания не будет — и ошибки тоже.


Какие объекты можно использовать как ключи dict и элементы set?

Коротко. Только хешируемые: те, у которых есть __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

← Оглавление · 🏠 Домой · Truthy and Falsy →