Если вы только начинаете изучать 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)


  1. BobovorTheCommentBeast
    17.09.2025 19:53

    Синтаксис поменялся на человеческий, статья устарела. Нейронкой писали небось? Я эту скотину не могу приучить использовать новый синтаксис.


    1. khanz Автор
      17.09.2025 19:53

      В целом это статья на понимание концепции дженериков, учту насчет обновления


    1. khanz Автор
      17.09.2025 19:53

      Кстати да, нейронки не умеют нормально в дженерики, только после долгого обмывания костей так скажем)
      нейронка только в плане каких то штук на копание материалов и сравнение, сейчас с ними что-то делается проще, если использовать с умом, остается только подход менять, мне не нравится концепт копировать и вставить без понимания, на vc.ru такого много в разделе разработки и рекламы


    1. ammo
      17.09.2025 19:53

      Еще бесит всякими Optional, List и т.д. Постоянно пытается их впихнуть


  1. upcFrost
    17.09.2025 19:53

    А потом чпок - и ide их просто не распознает


  1. santjagocorkez
    17.09.2025 19:53

    Что-то я последний пример не понял. Каким образом объявленный и никак не задействованный Addable вдруг стал использоваться в коде реализации?


    1. khanz Автор
      17.09.2025 19:53

      Поправил использование Addable, спасибо за замечание, теперь тип T изменен с аргументом bound