Шаг 141.
Python: сборник рецептов.
Классы и объекты. Определение интерфейса или абстрактного базового класса

    На этом шаге мы рассмотрим особенности реализации такого интерфейса или абстрактного класса.

Задача

    Вы хотите определить класс, который будет служить интерфейсом, или абстрактный базовый класс, из которого вы сможете производить проверку типов и убеждаться, что некоторые методы реализованы в подклассах.

Решение

    Чтобы определить абстрактный базовый класс, воспользуйтесь модулем abc. Например:

from abc import ABCMeta, abstractmethod

class IStream(metaclass=ABCMeta):
    @abstractmethod
    def read(self, maxbytes=-1):
        pass

    @abstractmethod
    def write(self, data):
        pass

    Главная возможность абстрактного базового класса - в том, что он не может напрямую порождать экземпляры. Если вы попробуете это сделать, то получите ошибку:

a = IStream() # TypeError: Can't instantiate abstract class 
              # IStream with abstract methods read, write

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

class SocketStream(IStream): 
    def read(self, maxbytes=-1):
        .  .  .
    def write(self, data):
        .  .  .

    В основном абстрактные базовые классы используются в коде, где нужно принудительно реализовать ожидаемый программный интерфейс. Например, можно посмотреть на базовый класс IStream как на высокоуровневую спецификацию для интерфейса, который позволяет читать и записывать данные. Код, который явно проверяет наличие этого интерфейса, может быть написан так:

def serialize(obj, stream):
    if not isinstance(stream, IStream):
        raise TypeError('Expected an IStream')
    .  .  .

    Вы можете подумать, что такого рода проверка типов работает лишь путем создания подкласса абстрактного базового класса, но абстрактный базовый класс позволяет другим классам регистрироваться в качестве реализовывающих требуемый интерфейс. Например, вы можете сделать так:

import io
#  Регистрируем встроенные классы ввода-вывода
#  в качестве поддерживающих для нашего интерфейса 
IStream.register(io.IOBase)
#  Открыть обычный файл и провести проверку типа 
f = open('foo.txt')
isinstance(f, IStream) # Вернет True

    Нужно отметить, что @abstractmethod может быть также применен к статическим методам, методам класса и свойствам. Вам нужно просто убедиться, что вы применяете его в правильной последовательности. @abstractmethod нужно писать прямо перед определением функции, как показано тут:

from abc import ABCMeta, abstractmethod

class A(metaclass=ABCMeta):
    @property
    @abstractmethod
    def name(self):
        pass

    @name.setter
    @abstractmethod
    def name(self, value):
        pass

    @classmethod
    @abstractmethod
    def method1(cls):
       pass

    @staticmethod
    @abstractmethod
    def method2():
        pass


Обсуждение

    Предварительно определенные абстрактные базовые классы можно найти во многих местах стандартной библиотеки. Модуль collections определяет разнообразные абстрактные базовые классы, относящиеся к контейнерам и итераторам (последовательности, отображения, множества и т. п.), библиотека numbers определяет абстрактные базовые классы, связанные с числовыми объектами (целые числа, числа с плавающей точкой, дроби и т. п.), библиотека io - связанные с управлением вводом-выводом.

    Вы можете использовать предопределенные абстрактные базовые классы для выполнения более обобщенной проверки типов. Вот несколько примеров:

import collections

# Проверить, является ли x последовательностью
if isinstance(x, collections.Sequence):
    . . .

# Проверить, является ли x итерируемым объектом
if isinstance(x, collections.Iterable):
    . . .

# Проверить, есть ли у x размер
if isinstance(x, collections.Sized):
    . . .

# Проверить, является ли x отображением
if isinstance(x, collections.Mapping):
    . . .

    Стоит отметить, что некоторые библиотечные модули могут не использовать эти предопределенные классы так, как вы могли бы предположить. Например:

>>> from decimal import Decimal
>>> import numbers
>>> x = Decimal('3.4')
>>> isinstance(x, numbers.Real)
False
>>> 

    Хотя значение 3.4 технически является вещественным числом, результат проверки типов не подтверждает этого, чтобы помочь избежать случайного смешивания обычных чисел с плавающей точкой и десятичных дробей из модуля decimal. Поэтому если вы используете функциональность абстрактных базовых классов, стоит аккуратно писать тесты, которые проверяют, что поведение именно таково, какое вам требуется.

    Хотя абстрактные базовые классы облегчают проверку типов, это не стоит слишком часто использовать в программах. В своей основе Python является гибким динамическим языком. Попытки понавставлять повсюду принудительные ограничения типов ведут к более сложному коду, нежели необходимо. Вы должны принять гибкость Python.

    На следующем шаге мы рассмотрим реализации модели данных или системы типов.




Предыдущий шаг Содержание Следующий шаг