Группировка переменных в Python
Узнайте, как группировать переменные в Python с помощью классов, датаклассов, именованных кортежей, SimpleNamespace и словарей — с примерами.
Когда программе нужно отслеживать несколько связанных данных — например, имя пользователя, возраст и email — хранить их в отдельных, несвязанных переменных становится неудобно. Python предлагает несколько инструментов для группировки переменных под одним именем, чтобы они передавались вместе и оставались организованными. В этой главе рассматриваются наиболее распространённые подходы, когда использовать каждый из них и связанные компромиссы.
Темы:
- Зачем нужна группировка переменных
- Использование обычного словаря
- Использование
types.SimpleNamespaceдля точечного доступа - Использование
collections.namedtupleдля лёгких неизменяемых записей - Использование класса с
__init__ - Использование
@dataclass(Python 3.7+) для самого чистого синтаксиса - Выбор подходящего инструмента
Зачем группировать переменные?
Предположим, вы пишете скрипт, обрабатывающий учётные записи пользователей. Без группировки вы могли бы написать:
user_name = "Alice"
user_age = 30
user_email = "[email protected]"Это работает для одного пользователя, но перестаёт работать, когда нужно два пользователя или когда данные передаются в функцию:
def greet(name, age, email):
print(f"Hello {name}, age {age} ({email})")
greet(user_name, user_age, user_email)Три отдельных аргумента нужно синхронизировать везде. Группировка решает эту проблему, объединяя данные:
user = {"name": "Alice", "age": 30, "email": "[email protected]"}
def greet(user):
print(f"Hello {user['name']}, age {user['age']} ({user['email']})")
greet(user)Теперь у функции один параметр вместо трёх, а добавление нового поля затрагивает только словарь.
Использование словаря
Словарь Python — это самый простой способ группировки именованных переменных. Ключи — строки; значения могут быть любого типа.
point = {"x": 10, "y": 20, "label": "origin"}
print(point["x"]) # 10
print(point["label"]) # origin
# Update a field
point["x"] = 15
print(point)
# {'x': 15, 'y': 20, 'label': 'origin'}Когда использовать: Быстрая разовая группировка, данные JSON, ситуации, когда набор полей заранее не определён.
Недостатки: Доступ к полям осуществляется через строковые ключи (point["x"]), что многословнее точечной нотации и не даёт автодополнения в IDE.
Использование types.SimpleNamespace
SimpleNamespace — это тонкая обёртка, обеспечивающая точечный доступ в специальном пространстве имён без написания класса.
from types import SimpleNamespace
point = SimpleNamespace(x=10, y=20, label="origin")
print(point.x) # 10
print(point.label) # origin
# Update a field
point.x = 15
print(point)
# namespace(x=15, y=20, label='origin')Объекты SimpleNamespace изменяемы — вы можете добавлять, изменять или удалять атрибуты в любое время:
from types import SimpleNamespace
config = SimpleNamespace(debug=False, timeout=30)
config.debug = True # update
config.retries = 3 # add new attribute
del config.timeout # remove
print(vars(config))
# {'debug': True, 'retries': 3}Когда использовать: Замена словаря, когда нужен точечный доступ без методов и проверки типов. Хорошо подходит для тестовых фикстур и простых объектов конфигурации.
Использование collections.namedtuple
namedtuple — это неизменяемая лёгкая запись. Она ведёт себя как обычный кортеж, но позволяет обращаться к полям по имени, а не только по индексу.
from collections import namedtuple
# Define the type once
Point = namedtuple("Point", ["x", "y"])
# Create an instance
p = Point(x=10, y=20)
print(p.x) # 10
print(p.y) # 20
print(p[0]) # 10 — index access still works
print(p) # Point(x=10, y=20)Поскольку экземпляры namedtuple неизменяемы, изменить поле после создания нельзя:
from collections import namedtuple
Color = namedtuple("Color", ["red", "green", "blue"])
white = Color(255, 255, 255)
# white.red = 0 # AttributeError: can't set attributeЕсли нужна изменённая копия, используйте метод _replace() — он возвращает новый экземпляр:
from collections import namedtuple
Color = namedtuple("Color", ["red", "green", "blue"])
white = Color(255, 255, 255)
grey = white._replace(red=128, green=128, blue=128)
print(grey)
# Color(red=128, green=128, blue=128)Когда использовать: Неизменяемые записи, где важны имена полей — координаты, цвета RGB, строки базы данных. Меньше памяти, чем у полноценного класса.
Использование класса
Для группированных переменных, которым также нужно поведение (методы), определите класс с методом __init__:
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
def greet(self):
return f"Hello, I am {self.name} and I am {self.age} years old."
alice = User("Alice", 30, "[email protected]")
print(alice.name) # Alice
print(alice.greet()) # Hello, I am Alice and I am 30 years old.
# Update a field
alice.age = 31
print(alice.age) # 31Несколько экземпляров остаются независимыми — каждый хранит своё значение name, age и email:
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
alice = User("Alice", 30, "[email protected]")
bob = User("Bob", 25, "[email protected]")
print(alice.name, bob.name) # Alice BobКогда использовать: Когда сгруппированные данные также нуждаются в методах, логике валидации или наследовании. Классы — основа объектно-ориентированного Python — см. Классы и объекты Python для полного объяснения.
Использование @dataclass (Python 3.7+)
Декоратор @dataclass автоматически генерирует __init__, __repr__ и __eq__ из аннотированных полей класса, устраняя большую часть шаблонного кода:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
label: str = "unnamed"
p = Point(x=3.0, y=4.0)
print(p) # Point(x=3.0, y=4.0, label='unnamed')
print(p.label) # unnamed
p.label = "A"
print(p) # Point(x=3.0, y=4.0, label='A')Поля со значением по умолчанию должны идти после полей без значения по умолчанию (то же правило, что и для обычных аргументов функций).
Неизменяемый датакласс с frozen=True
Передайте frozen=True, чтобы запретить изменение любого поля после создания — по поведению похоже на namedtuple, но с полными возможностями класса:
from dataclasses import dataclass
@dataclass(frozen=True)
class RGB:
red: int
green: int
blue: int
white = RGB(255, 255, 255)
print(white)
# RGB(red=255, green=255, blue=255)
# white.red = 0 # FrozenInstanceError: cannot assign to field 'red'Группировка нескольких записей в список
Датаклассы естественно работают со списками, когда нужна коллекция записей:
from dataclasses import dataclass
from typing import List
@dataclass
class Product:
name: str
price: float
in_stock: bool = True
inventory: List[Product] = [
Product("Widget", 9.99),
Product("Gadget", 24.99),
Product("Doohickey", 4.50, in_stock=False),
]
for item in inventory:
status = "available" if item.in_stock else "out of stock"
print(f"{item.name}: ${item.price:.2f} ({status})")Вывод:
Widget: $9.99 (available)
Gadget: $24.99 (available)
Doohickey: $4.50 (out of stock)Полный набор возможностей датаклассов, включая field(), __post_init__ и наследование, см. в Python Dataclasses.
Группировка переменных через атрибуты класса
Иногда нужно хранить общие константы, привязанные к группе, а не данные экземпляра. Атрибуты класса (определённые непосредственно в теле класса, вне __init__) являются общими для всех экземпляров:
class AppConfig:
MAX_RETRIES = 3
TIMEOUT = 30
BASE_URL = "https://api.example.com"
print(AppConfig.MAX_RETRIES) # 3
print(AppConfig.BASE_URL) # https://api.example.comСоздавать экземпляр AppConfig для чтения его атрибутов не нужно — рассматривайте сам класс как пространство имён для связанных констант. Это лёгкий паттерн для групп конфигурации. Подробнее о разнице между атрибутами класса и атрибутами экземпляра см. в Классы и объекты Python.
Выбор подходящего инструмента
| Инструмент | Изменяемый | Точечный доступ | Методы | Аннотации типов | Лучше всего для |
|---|---|---|---|---|---|
dict | Да | Нет (["key"]) | Нет | Нет | Динамические / неизвестные поля |
SimpleNamespace | Да | Да | Нет | Нет | Разовая конфигурация, тестовые фикстуры |
namedtuple | Нет | Да | Нет | Частично | Неизменяемые записи, небольшие данные |
class | Да | Да | Да | Через аннотации | ООП с поведением |
@dataclass | Да* | Да | Да | Да | Структурированные записи с методами |
*frozen=True делает датакласс неизменяемым.
Практическое правило:
- Используйте
dict, когда структура заранее неизвестна. - Используйте
SimpleNamespace, когда нужен точечный доступ без определения класса. - Используйте
namedtupleдля простых неизменяемых записей (координаты, цвета, строки). - Используйте обычный
class, когда нужны методы и полноценное ООП. - Используйте
@dataclass, когда нужна структурированная запись с необязательными методами — он даёт больше всего при минимуме шаблонного кода.
Связанные темы
- Переменные Python — как работают переменные и правила именования
- Имена переменных — соглашения об именовании и лучшие практики
- Глобальные переменные — переменные уровня модуля и ключевое слово
global - Классы и объекты Python — полное объяснение ООП
- Python Dataclasses — подробный разбор
@dataclass - Присваивание нескольких значений — распаковка и множественное присваивание