Типы данных в Python

Первый вопрос на любом Python-собеседовании - и первый шанс показать, что ты понимаешь язык глубже, чем просто «list - это список»

Типы данных в Python.md

Типы данных

Неизменяемые (Immutable) - объект нельзя изменить после создания:

  • int - целые числа
  • float - числа с плавающей точкой
  • complex - комплексные числа
  • bool - булев тип (True / False)
  • str - строки
  • tuple - кортеж
  • frozenset - замороженное множество
  • bytes - байты
  • NoneType - тип None

Изменяемые (Mutable) - объект можно изменять после создания:

  • list - список
  • dict - словарь
  • set - множество
  • bytearray - изменяемые байты

В Python типы делятся на изменяемые и неизменяемые. Неизменяемые - int, float, bool, str, tuple, frozenset, bytes, NoneType - после создания не меняются, любая «модификация» создаёт новый объект в памяти. Изменяемые - list, dict, set, bytearray - меняются на месте, id объекта остаётся прежним. Практически это важно в трёх местах: хэшируемость, поведение при передаче в функцию, и ловушка с изменяемым дефолтным аргументом.


Простыми словами

Каждый объект в Python живёт в памяти по какому-то адресу. Функция id() этот адрес показывает.

Если объект неизменяемый - содержимое по этому адресу не меняется никогда. Когда ты «меняешь» строку или число, Python создаёт новый объект и перепривязывает переменную к нему. Старый объект остаётся в памяти, пока сборщик мусора его не уберёт.

Если объект изменяемый - содержимое меняется прямо на месте. id() до и после изменения одинаковый.


Код

# Неизменяемый - id меняется, создаётся новый объект
s = "hello"
print(id(s))    # 140234567

s += " world"
print(id(s))    # 140234999 - уже другой адрес


# Изменяемый - id остаётся, объект меняется на месте
lst = [1, 2, 3]
print(id(lst))  # 140235111

lst.append(4)
print(id(lst))  # 140235111 - тот же адрес
# Хэшируемость - неизменяемые можно использовать как ключи словаря
d = {(1, 2): "tuple как ключ"}   # работает
d = {[1, 2]: "список как ключ"}  # TypeError: unhashable type
# Передача в функцию
def modify(x):
    x += [4]       # список меняется на месте

def no_modify(x):
    x += " world"  # создаётся новый объект строки

lst = [1, 2, 3]
modify(lst)
print(lst)  # [1, 2, 3, 4] - изменился

s = "hello"
no_modify(s)
print(s)    # "hello" - не изменился
# Ловушка с дефолтным аргументом
def add_item(item, lst=[]):   # список создаётся ОДИН РАЗ при определении функции
    lst.append(item)
    return lst

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] - список не сбросился!

# Правильно
def add_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst
# Edge case - tuple с изменяемым содержимым
t = ([1, 2], [3, 4])
t[0].append(99)
print(t)  # ([1, 2, 99], [3, 4])
# Сам tuple не изменился - те же ссылки на те же объекты
# Но содержимое вложенного списка поменялось

Глубже

Разделение на изменяемые и неизменяемые типы - это не просто классификация, а фундаментальное архитектурное решение языка с конкретными последствиями.

Хэшируемость. Хэш объекта вычисляется из его содержимого. Если содержимое может меняться - хэш тоже изменится, и объект «потеряется» внутри словаря или множества. Поэтому хэш есть только у неизменяемых объектов. frozenset существует именно для этого - это set, который можно положить внутрь другого set или использовать как ключ словаря.

Передача в функцию. Python использует модель «передача по ссылке на объект» (pass by object reference). Это не передача по значению (как в C с примитивами) и не передача по ссылке в классическом смысле. Функция получает копию ссылки на объект. Если объект изменяемый - через эту ссылку его можно испортить. Если неизменяемый - любая «модификация» внутри функции создаст новый локальный объект, оригинал не тронется.

Строки. Неизменяемость строк даёт три бонуса: безопасность в многопоточном коде (несколько потоков читают одну строку без блокировок), кэширование хэша (вычисляется один раз при первом использовании как ключа), и интернирование - Python может хранить одну копию часто используемых строк и давать на неё несколько ссылок.

bool - это int. bool буквально наследуется от int. True == 1, False == 0, и True + True вернёт 2. Это не магия и не совпадение - это дизайн языка.


FAQ

Чем tuple отличается от list? `tuple` - неизменяемый, `list` - изменяемый. `tuple` занимает меньше памяти, быстрее итерируется и может быть ключом словаря. Используй `tuple` для данных, которые не должны меняться - координаты, RGB-цвета, возвращаемые пары значений из функции.
Почему строки неизменяемые? Это архитектурное решение. Неизменяемость строк обеспечивает потокобезопасность, позволяет кэшировать хэш и даёт возможность интернирования - Python может хранить одну копию одинаковых строк в памяти и раздавать ссылки на неё.
Что такое frozenset и зачем он нужен? Это неизменяемый `set`. Нельзя добавлять или удалять элементы. Зато, в отличие от обычного `set`, его можно использовать как ключ словаря или положить внутрь другого `set`. Полезен, когда нужна семантика множества, но объект должен быть хэшируемым.
bool - это подтип int? Да. `bool` наследуется от `int`. `True == 1` и `False == 0` - не совпадение, а буквальное наследование. `True + True` вернёт `2`, `isinstance(True, int)` вернёт `True`.
Можно ли изменить элемент внутри tuple? Сам `tuple` изменить нельзя - присвоение `t[0] = x` упадёт с `TypeError`. Но если внутри лежит изменяемый объект (например, список), его содержимое изменить можно. Формально `tuple` при этом не меняется - в нём хранятся ссылки, и ссылки остаются теми же.