Абстрактные классы, утиная типизация и Protocol

⏱ 35 мин · 🧩 заданий: 5

Утиная типизация

If it walks like a duck and it quacks like a duck, then it must be a duck.

Python не спрашивает, какого класса объект. Ему важно, умеет ли объект то, что от него требуют:

class Duck:
    def speak(self):
        return "Кря"

class Robot:                     # не связан с Duck никаким наследованием
    def speak(self):
        return "Бип-буп"

def make_noise(thing):
    return thing.speak()          # нужен только метод speak()

for t in [Duck(), Robot()]:
    print(make_noise(t))

Именно так работает весь Python: for принимает всё, у чего есть __iter__; len() — всё, у чего __len__; json.dump пишет в любой объект с методом write.

import io, json

class Collector:                 # «файлоподобный» объект: у него есть write()
    def __init__(self):
        self.parts = []
    def write(self, s):
        self.parts.append(s)

c = Collector()
json.dump({"a": [1, 2]}, c)
print(c.parts)

buf = io.StringIO()               # стандартный «файл в памяти»
print("привет", file=buf)
print(repr(buf.getvalue()))

EAFP против LBYL

data = {"name": "Анна"}

# LBYL — Look Before You Leap
if "age" in data:
    age = data["age"]
else:
    age = None

# EAFP — Easier to Ask Forgiveness than Permission (питоничный стиль)
try:
    age = data["age"]
except KeyError:
    age = None
print(age)

EAFP короче, не дублирует проверку и избегает гонок (файл могли удалить между проверкой exists() и открытием).

Абстрактные базовые классы

Утиная типизация гибкая, но ошибки «забыл реализовать метод» всплывают только при вызове. ABC позволяет объявить контракт явно:

from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: int) -> str:
        """Списать деньги и вернуть id транзакции."""

    @property
    @abstractmethod
    def name(self) -> str: ...

    def pay(self, amount):                    # обычный метод использует абстрактные
        tx = self.charge(amount)
        return f"{self.name}: {amount} ₽, транзакция {tx}"

class FakeGateway(PaymentGateway):
    name = "Тестовый шлюз"
    def charge(self, amount):
        return f"tx-{amount}"

print(FakeGateway().pay(500))

try:
    PaymentGateway()
except TypeError as e:
    print("TypeError:", e)

class Broken(PaymentGateway):
    name = "Сломанный"
try:
    Broken()
except TypeError as e:
    print("TypeError:", e)

Класс с хотя бы одним нереализованным @abstractmethod нельзя создать — ошибка возникает сразу при создании объекта, а не при вызове метода.

collections.abc

Модуль collections.abc описывает протоколы встроенных коллекций. Наследуясь от них, вы реализуете пару методов и получаете остальные бесплатно:

ABC Нужно реализовать Получаете даром
Iterable __iter__ —
Sized __len__ —
Container __contains__ —
Sequence __getitem__, __len__ __contains__, __iter__, __reversed__, index, count
Mapping __getitem__, __iter__, __len__ get, keys, items, values, __contains__, __eq__
MutableMapping + __setitem__, __delitem__ + pop, update, setdefault, clear
from collections.abc import MutableMapping

class LowerDict(MutableMapping):
    """Словарь, нечувствительный к регистру ключей."""
    def __init__(self, *args, **kwargs):
        self._data = {}
        self.update(*args, **kwargs)          # update() достался от MutableMapping
    def __getitem__(self, key):
        return self._data[key.lower()]
    def __setitem__(self, key, value):
        self._data[key.lower()] = value
    def __delitem__(self, key):
        del self._data[key.lower()]
    def __iter__(self):
        return iter(self._data)
    def __len__(self):
        return len(self._data)

headers = LowerDict({"Content-Type": "text/html"})
print(headers["content-type"], "CONTENT-TYPE" in headers, headers.get("X-Missing", "—"))
headers.setdefault("Accept", "*/*")
print(dict(headers.items()))

isinstance без наследования

Некоторые ABC распознают класс по наличию методов (__subclasshook__):

from collections.abc import Iterable, Sized, Hashable

class Bag:
    def __iter__(self):
        return iter([1, 2])

print(isinstance(Bag(), Iterable))      # True, хотя Bag не наследует Iterable
print(isinstance(Bag(), Sized))         # False — нет __len__
print(isinstance([], Hashable), isinstance((), Hashable))

Класс можно и явно «зарегистрировать» как виртуальный подкласс: Iterable.register(MyClass).

Protocol: структурная типизация

ABC требует наследования — это номинальная типизация. typing.Protocol описывает интерфейс по структуре: подходит любой класс с нужными методами. Это утиная типизация, которую понимают mypy и IDE:

from typing import Protocol, runtime_checkable

@runtime_checkable
class SupportsClose(Protocol):
    def close(self) -> None: ...

class DbConnection:                 # ничего не наследует
    def close(self):
        print("  соединение закрыто")

class Report:
    pass

def shutdown(resources: list[SupportsClose]) -> None:
    for r in resources:
        r.close()

shutdown([DbConnection()])
print(isinstance(DbConnection(), SupportsClose), isinstance(Report(), SupportsClose))

Без @runtime_checkable протокол работает только для статических проверок (mypy), а isinstance бросит TypeError. И isinstance проверяет лишь наличие методов, а не их сигнатуры.

Что выбрать

Ситуация Инструмент
Функции нужен «объект с методом X», классы ваши и чужие утиная типизация + Protocol для подсказок типов
Есть иерархия ваших классов, общий код в базовом классе, нужно запретить неполные реализации ABC + @abstractmethod
Свой контейнер, похожий на list/dict collections.abc.Sequence / MutableMapping
Проверить «можно ли итерировать» isinstance(x, Iterable) или просто EAFP

Вопрос на собеседовании

«Как в Python сделать интерфейс?» — В Python нет ключевого слова interface. Варианты: абстрактный класс abc.ABC с @abstractmethod (наследники обязаны реализовать методы, иначе объект не создать) или typing.Protocol для структурной типизации (класс подходит, если у него есть нужные методы, проверяет mypy). На практике часто хватает утиной типизации.

Итог

  • Утиная типизация: важны методы объекта, а не его класс. Питоничный стиль — EAFP.
  • ABC + @abstractmethod — явный контракт; неполную реализацию нельзя создать.
  • collections.abc дают готовые протоколы коллекций и методы-примеси.
  • Protocol — структурная типизация для mypy; @runtime_checkable — для isinstance.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Создайте абстрактный класс Shape(ABC) с абстрактными методами area() и perimeter() и обычным методом describe(), который возвращает строку "площадь=12.00, периметр=14.00" (числа с двумя знаками после точки). Затем реализуйте наследника Rect(w, h).

Какие тесты проверяют код (3)
assert Rect(3, 4).describe() == "площадь=12.00, периметр=14.00"
try:
    Shape()
    ok = False
except TypeError:
    ok = True
assert ok, "Shape должен быть абстрактным: Shape() → TypeError"

class Bad(Shape):
    def area(self):
        return 0
try:
    Bad()
    ok = False
except TypeError:
    ok = True
assert ok, "Наследник без perimeter() не должен создаваться"

Задание 2 Программа на Python

Реализуйте класс Range3 — неизменяемую последовательность из трёх чисел start, start+1, start+2, унаследовав его от collections.abc.Sequence. Достаточно определить __len__ и __getitem__ — методы index, count, __contains__, __reversed__ и __iter__ Sequence даст бесплатно.

Какие тесты проверяют код (4)
r = Range3(10); assert list(r) == [10, 11, 12]
r = Range3(10); assert 11 in r and r.index(12) == 2 and r.count(10) == 1
assert list(reversed(Range3(1))) == [3, 2, 1]
assert Range3(0)[-1] == 2
Задание 3 Один вариант ответа

Что такое утиная типизация?

Задание 4 Один вариант ответа

Чем typing.Protocol отличается от abc.ABC?

Задание 5 Один вариант ответа

Какой стиль считается питоничным: EAFP или LBYL?

Войдите, чтобы отмечать пройденные уроки.