Создание собственных типов данных в языке 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; создать экземпляр абстрактного класса без реализации всех методов.

- приватные атрибуты python (приватные атрибуты в python)
- создание типов python (создание пользовательских типов данных в python)
- тип данных класса python (тип данных класса в python)

Расширенные примеры пользовательских типов

Итератор на основе класса

Реализуем тип, который можно использовать в цикле 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) для автоматической настройки классов.

Создание пользовательских типов данных в Python - comments

En
создание типов python (python)