Абстрактные классы и интерфейсы в Питоне

    Абстрактные базовые классы и интерфейсы — близкие по назначению и смыслу сущности. Как первые, так и вторые представляют собой своеобразный способ документирования кода и помогают ограничить (decouple) взаимодействие отдельных абстракций в программе (классов).

    Питон — очень гибкий язык. Одна из граней этой гибкости — возможности, предоставляемые метапрограммированием. И хотя в ядре языка абстрактные классы и интерфейсы не представлены, первые были реализованы в стандартном модуле abc, вторые — в проекте Zope (модуль zope.interfaces).

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



    2 Абстрактные базовые классы (abс)



    Начиная с версии языка 2.6 в стандартную библиотеку включается модуль abc, добавляющий в язык абстрактные базовые классы (далее АБК).

    АБК позволяют определить класс, указав при этом, какие методы или свойства обязательно переопределить в классах-наследниках:

    from abc import ABCMeta, abstractmethod, abstractproperty
    class Movable():
        __metaclass__=ABCMeta
    
        @abstractmethod
        def move():
        """Переместить объект"""
        
        @abstractproperty
        def speed():
        """Скорость объекта"""
    


    Таким образом, если мы хотим использовать в коде объект, обладающий возможностью перемещения и определенной скоростью, то следует использовать класс Movable в качестве одного из базовых классов.

    Наличие необходимых методов и атрибутов объекта теперь гарантируется наличием АБК среди предков класса:

    class Car(Movable):
        def __init__:
            self.speed = 10
            self.x = 0
    
        def move(self):
            self.c += self.speed
            def speed(self):
            return self.speed
        
    assert issubclass(Car, Movable)
    assert ininstance(Car(), Movable)
    


    Видно, что понятие АБК хорошо вписывается в иерархию наследования классов, использовать их легко, а реализация, если заглянуть в исходный код модуля abc, очень проста. Абстрактные классы используются в стандартных модулях collections и number, задавая необходимые для определения методы пользовательских
    классов-наследников.

    Подробности и соображения по поводу использования АБК можно найти в PEP 3119
    (http://www.python.org/dev/peps/pep-3119/).

    3 Интерфейсы (zope.interfaces)



    Реализация проекта Zope в работе над Zope3 решила сделать акцент на компонентной архитектуре; фреймворк превратился в набор практически независимых компонент. Клей, соединяющий компоненты — интерфейсы и основывающиеся на них адаптеры.

    Модуль zope.interfaces — результат этой работы.

    В простейшем случае использвание интерфейсов напоминает примерение АБК:

    import zope.interface
    
    class IVehicle(zope.interface.Interface):
        """Any moving thing"""
        speed = zope.interface.Attribute("""Movement speed""")
        def move():
            """Make a single step"""
        
    class Car(object):
        zope.interface.implements(IVehicle)
    
        def __init__:
            self.speed = 1
            self.location = 1
    
        def move(self):
            self.location = self.speed*1
            print "moved!"
        
    assert IVehicle.implementedBy(Car)
    assert IVehicle.providedBy(Car())
    


    В интерфейсе декларативно показывается, какие атрибуты и методы должны быть у объекта. Причем класс реализует (implements) интерфейс, а объект класса — предоставляет (provides). Следует обратить внимание на разницу между этими понятиями!

    «Реализация» чем-либо интерфейса означает, что только «производимая» сущность будет обладать необходимыми свойствами; а «предоставление» интерфейса говорит о конкретных возможностях оцениваемой сущности. Соответственно, в Питоне классы, кстати, могут как реализовывать, так и предоставлять интерфейс.

    На самом деле декларация implement(IVehicle) — условность; просто обещание, что данный класс и его объекты ведут себя именно таким образом. Никаких реальных проверок проводиться не будет

    class IVehicle(zope.interface.Interface):
        """Any moving thing"""
        speed = zope.interface.Attribute("""Movement speed""")
    
        def move():
            """Make a single step"""
    
    class Car(object):
        zope.interface.implements(IVehicle)
    
    assert IVehicle.implementedBy(Car)
    assert IVehicle.providedBy(Car())
    


    Видно, что в простейших случаях интерфейсы только усложняют код, как, впрочем, и АБК

    Компонентная архитектура Zope включает еще одно важное понятие — адаптеры. Вообще говоря, это простой шаблон проектирования, корректирующий один класс для использования где-то, где требуется иной комплект методов и атрибутов. Итак,

    4 Адаптеры



    Рассмотрим, сильно упростив, пример из Comprehensive Guide to Zope Component Architecture.

    Предположим, что имеется пара классов, Guest и Desk. Определим интерфейсы к ним, плюс класс, реализующий интерфейс Guest:

    import zope.interface
    from zope.interface import implements
    
    from zope.component import adapts, getGlobalSiteManager
    
    class IDesk(zope.interface.Interface):
        def register():
            "Register a person"
    
    class IGuest(zope.interface.Interface):
        name = zope.interface.Attribute("""Person`s name""")
    
    class Guest(object):
        implements(IGuest)
    
        def __init__(self, name):
            self.name=name
    


    Адаптер должен учесть анонимного гостя, зарегистрировав в списке имен:

    class GuestToDeskAdapter(object):
        adapts(IGuest)
        implements(IDesk)
        
        def __init__(self, guest):
            self.guest=guest
        
        def register(self):
            guest_name_db.append(self.guest.name)
    


    Существует реестр, который ведет учет адаптеров по интерфейсам. Благодаря ему можно получить адаптер, передав в вызов класса-интерфейса адаптируемый объект. Если адаптер не зарегистрирован, то вернется второй аргумент интерфейса:

    guest = Guest("Ivan")
    adapter = IDesk(guest, alternate=None)
    print adapter
    >>>>None found
    
    gsm = getGlobalSiteManager()
    gsm.registerAdapter(GuestToDeskAdapter)
    
    adapter = IDesk(guest, alternate="None found")
    print adapter
    
    >>>>__main__.GuestToDeskAdapter object at 0xb7beb64c>
    


    Такую инфраструктуру удобно использовать для разделения кода на компоненты и их связывания.

    Один из ярчайших примеров использования такого подхода помимо самого Zope — сетевой фреймворк Twisted, где изрядная часть архитектуры опирается на интерфейсы из zope.interfaces.

    5 Вывод



    При ближайшем рассмотрении оказывается, что интерфейсы и абстрактные базовые классы — разные вещи.

    Абстрактные классы в основном жестко задают обязательную интерфейсную часть. Проверка объекта на соответствие интерфейсу абстрактного класса проверяется при помощи встроенной функции isinstance; класса — issubclass. Абстрактный базовый класс должен включаться в иерархию в виде базового класса либо mixin`а.

    Минусом можно считать семантику проверок issubclass, isinstance, которые пересекаются с обычными классами (их иерархией наследования). На АБК не выстраивается никаких допонительных абстракций.

    Интерфейсы — сущность декларативная, они не ставят никаких рамок; просто утверждается, что класс реализует, а его объект предоставляет интерфейс. Семантически утверждения implementedBy, providedBy являются более корректными. На такой простой базе удобно выстраивать компонентную архитектуру при помощи адапетров и других производных сущностей, что и делают крупные фреймворки Zope и Twisted.

    Надо понимать, что использование обоих инструментов имеет смысл только при построении и использовании сравнительно крупных ООП-систем — фреймворков и библиотек, в малых программах они могут только запутать и усложнить код код лишними абстракциями.
    Метки:
    • +33
    • 39,7k
    • 9
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 9
    • 0
      Где-то видел такой подход:
      class interface:
          def query(self):
              pass
          def select(self):
              pass
      

      • 0
        class Derived(interface):
            pass
        
        derived_object = Derived()
        


        Будет работать, несмотря на то что потомок не реализовал методы query и select.
        Если бы в этом случае использовался, например, модуль ABC, то при попытке создать экземпляр класса Derived выбросилось бы исключение TypeError. Пример пользователя neithere тоже выбросит исключение, но только при попытке вызвать какой-либо из нереализованных методов
        • 0
          Не пробовал, но, думаю, аналогичный abc функционал можно сделать инвариантами в zope.interfaces
      • +6
        Или так:

        class Movable:    
            @property
            def speed(self):
                raise NotImplementedError
            def move(self):
                raise NotImplementedError
        

        Преимущество — используются только стандартные средства.
        Недостаток — ошибка вываливается при обращении к атрибуту, а не на этапе создания экземпляра.
        • 0
          Ну вы сами все сказали. Развитие этой идеи и приводит к интерфейсам либо велосипедо-интерфейсам.
        • 0
          А можно попросить подсветить?
          • 0
            Можно. Ночью лень уже стало заморачиваться :)
          • +3
            Не знаю как там в Zope. Но интерфейсы в нетипизированном языке — разве что дополнение к документации.
            • +5
              В общем-то да, такие штуки редко нужны. Однако, если система действительно большая и сложная, то без интерфейсов становится тяжеловато. Компонентную архитектуру ввели в Zope именно поэтому — упростить и разделить на явные стандартизированные составляющие.

              Что бы там не говорили про Яву в малых и десктопных приложениях, а рынок средних-крупных систем для бизнеса и финансов она держит плотно, возможно, именно благодаря таким фишкам.

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