- Аннотировать функции и переменные и объяснять, что аннотации не проверяются во время выполнения
- Описывать сложные типы с помощью
Callable,Literal, обобщённых функций иProtocol - Писать класс данных с
@dataclass,field(default_factory=...)и__post_init__ - Уместно выбирать параметры
frozen,orderиslots
Посмотри на def price(total, discount): discount — это 0,1 или 10? Может ли total быть None? В маленьком скрипте ты это помнишь, а в проекте на 50 000 строк, над которым работают пять программистов, — нет. Аннотации типов записывают ответ прямо в код: def price(total: float, discount: float) -> float. Редакторы используют их для автодополнения, а анализаторы вроде mypy находят ошибки ещё до запуска программы. Dataclasses построены на тех же аннотациях и пишут скучные части класса за тебя.
Аннотации функций и переменных
Аннотация пишется после двоеточия, а тип возвращаемого значения — после стрелки ->. Начиная с Python 3.9 встроенные коллекции можно параметризовать напрямую: list[int], dict[str, float], tuple[int, int], set[str]. Начиная с Python 3.10 объединение типов записывается через |: int | None означает целое число или None. Аннотировать можно и переменные: count: int = 0.
def average(scores: list[float], precision: int = 1) -> float:
return round(sum(scores) / len(scores), precision)
print(average([4.5, 5, 3.5]))
print(average.__annotations__)
print(average((1, 2)))▸ Ожидаемый результат
4.3
{'scores': list[float], 'precision': <class 'int'>, 'return': <class 'float'>}
1.5Последняя строка важна: кортеж (1, 2) был принят, хотя в аннотации указано list[float]. Python не проверяет аннотации во время выполнения. Они хранятся в __annotations__ и читаются инструментами. Чтобы действительно находить ошибки, запусти статический анализатор типов:
$ mypy shop.py
shop.py:12: error: Argument 1 to "average" has incompatible type "str"; expected "list[float]" [arg-type]
Found 1 error in 1 file (checked 1 source file)pip install mypy и анализирует файл, не запуская его.Инструменты модуля typing
Callable[[int], int]— функция, которая принимаетintи возвращаетint.Literal['r', 'w']— допустимы только именно эти значения.type Vector = list[float](Python 3.12+) — псевдоним типа: короткое имя для длинного типа.def first[T](items: list[T]) -> T(Python 3.12+) — обобщённая функция:T— переменная типа, поэтому дляlist[str]результат будетstr. В старых версиях это пишут черезT = TypeVar('T').Any— отключает проверку; используй его как можно реже.
from typing import Callable, Literal
type Vector = list[float]
Mode = Literal['r', 'w']
def scale(v: Vector, k: float) -> Vector:
return [k * x for x in v]
def first[T](items: list[T], default: T | None = None) -> T | None:
return items[0] if items else default
def apply(func: Callable[[int], int], value: int) -> int:
return func(value)
def describe(mode: Mode) -> str:
return 'read' if mode == 'r' else 'write'
print(scale([1.0, 2.5], 2))
print(first(['Aysel', 'Murad']), first([], default='nobody'))
print(apply(lambda n: n * n, 7))
print(describe('w'))▸ Ожидаемый результат
[2.0, 5.0] Aysel nobody 49 write
Философия Python — утиная типизация (duck typing): если у объекта есть нужный метод, он подходит. typing.Protocol переносит эту идею в систему типов. Протокол описывает, что объект умеет делать; любой класс с нужными методами автоматически ему соответствует, не наследуясь от него. Это называется структурной типизацией:
from typing import Protocol
class HasArea(Protocol):
def area(self) -> float: ...
class Square:
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side ** 2
class Circle:
def __init__(self, r: float) -> None:
self.r = r
def area(self) -> float:
return 3.14159 * self.r ** 2
def total_area(shapes: list[HasArea]) -> float:
return sum(s.area() for s in shapes)
print(round(total_area([Square(2), Circle(1)]), 2))▸ Ожидаемый результат
7.14
Square и Circle ничего не знают о HasArea, но анализатор типов принимает оба класса в list[HasArea], потому что у каждого есть метод area() -> float. Класс, в котором забыли area, будет отмечен как ошибка ещё до запуска программы.
dataclasses: классы без шаблонного кода
Классу, который в основном хранит данные, нужны __init__, копирующий аргументы в атрибуты, понятный __repr__ и __eq__, сравнивающий поле за полем. Писать их вручную долго, и легко ошибиться. Декоратор **@dataclass читает аннотированные атрибуты класса (поля**) и генерирует эти методы за тебя:
from dataclasses import dataclass, field
@dataclass
class Student:
name: str
grade: int
scores: list[int] = field(default_factory=list)
def average(self) -> float:
return sum(self.scores) / len(self.scores) if self.scores else 0.0
a = Student('Aysel', 9, [5, 4, 5])
b = Student('Aysel', 9, [5, 4, 5])
print(a)
print(a == b, a is b)
print(round(a.average(), 2))
print(Student('Murad', 8))▸ Ожидаемый результат
Student(name='Aysel', grade=9, scores=[5, 4, 5]) True False 4.67 Student(name='Murad', grade=8, scores=[])
a == b равно True, потому что сгенерированный __eq__ сравнивает поля, а a is b — False: это два разных объекта. Поле scores у Мурада получило новый пустой список благодаря field(default_factory=list): фабрика вызывается отдельно для каждого нового объекта.
from dataclasses import dataclass
try:
@dataclass
class Basket:
items: list[str] = []
except ValueError as e:
print(e)▸ Ожидаемый результат
mutable default <class 'list'> for field items is not allowed: use default_factory
frozen, order, slots и __post_init__
Параметры декоратора включают дополнительные возможности. frozen=True делает объекты неизменяемыми (а значит, хешируемыми: они могут быть элементами множеств и ключами словарей), order=True генерирует <, <=, >, >=, которые сравнивают поля по порядку, как кортежи, а slots=True (Python 3.10+) хранит атрибуты не в словаре, а в __slots__ — это экономит память и ускоряет доступ. Метод __post_init__ выполняется сразу после сгенерированного __init__ — это место для проверок:
from dataclasses import dataclass, replace, asdict
@dataclass(frozen=True, order=True, slots=True)
class Version:
major: int
minor: int = 0
patch: int = 0
def __post_init__(self):
if self.major < 0:
raise ValueError('major must be >= 0')
v1 = Version(0, 1)
v2 = replace(v1, minor=2)
print(v1 < v2)
print(sorted([Version(1), v2, v1]))
print(asdict(v2))
print({v1, Version(0, 1, 0)})
try:
v1.major = 1
except AttributeError as e:
print(type(e).__name__)▸ Ожидаемый результат
True
[Version(major=0, minor=1, patch=0), Version(major=0, minor=2, patch=0), Version(major=1, minor=0, patch=0)]
{'major': 0, 'minor': 2, 'patch': 0}
{Version(major=0, minor=1, patch=0)}
FrozenInstanceErrorРазберём вывод. sorted работает благодаря order=True: версии сравниваются как кортежи (major, minor, patch). В множестве остался один элемент, потому что у равных неизменяемых объектов и хеши равны. Попытка присваивания вызвала FrozenInstanceError — подкласс AttributeError: после создания объект изменить нельзя.
| Параметр | Что даёт |
|---|---|
frozen=True | неизменяемые объекты; присваивание вызывает FrozenInstanceError; хеширование |
order=True | сравнение по полям < <= > >=, объекты можно сортировать |
slots=True | __slots__: меньше памяти, быстрый доступ, новые атрибуты добавить нельзя |
kw_only=True | все поля передаются только по имени |
field(default_factory=...) | новое значение по умолчанию для каждого объекта |
field(repr=False, compare=False) | исключает поле из repr или сравнений |
Допиши dataclass Cart: добавь поле items — по умолчанию пустой словарь (у каждой корзины свой), пусть add сохраняет товар с его ценой, а total возвращает сумму цен, округлённую до 2 знаков.
from dataclasses import dataclass, field
@dataclass
class Cart:
owner: str
# add a field `items`: dict[str, float], an empty dict by default
def add(self, name: str, price: float) -> None:
...
def total(self) -> float:
...
a = Cart('Aysel')
b = Cart('Murad')
a.add('tea', 3.5)
a.add('bread', 0.8)
print(a)
print(b)
print(a.total())▸ Ожидаемый результат
Cart(owner='Aysel', items={'tea': 3.5, 'bread': 0.8})
Cart(owner='Murad', items={})
4.3Бегунов нужно отсортировать по времени (самый быстрый первым), а при равном времени — по имени. Не используя аргумент key, измени класс так, чтобы sorted() сортировал именно так.
from dataclasses import dataclass
@dataclass(order=True, frozen=True)
class Runner:
name: str
time: float
results = [Runner('Murad', 12.4), Runner('Leyla', 11.9), Runner('Aysel', 12.4)]
for r in sorted(results):
print(r.name, r.time)▸ Ожидаемый результат
Leyla 11.9 Aysel 12.4 Murad 12.4
Главное
- Аннотации (
x: int,-> str,list[int],int | None) документируют типы; Python не проверяет их во время выполнения. - mypy или pyright проверяют аннотации статически, до запуска программы.
- Обобщённые типы (
def first[T]),Callable,LiteralиProtocolописывают сложные контракты;Protocol— это структурная типизация. @dataclassгенерирует__init__,__repr__и__eq__по аннотированным полям; изменяемым значениям по умолчанию нуженfield(default_factory=...).frozen,order,slotsи__post_init__добавляют неизменяемость, упорядочивание, экономию памяти и проверки.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
x: int?