Если вы только начинаете изучать Python и слышите слово дженерики, скорее всего в голове сразу каша: «что это вообще такое?». На самом деле дженерики - это очень простая идея. Представьте, что у вас есть коробка. В коробку можно положить игрушки, яблоки, книжки - всё что угодно.
Но иногда вы хотите, чтобы в коробкележали только яблоки. А иногда — только игрушки. И вот тут вам помогают generics.
Что такое generics?
Generics — это способ написать класс или функцию один раз, но при этом заранее указать, с каким типом объектов он будет работать. Это как шаблон: «эта коробка для яблок», «эта корзина для бананов», «этот калькулятор для чисел».
В Python для этого используется модуль typing и конструкция TypeVar.
Пример 1. Корзина для предметов
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 может хранить что угодно: строки, числа, даже смешанные объекты. Это удобно, но небезопасно - легко ошибиться.
Теперь сделаем ту же коробку, но с дженериком:
if __name__ == '__main__': apple_box = Box('apple') print(apple_box.get_item()) number_box = Box(123) print(number_box.get_item()) apple_box = Box[str](1) # mypy error print(apple_box.get_item()) print(some_box.get_item())
Теперь в коде, где мы в коробку для яблок пытаемся положить число, mypy и ide подсветит, что мы делаем что-то не то.
Пример 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()) number_basket = Basket[int]() number_basket.add(1) number_basket.add(2) print(number_basket.get_all()) fruit_basket.add(2) # mypy/ide warning
Если попытаться добавить число в Basket[str], IDE и mypy сразу скажут, что это ошибка.
Пример 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)) # сработает, но mypy будет ругаться print(calc.add(3.5)) print(calc.add('s')) # warning
Пример 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()) product_repo = Repository[Product]() product_repo.add(Product('Iphone')) product_repo.add(Product('Laptop')) print(product_repo.get_all()) user_repo.add(Product('Table')) # warning product_repo.add(User('Miki')) # warning
Пример 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()) print(Summer(['a', 'b', 'c']).total())
В общем и целом, все :-)
Буду рад обратной связи, это моя первая статья на хабре, волнительно!
