Типы данных в Python
Первый вопрос на любом Python-собеседовании - и первый шанс показать, что ты понимаешь язык глубже, чем просто «list - это список»
Типы данных
Неизменяемые (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. Это не магия и не совпадение - это дизайн языка.