Если вы только начинаете изучать 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())
В общем и целом, все :-)
Буду рад обратной связи, это моя первая статья на хабре, волнительно!
Комментарии (0)
santjagocorkez
17.09.2025 19:53Что-то я последний пример не понял. Каким образом объявленный и никак не задействованный Addable вдруг стал использоваться в коде реализации?
khanz Автор
17.09.2025 19:53Поправил использование Addable, спасибо за замечание, теперь тип T изменен с аргументом bound
BobovorTheCommentBeast
Синтаксис поменялся на человеческий, статья устарела. Нейронкой писали небось? Я эту скотину не могу приучить использовать новый синтаксис.
khanz Автор
В целом это статья на понимание концепции дженериков, учту насчет обновления
khanz Автор
Кстати да, нейронки не умеют нормально в дженерики, только после долгого обмывания костей так скажем)
нейронка только в плане каких то штук на копание материалов и сравнение, сейчас с ними что-то делается проще, если использовать с умом, остается только подход менять, мне не нравится концепт копировать и вставить без понимания, на vc.ru такого много в разделе разработки и рекламы
ammo
Еще бесит всякими Optional, List и т.д. Постоянно пытается их впихнуть