Generics
Generics
Generics — это способ написать класс или функцию один раз, но при этом заранее указать, с каким типом объектов он будет работать. Для этого в Python используется модуль typing и конструкция TypeVar, позволяющая задать универсальный параметр типа. Например, можно создать класс Box, который будет работать с любым типом T, и методы этого класса будут возвращать и принимать значения именно этого типа. Это улучшает читаемость кода и позволяет статическим анализаторам (например, mypy) проверять соответствие типов на этапе разработки.
Кроме TypeVar, часто применяется класс Generic, который позволяет создавать универсальные классы с параметрами типов. В Python 3.9+ появились обобщённые типы для встроенных коллекций (list, dict и др.), которые можно параметризовать напрямую как list[int] или dict[str, float]. Python 3.12 добавляет новый синтаксис для generic-типов, улучшающий удобство и гибкость их использования.
Основные возможности и примеры применения generics в Python:
-
Универсальные контейнеры, например корзина или репозиторий, которые могут хранить элементы произвольного типа с типобезопасностью.
-
Функции, которые принимают и возвращают значения одного и того же параметризованного типа.
-
Поддержка ковариантности и контравариантности через параметры типа.
-
Поддержка обобщённых коллекций с аннотациями типов, улучшенная в новых версиях Python.
Пример простого generic-класса:
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
def get_item(self) -> T:
return self.item
Такой класс Box можно использовать с любым типом, например Box[int] или Box[str], и это будет проверяться статическим анализатором.
Теперь сделаем ту же коробку, но с дженериком:
Пример 1. Корзина для предметов
if __name__ == '__main__':
apple_box = Box('apple')
print(apple_box.get_item()) # выведет 'apple'
number_box = Box(123)
print(number_box.get_item()) # выведет 123
apple_box = Box[str](1) # mypy выдаст ошибку, так как ожидается строка, а передаётся число
print(apple_box.get_item()) # выведет 1, несмотря на ошибку mypy — в рантайме Python не проверяет типы
print(some_box.get_item()) # вызовет ошибку NameError, так как some_box не определён в коде
Пример 2. Коллекция с ограничением типов
Сделаем корзину (Basket), куда можно складывать предметы только одного типа:
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Basket(Generic[T]):
def __init__(self):
self.items: List[T] = []
def add(self, item: T) -> None:
self.items.append(item)
def get_all(self) -> List[T]:
return self.items
if __name__ == '__main__':
fruit_basket = Basket[str]()
fruit_basket.add("apple")
fruit_basket.add("orange")
print(fruit_basket.get_all()) # Выведет ['apple', 'orange']
number_basket = Basket[int]()
number_basket.add(1)
number_basket.add(2)
print(number_basket.get_all()) # Выведет [1, 2]
fruit_basket.add(2) # mypy/IDE выдаст предупреждение, т.к. добавляется int вместо str
Пример 3. Ограниченные дженерики (bound)
Иногда нужно разрешить только числа, а не всё подряд. Тогда мы говорим: «T должен быть числом» (bound=float):
from typing import Generic, TypeVar
NumberT = TypeVar('NumberT', bound=float)
class Calculator(Generic[NumberT]):
def __init__(self, value: NumberT) -> None:
self.value = value
def add(self, other: NumberT) -> NumberT:
return self.value + other
if __name__ == '__main__':
calc = Calculator(10.5)
print(calc.add(2)) # сработает, выведет 12.5, но mypy выдаст предупреждение, так как передается int, а ожидается float
print(calc.add(3.5)) # сработает и выведет 14.0
print(calc.add('s')) # вызовет ошибку времени выполнения TypeError при попытке сложить float и str, но mypy выдаст предупреждение заранее
Пример 4. Репозиторий
Представьте, что у нас есть база данных с разными сущностями: User, Product. Вместо того чтобы писать одинаковый код для каждой, мы можем сделать дженерик-репозиторий:
from typing import Generic, TypeVar
T = TypeVar('T')
class Repository(Generic[T]):
def __init__(self):
self.items: list[T] = []
def add(self, item: T) -> None:
self.items.append(item)
def get_all(self) -> list[T]:
return self.items
class User:
def __init__(self, name: str) -> None:
self.name = name
def __repr__(self):
return f"User: {self.name}"
class Product:
def __init__(self, title: str) -> None:
self.title = title
def __repr__(self):
return f"Production: {self.title}"
if __name__ == '__main__':
user_repo = Repository[User]()
user_repo.add(User('Alice'))
user_repo.add(User('Bob'))
print(user_repo.get_all()) # выведет [User: Alice, User: Bob]
product_repo = Repository[Product]()
product_repo.add(Product('Iphone'))
product_repo.add(Product('Laptop'))
print(product_repo.get_all()) # выведет [Production: Iphone, Production: Laptop]
user_repo.add(Product('Table')) # предупреждение mypy, но выполнится — в user_repo появится Product('Table')
product_repo.add(User('Miki')) # предупреждение mypy, но выполнится — в product_repo появится User('Miki')
Пример 5. Дженерики + Protocol
А что если мы хотим складывать объекты (например, числа или строки)? Тогда можно сказать: «принимаю любой тип, у которого есть оператор +».
from typing import Protocol, TypeVar, Generic
class Addable(Protocol):
def __add__(self, other: "Addable") -> "Addable": ...
T = TypeVar("T", covariant=True, bound=Addable)
class Summer(Generic[T]):
def __init__(self, items: list[T]) -> None:
self.items = items
def total(self) -> T:
result = self.items[0]
for item in self.items[1:]:
result += item
return result
if __name__ == '__main__':
print(Summer([1, 2, 3]).total()) # выведет 6 (сложение целых чисел)
print(Summer(['a', 'b', 'c']).total()) # выведет 'abc' (конкатенация строк)
Таким образом, generics в Python позволяют писать более универсальный и типобезопасный код, облегчая поддержку и проверку программных систем.