W3docs

Группировка переменных в 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, когда нужна структурированная запись с необязательными методами — он даёт больше всего при минимуме шаблонного кода.

Связанные темы

Практика

Практика
In Python, what are the main reasons for grouping variables into classes?
In Python, what are the main reasons for grouping variables into classes?
Was this page helpful?