Перейти до змісту

Кортежі (tuple) та іменовані кортежі (namedtuple)

Довідник. Ноутбук уроку, де кортежі й NamedTuple з'являються вперше в контексті задачі (система чайових на реальних даних ресторану), — Урок 5.

1. Базовий кортеж (tuple)

Кортеж (tuple) — вбудована структура даних у Python, яка працює майже так само, як список (list), але з однією критичною відмінністю: кортежі є незмінними (immutable) послідовностями. Після створення об'єкта-кортежу ви не можете змінити, додати або видалити його елементи.

  • Створення: круглі дужки () або функція tuple(). Елементи розділяються комами.

Для чого використовується:

  1. Для зберігання даних, які не повинні змінюватися протягом роботи програми (наприклад, координати, налаштування).
  2. Для повернення декількох значень із функції.
  3. Як ключі для словників (dict), оскільки кортежі є хешованими (на відміну від списків).
# 1. Базове створення та спроба зміни
point = (10, 20, 30)
print("Координата X:", point[0])
# point[0] = 15  # ❌ TypeError! Кортежі незмінні

# 2. Реальний приклад: повернення декількох значень з функції
def get_screen_resolution():
    return 1920, 1080  # Python автоматично пакує це в кортеж!

width, height = get_screen_resolution()  # розпакування кортежу
print(f"Роздільна здатність: {width}x{height}")

# 3. Кортеж як ключ для словника (зі списком так не вийде)
map_grid = {
    (0, 0): "Старт",
    (1, 2): "Скарб",
    (5, 5): "Ворог",
}
print("Що на позиції (1, 2)?", map_grid[(1, 2)])

Результат:

Координата X: 10
Роздільна здатність: 1920x1080
Що на позиції (1, 2)? Скарб

Методи кортежу

Оскільки кортеж незмінний, він має лише два методи:

  • count(x) — кількість входжень елемента x;
  • index(x) — індекс першого входження елемента x.
history = ('login', 'click', 'view', 'click', 'logout')
print("Кількість кліків:", history.count('click'))
print("Індекс першого перегляду:", history.index('view'))

Результат:

Кількість кліків: 2
Індекс першого перегляду: 2

2. Іменований кортеж (namedtuple) — "незмінний словник"

Звичайний tuple дуже швидкий і зручний, але має недолік: щоб отримати доступ до даних, потрібно пам'ятати їхні числові індекси. Якщо полів багато (2-3 і більше), це стає незручним.

Іменований кортеж (namedtuple) дозволяє використовувати цю структуру як простий незмінний контейнер, що поєднує гнучкість повноцінних класів та зручність словників. Це фабрика класів, яка створює підкласи звичайного tuple — екземпляри іменованого кортежу залишаються абсолютно незмінними.

Чому namedtuple схожий на словник (dict), але кращий

  • Доступ за іменами: можна звертатися до значень не лише за числовими індексами чи через ітерування (все ще підтримується), а й за зрозумілими іменами атрибутів.
  • Незмінність: на відміну від словника, значення не можна перезаписати випадково — це гарантує цілісність даних.
  • Захист від помилок: не потрібно писати імена полів як рядкові ключі (як у словниках) — менше шансів на опечатку.
  • Конвертація в dict: будь-який namedtuple має вбудований ._asdict(), який миттєво конструює звичайний словник з його полів.
from collections import namedtuple

# 1. Оголошення структури (шаблону)
Car = namedtuple('Car', 'color mileage')

# 2. Створення екземпляра
my_car = Car(color='червоний', mileage=1234)

# 3. Доступ до даних (як у словнику, але через крапку)
print("Колір:", my_car.color)
print("Пробіг:", my_car.mileage)

# 4. Доступ за індексом зберігається
print("Індекс 0:", my_car[0])

# 5. Конвертація у словник
car_dict = my_car._asdict()
print("Словник:", car_dict)

# Реальний приклад: рядок з бази даних
DBRow = namedtuple('UserRow', ['id', 'email', 'role'])
user_record = DBRow(id=404, email='admin@site.com', role='superadmin')

print(f"\nКористувач {user_record.email} має роль {user_record.role}")

Результат:

Колір: червоний
Пробіг: 1234
Індекс 0: червоний
Словник: {'color': 'червоний', 'mileage': 1234}

Користувач admin@site.com має роль superadmin

3. Сучасний підхід: typing.NamedTuple

typing.NamedTuple — сучасний, вдосконалений "молодший брат" класичного collections.namedtuple. Починаючи з Python 3.6 він отримав підтримку зручного синтаксису класів (PEP 526) та можливість додавати анотації типів.

Синтаксис та базове створення

На відміну від старого підходу, де параметри передавалися рядком, NamedTuple використовує звичайне визначення класу. Кожне поле обов'язково повинно мати анотацію типу. Завдяки цьому код читабельніший, а в IDE працює автодоповнення й підсвічування помилок.

from typing import NamedTuple

# Оголошення структури
class UserProfile(NamedTuple):
    user_id: int
    phone: str
    status: str = "Active"  # можна задати значення за замовчуванням

# Створення об'єкта
user1 = UserProfile(user_id=101, phone="+380991234567")
print("Телефон:", user1.phone)
print("Статус (за замовчуванням):", user1.status)

Результат:

Телефон: +380991234567
Статус (за замовчуванням): Active

Ключові особливості typing.NamedTuple

  1. Абсолютна незмінність: оскільки NamedTuple є підкласом tuple, його екземпляри не можна змінювати після створення.
  2. Типізація лише для статичного аналізу: Python ігнорує ці типи під час виконання (runtime) — технічно можна передати рядок замість числа, і код не впаде. Але інструменти на кшталт mypy цю помилку відловлять.
  3. Гібридний доступ: дані доступні як за іменами атрибутів, так і за числовими індексами; підтримується розпакування.
  4. Економічність: екземпляри не використовують словник __dict__ під капотом — споживають так само мало пам'яті, як звичайні кортежі.

Власні методи та вбудовані функції

Синтаксис класів дозволяє додавати власні методи прямо в тіло NamedTuple. Він також сумісний із класичним namedtuple і має вбудовані методи (починаються з підкреслення):

  • ._asdict() — дані у вигляді словника;
  • ._replace(**kwargs) — створює новий екземпляр із заміненими полями;
  • ._fields — кортеж рядків з іменами всіх полів;
  • .__annotations__ — словник з інформацією про типи.
class Coordinate(NamedTuple):
    lat: float
    lon: float
    reference: str = 'WGS84'

    # Власний метод
    def get_formatted(self) -> str:
        ns = 'N' if self.lat >= 0 else 'S'
        we = 'E' if self.lon >= 0 else 'W'
        return f"{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}"

moscow = Coordinate(55.756, 37.617)
print("Форматований вивід:", moscow.get_formatted())

# Використання вбудованих методів
new_coord = moscow._replace(lon=40.000)  # зміна даних через створення копії
print("Нова координата:", new_coord)
print("Поля структури:", Coordinate._fields)

Результат:

Форматований вивід: 55.8°N, 37.6°E
Нова координата: Coordinate(lat=55.756, lon=40.0, reference='WGS84')
Поля структури: ('lat', 'lon', 'reference')

4. Чому typing.NamedTuple замість dict?

  1. Економія пам'яті: об'єкти NamedTuple реалізовані на основі кортежів, тому використовують пам'ять так само ефективно. Словники потребують значно більше місця через хеш-таблиці та зберігання імен ключів для кожного запису.
  2. Незмінність і цілісність даних: на відміну від словників (змінних — можна вільно додавати/видаляти/змінювати елементи), NamedTuple створює незмінні об'єкти.
  3. Захист від опечаток: словники не захищені від неправильного написання ключів; NamedTuple — жорсткий шаблон, що гарантує використання лише правильних імен полів.
  4. Зручний доступ до полів: user.name замість user['name'] — чистіший і зрозуміліший код.
  5. Статична перевірка типів: анотації полів допомагають інструментам на кшталт mypy виявляти помилки ще до запуску програми.
  6. Синтаксис class і розширюваність: легко задати значення за замовчуванням, додати власні методи чи docstring — недоступно для звичайних словників.

Висновок: typing.NamedTuple — гарний варіант за замовчуванням для простих записів, структур або об'єктів перенесення даних (DTO): проста, ефективна за пам'яттю структура з незмінними даними, захистом від опечаток і перевіркою типів.

# ПРАКТИЧНЕ ПОРІВНЯННЯ: обробка конфігурації

# ❌ Підхід зі словником (dict)
config_dict = {
    "host": "localhost",
    "port": 8080,
    "debug": True,
}
# Легко помилитися в імені ключа — програма впаде не одразу
# Легко випадково перезаписати: config_dict['port'] = 9999
print("Dict Config:", config_dict.get("host"))

# ✅ Підхід з NamedTuple
class ServerConfig(NamedTuple):
    host: str
    port: int
    debug: bool = False

config_obj = ServerConfig(host="localhost", port=8080, debug=True)
# config_obj.port = 9999  # одразу підсвітить помилку (незмінність!)
# config_obj.hst          # одразу підсвітить помилку (захист від опечаток!)
print("NamedTuple Config:", config_obj.host)

Результат:

Dict Config: localhost
NamedTuple Config: localhost


Дивись також: Списки · Множини · Словники