Кортежи и NamedTuple: иммутабельные структуры
tuple - неизменяемая упорядоченная последовательность. На первый взгляд - просто иммутабельный list, но есть нюансы. И есть полезная типизированная версия - NamedTuple с именованными полями. В этом уроке - когда использовать кортежи, в чём отличие от списка, и зачем нужны namedtuples.
Создание кортежа
# Литерал
point = (1, 2, 3)
# Без скобок (запятая - главная часть синтаксиса)
point = 1, 2, 3
# Пустой
empty = ()
# С одним элементом - ВАЖНО: запятая обязательна
single = (1,) # tuple
not_tuple = (1) # это просто int 1 в скобках!
# Из iterable
chars = tuple("hello") # ('h', 'e', 'l', 'l', 'o')
nums = tuple([1, 2, 3]) # (1, 2, 3)
1, 2, 3 # тоже tuple - (1, 2, 3)
(1, 2, 3) # tuple
(1) # int!
(1,) # tuple из одного элемента
1, # тоже tuple - (1,)
Tuple vs List
Главные различия:
| Свойство | tuple | list |
|---|---|---|
| Изменяемость | immutable | mutable |
| Литерал | (1, 2) или 1, 2 | [1, 2] |
| Хешируемый | да (если элементы тоже) | нет |
| Используется как ключ dict | да | нет |
| Память | чуть меньше | чуть больше |
| Скорость доступа | чуть быстрее | чуть медленнее |
| Методы | count, index | много (append, pop, sort...) |
Когда какой:
- list - набор похожих элементов, который меняется (массив, очередь, список пользователей)
- tuple - фиксированная запись разнородных данных (координаты
(x, y), цвет(r, g, b), возвращаемое из функции)
Эмпирическое правило: если хочется обращаться по индексу с разным семантическим значением (item[0] - имя, item[1] - возраст), это скорее всего должен быть namedtuple или dataclass. Про изменяемый list - в предыдущем уроке.
Операции с tuple
Те же что у list, но без модификации:
t = (1, 2, 3, 4, 5)
t[0] # 1
t[-1] # 5
t[1:3] # (2, 3)
len(t) # 5
2 in t # True
t.count(2) # 1
t.index(3) # 2
# Конкатенация - возвращает новый tuple
t + (6, 7) # (1, 2, 3, 4, 5, 6, 7)
# Повторение
(1, 2) * 3 # (1, 2, 1, 2, 1, 2)
# Распаковка
a, b, c, d, e = t
first, *rest = t # first=1, rest=[2, 3, 4, 5] - rest всегда list!
*rest собирает остаток в list, не tuple - это особенность распаковки.
Иммутабельность - не глубокая
tuple immutable на верхнем уровне - нельзя переприсвоить элементы. Но если элемент сам mutable, его можно изменить:
t = ([1, 2], [3, 4])
t[0].append(99)
print(t) # ([1, 2, 99], [3, 4])
t[0] = [5, 6] # TypeError - переприсвоить нельзя
Это уже обсуждали в уроке про ссылки и изменчивость. Иммутабельность tuple - про структуру, не про содержимое ссылок.
Возврат нескольких значений из функции
Главное идиоматическое применение tuple:
def divmod(a, b):
return a // b, a % b # возвращает tuple (q, r)
q, r = divmod(7, 3) # 2, 1
Скобки опциональны - запятая делает tuple. Распаковка на стороне вызова даёт удобный синтаксис.
NamedTuple - кортеж с именованными полями
Если у tuple элементы имеют семантику, имена улучшают читаемость:
# Безымянный tuple - что есть что?
user = ("Alice", 30, "alice@example.com")
print(user[0]) # имя? фамилия?
# С namedtuple - явно
from collections import namedtuple
User = namedtuple("User", ["name", "age", "email"])
user = User("Alice", 30, "alice@example.com")
print(user.name) # Alice
print(user.age) # 30
print(user.email) # alice@example.com
# Можно по индексу и распаковывать
print(user[0]) # Alice
name, age, email = user
NamedTuple это полноценный tuple с дополнительным атрибутным доступом. Все методы tuple (count, index) работают.
typing.NamedTuple - современный синтаксис
С Python 3.5 есть class-based вариант с type hints:
from typing import NamedTuple
class User(NamedTuple):
name: str
age: int
email: str
user = User(name="Alice", age=30, email="alice@example.com")
print(user.name, user.age)
Преимущества:
- Type hints для каждого поля
- Можно добавлять методы (читать только, поскольку tuple immutable)
- Better support в IDE
- Возможны default-значения
class User(NamedTuple):
name: str
age: int
email: str = "unknown@example.com" # default
def display_name(self) -> str:
return f"{self.name} ({self.email})"
user = User("Alice", 30)
print(user.display_name()) # Alice (unknown@example.com)
NamedTuple vs dataclass
NamedTuple и @dataclass похожи, но разные:
| Свойство | NamedTuple | @dataclass |
|---|---|---|
| Изменяемость | immutable | mutable (или frozen=True) |
| Базовый класс | tuple | object |
Распаковка a, b = obj | работает | нет (без iter) |
| Память | компактнее | больше |
| Поддерживает наследование | сложно | проще |
| Методы | можно добавлять | можно добавлять |
| Mutable defaults | нельзя | нельзя без field |
Когда NamedTuple:
- Данные не меняются (DTO, results)
- Возвращаемое значение функции с несколькими полями
- Hashable нужен (можно как ключ dict)
- Распаковка удобна (
x, y = point)
Когда @dataclass:
- Объект меняется со временем
- Сложная логика (методы, наследование)
- Нужен большой публичный API
В новых проектах часто предпочитают @dataclass(frozen=True) - читается ближе к обычным классам.
Методы NamedTuple
User = namedtuple("User", ["name", "age", "email"])
user = User("Alice", 30, "a@b.c")
# Получить словарь
user._asdict() # {'name': 'Alice', 'age': 30, 'email': 'a@b.c'}
# Создать новый с заменой поля
older = user._replace(age=31)
print(older) # User(name='Alice', age=31, email='a@b.c')
print(user) # User(name='Alice', age=30, email='a@b.c') - оригинал не изменился
# Список имён полей
User._fields # ('name', 'age', 'email')
# Создать из iterable
data = ["Bob", 25, "b@b.c"]
user2 = User._make(data)
Методы начинаются с _ чтобы не конфликтовать с именами полей.
Подводные камни
1. Single-element tuple без запятой
single = (1) # это int 1!
single = (1,) # это tuple (1,)
single = 1, # тоже tuple
Без запятой Python не понимает что это кортеж - просто скобки группировки.
2. Распаковка не в tuple
first, *rest = (1, 2, 3, 4)
print(type(rest)) # list - не tuple!
Star-распаковка всегда даёт list, независимо от типа исходного.
3. Сравнение tuple
Кортежи сравниваются лексикографически - поэлементно:
(1, 2) < (1, 3) # True
(1, 2, 3) < (1, 2, 4) # True
(1, 2) < (1, 2, 0) # True - короткий "меньше"
Часто используется для сортировки по нескольким ключам:
sorted(users, key=lambda u: (u.last_name, u.first_name))
Размер в памяти
import sys
sys.getsizeof((1, 2, 3)) # ~64 байта
sys.getsizeof([1, 2, 3]) # ~88 байт - чуть больше
Tuple компактнее, потому что не нужно резервировать место под будущие append. Для миллионов маленьких записей это может быть важно.
Hash и использование как ключ
# tuple из hashable hashable
key = (1, "abc")
data = {key: "value"}
# tuple с mutable - не hashable
bad_key = (1, [2, 3])
hash(bad_key) # TypeError - unhashable type
Это позволяет использовать кортежи как ключи словаря для составных индексов:
inventory = {}
inventory[("warehouse-A", "SKU-001")] = 100
inventory[("warehouse-A", "SKU-002")] = 50
inventory[("warehouse-B", "SKU-001")] = 200
Сравнение с Go и PHP
Go не имеет встроенных кортежей. Похожее достигается через структуры:
type Point struct {
X, Y int
}
p := Point{1, 2}
Множественные возвращаемые значения - синтаксис языка, не структура:
func divmod(a, b int) (int, int) {
return a / b, a % b
}
PHP имеет ассоциативные массивы вместо специальных кортежей. С PHP 7.1 появилась распаковка через list().
Python tuple более компактный и идиоматичный для группировки разнородных данных.
Мини-задание
- Single-element tuple:
def is_tuple(obj):
return isinstance(obj, tuple)
print(is_tuple((1))) # False - это int
print(is_tuple((1,))) # True - tuple
print(is_tuple(1,)) # True - тоже tuple (запятая делает дело)
- NamedTuple как DTO:
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float
lng: float
elevation: float = 0.0
def distance_to(self, other):
# упрощённая евклидова
return ((self.lat - other.lat) ** 2 +
(self.lng - other.lng) ** 2 +
(self.elevation - other.elevation) ** 2) ** 0.5
a = Coordinate(55.75, 37.62) # Москва без elevation
b = Coordinate(51.50, -0.12, 100.0) # Лондон с elevation
print(a.distance_to(b))
- Tuple как ключ:
# Курсы валют между парами
rates = {
("USD", "EUR"): 0.92,
("USD", "GBP"): 0.79,
("EUR", "USD"): 1.09,
}
def convert(amount, frm, to):
if frm == to:
return amount
rate = rates.get((frm, to))
if rate is None:
raise ValueError(f"Нет курса {frm}->{to}")
return amount * rate
print(convert(100, "USD", "EUR")) # 92.0
Что дальше
Освоили tuple и NamedTuple. В следующем уроке - словари (dict): основная structure для key-value данных в Python.