Создание собственных типов данных в языке Python
В Python любой тип данных можно определить самостоятельно. Пользовательские типы (классы) позволяют объединять данные и методы их обработки, реализовывать абстракции предметной области. В статье рассмотрены основные подходы к созданию таких типов, их особенности и типичные ошибки.
Основные подходы к определению пользовательских типов
Классы как основной инструмент
Как создать полноценный тип данных с атрибутами и методами?
Базовый способ - объявление класса с помощью ключевого слова class. Внутри определяются конструктор __init__, строковое представление __str__ и другие необходимые методы.
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __str__(self) -> str:
return f"Point({self.x}, {self.y})"
def distance_from_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
атрибуты класса python (атрибуты классов и объектов в python)
# Использование: p = Point(3, 4) print(p) # Point(3, 4) print(p.distance_from_origin()) # 5.0
библиотека классов python (библиотека классов в python)
Конструктор инициализирует атрибуты экземпляра. Метод __str__ возвращает читаемое представление. Дополнительные методы реализуют поведение.
Типичные ошибки: забыть вызвать super().__init__() при наследовании от другого класса; не определить __repr__ (отладочное представление); использовать изменяемые объекты как значения по умолчанию (например, список в аргументе).
Вариант: dataclasses для простых структур данных
Как упростить создание классов, которые в основном хранят данные?
Декоратор @dataclass автоматически генерирует __init__, __repr__, __eq__ и другие методы на основе аннотаций полей.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
label: str = "A"
метод объекта python (методы объектов в python)
pt = Point(1.0, 2.0, "B") print(pt) # Point(x=1.0, y=2.0, label='B') print(pt == Point(1.0, 2.0, "B")) # True
Python структура объекта (структура объекта в python)
Класс получается компактным, поля автоматически документируются. Можно добавлять собственные методы.
Ошибки: забыть импортировать dataclass; ошибочно указать изменяемый тип по умолчанию (тогда используется field(default_factory=list)).
Вариант: namedtuple для неизменяемых объектов
Как создать лёгкий неизменяемый тип с именованными полями?
Фабричная функция namedtuple из модуля collections создаёт класс, экземпляры которого ведут себя как кортежи, но поля доступны по имени.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p[0]) # 3
Python создание объектов (создание объектов в python)
# Неизменяемость: # p.x = 5 # AttributeError
Self object python (объект self в python)
Экземпляры занимают меньше памяти, чем обычные классы. Подходит для простых структур без поведения.
Ошибки: имена полей не должны быть ключевыми словами Python; нельзя наследовать поведение; методы добавляются через наследование от сгенерированного класса, что неочевидно.
Вариант: typing.NamedTuple с аннотациями
Как совместить преимущества namedtuple и подсказки типов?
typing.NamedTuple позволяет задавать поля с типами и значениями по умолчанию, а также добавлять методы.
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
label: str = "origin"
def distance(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
Object attribute python (атрибуты объекта в python)
p = Point(0, 0) print(p.distance()) # 0.0 print(p.label) # origin
Python call method (вызов метода в python)
Типизация улучшает читаемость и поддержку IDE, но экземпляры остаются неизменяемыми.
Проблемы: нельзя изменить поле после создания; наследование от другого NamedTuple ограничено.
Вариант: динамическое создание классов через type()
Как создать класс программно, без использования синтаксиса class?
Функция type(name, bases, dict) возвращает новый класс, где name - имя класса, bases - кортеж базовых классов, dict - словарь атрибутов и методов.
def move(self, dx, dy):
self.x += dx
self.y += dy
Point = type('Point', (), {
'__init__': lambda self, x, y: setattr(self, 'x', x) or setattr(self, 'y', y),
'move': move,
'__repr__': lambda self: f"Point({self.x}, {self.y})"
})
p = Point(1, 2)
p.move(3, 4)
print(p) # Point(4, 6)
Python класс данных (класс данных в python)
# type() применяется при реализации метаклассов и фабрик классов.
Class method python (методы классов в python)
Подход полезен, когда структура класса определяется во время выполнения, например, на основе конфигурации.
Ошибки: легко ошибиться в области видимости; методы должны быть правильно привязаны к экземпляру (использовать self); отладка усложнена.
Вариант: абстрактные базовые классы для интерфейсов
Как задать общий контракт для группы родственных типов?
Модуль abc предоставляет ABC и декоратор abstractmethod. Абстрактные методы обязательно переопределяются в подклассах.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
Python object methods (методы объектов в python)
c = Circle(5) print(c.area()) # 78.53975 # Shape() вызовет TypeError
Обеспечивает полиморфизм и проверку на этапе инстанцирования.
Ошибки: забыть унаследоваться от ABC; создать экземпляр абстрактного класса без реализации всех методов.
Расширенные примеры пользовательских типов
Итератор на основе класса
Реализуем тип, который можно использовать в цикле for. Для этого определяются методы __iter__ и __next__.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for num in Countdown(3):
print(num)
3 2 1
Цикл for автоматически вызывает __iter__ и затем __next__ до возникновения StopIteration.
Контекстный менеджер (менеджер контекста)
Тип, поддерживающий конструкцию with, должен реализовать __enter__ и __exit__.
class ManagedFile:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.file = open(self.filename, 'w')
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
with ManagedFile('test.txt') as f:
f.write('Hello, world!')
# Файл test.txt создан и записан, после блока with автоматически закрыт.
Метод __exit__ вызывается даже при исключении, что гарантирует освобождение ресурсов.
Дескриптор для валидации атрибутов
Дескриптор - это класс, реализующий __get__, __set__ или __delete__. Используется для перехвата доступа к атрибуту.
class PositiveNumber:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError(f"{self.name} must be positive")
obj.__dict__[self.name] = value
class Order:
quantity = PositiveNumber()
price = PositiveNumber()
def __init__(self, quantity, price):
self.quantity = quantity
self.price = price
o = Order(10, 2.5)
print(o.quantity) # 10
# o.quantity = -1 # ValueError
10
Дескрипторы позволяют реализовать валидацию, преобразование и вычисляемые атрибуты.
Пользовательское исключение
Собственные типы исключений наследуют от Exception (или его подкласса) и несут дополнительную информацию.
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Недостаточно средств: баланс {balance}, требуется {amount}")
class BankAccount:
def __init__(self, owner, initial_balance=0):
self.owner = owner
self.balance = initial_balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
acc = BankAccount("Иван", 100)
acc.withdraw(50)
# acc.withdraw(200) # InsufficientFundsError
# При вызове acc.withdraw(200) возникнет исключение с сообщением.
Такие исключения упрощают обработку ошибок в сложных системах.
Метакласс для автоматической регистрации классов
Метакласс - это класс, экземпляром которого является класс. Позволяет изменять процесс создания класса.
class RegistryMeta(type):
registry = {}
def __new__(cls, name, bases, attrs):
new_class = super().__new__(cls, name, bases, attrs)
if name != 'BasePlugin': # Не регистрировать базовый класс
cls.registry[name] = new_class
return new_class
class BasePlugin(metaclass=RegistryMeta):
pass
class PluginA(BasePlugin):
def run(self):
print("Plugin A executed")
class PluginB(BasePlugin):
def run(self):
print("Plugin B executed")
print(RegistryMeta.registry)
{'PluginA': , 'PluginB': }
Метаклассы используются во фреймворках (Django, SQLAlchemy) для автоматической настройки классов.