Как стать автором
Обновить

Приватные методы без нижнего подчеркивания и интерфейсы в Python

Open source *Python *Программирование *API *GitHub


Привет, Habr. Недавно угорел по дизайну — модификаторам доступа и интерфейсам, потом перенес это на язык программирования Python. Прошу под кат — делюсь результатами и как это работает. Для заинтересовавшихся в конце статьи есть ссылка на проект на Github.

Модификаторы доступа


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

private (приватные) методы доступны только внутри класса, protected (защищенные) — внутри класса и в дочерних классах.

Как реализованы приватные и защищенные методы в Python


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

class Car:

    def _start_engine(self):
        return "Engine's sound."

    def run(self):
        return self._start_engine()


if __name__ == '__main__':
    car = Car()

    assert "Engine's sound." == car.run()
    assert "Engine's sound." == car._start_engine()

Можно определить следующие минусы:

  • Если бы метод _start_engine обновлял какие-то переменные класса или сохранял состояние, а не просто возвращал «тупой расчет», вы могли что-то поломать для будущей работы с классом. Вы не позволяете себе что-то чинить в моторе вашей машины, потому что тогда никуда не поедете, верно?
  • Вытекающий пункт из предыдущего — чтобы убедиться, что можно «безопасно» (вызов метода не навредит самому классу) использовать защищенный метод — нужно заглянуть в его код и потратить время.
  • Авторы библиотек рассчитывают, что никто не пользуется защищенными и приватными методами классов, которые вы используете в своих проектах. Поэтому могут в любой релиз изменить его реализацию (которая на публичные методы не повлияет из-за обратной совместимости, но вы — пострадаете).
  • Автор класса, ваш коллега, рассчитывает, что вы не увеличите технический долг проекта, использовав защищенный или приватный метод вне созданного им класса. Ведь тому, кто будет его (приватный метод класса) рефакторить или изменять, придется убедиться (например, через тесты), что его изменения не поломают ваш код. А если поломают — ему нужно будет тратить время на то, чтобы решить эту проблему (костылем, потому что надо на вчера).
  • Возможно, вы следите за тем, чтобы другие программисты не использовали защищенные или приватные методам на code review и «бьете за это по рукам», значит — тратите время.

Как реализовать защищенные методы с помощью библиотеки


from accessify import protected


class Car:

    @protected
    def start_engine(self):
        return "Engine's sound."

    def run(self):
        return self.start_engine()


if __name__ == '__main__':
    car = Car()

    assert "Engine's sound." == car.run()

    car.start_engine()

Попытавшись вызвать метод start_engine за пределами класса, вы получите следующую ошибку (метод недоступен согласно политике доступа):

Traceback (most recent call last):
  File "examples/access/private.py", line 24, in <module>
    car.start_engine()
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/main.py", line 92, in private_wrapper
    class_name=instance_class.__name__, method_name=method.__name__,
accessify.errors.InaccessibleDueToItsProtectionLevelException: Car.start_engine() is inaccessible due to its protection level

Используя библиотеку:

  • Вам не надо использовать некрасивое (субъективно) нижнее или двойное нижнее подчеркивание.
  • Получаете красивый (субъективно) метод внедрения модификаторов доступа в код — декораторы private и protected.
  • Перекладываете ответственность с человека на интерпретатор.

Как это работает:

  1. Декоратор private или protected — самый «высоко» расположенный декоратор, срабатывает до метода класса, которому объявили приватный или защищенный модификатор доступа.


  2. В декораторе с помощью встроенной библиотеки inspect достается текущий объект из стека вызовов — inspect.currentframe(). У этого объекта есть следующие полезные нам атрибуты: пространство имен (locals) и ссылка на предыдущий объект из стека вызова (объект, который вызывает метод с модификатором доступа).


    (Очень упрощенная иллюстрация)
  3. inspect.currentframe().f_back — используем этот атрибут, чтобы проверить, находится ли предыдущий объект из стека вызова в теле класса или нет. Для этого смотрим на пространство имен — f_locals. Если атрибут self в пространстве имен есть — метод вызывается внутри класса, если нет — вне класса. Если вызывать метод с приватным или защищенным модификатором доступа вне класса — будет ошибка политики доступа.

Интерфейсы


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

Пример


Имеем класс User, который использует объект storage, чтобы создать нового пользователя.

class User:

    def __init__(self, storage):
        self.storage = storage

    def create(self, name):
        return storage.create_with_name(name=name)

Сохранять пользователя можно в базу данных, используя DatabaseStorage.create_with_name.

class DatabaseStorage:

    def create_with_name(self, name):
        ...

Сохранять пользователя можно в файлы, используя FileStorage.create_with_name.

class FileStorage:

    def create_with_name(self, name):
        ...

За счет того, что сигнатуры методов create_with_name (название, аргументы) у классов одинаковые — классу User не стоит волноваться какой объект ему подставили, если у обоих одинаковые методы. Это может быть достигнуто если классы FileStorage и DatabaseStorage реализуют одинаковый интерфейс (то есть связаны контрактом определить какой-то метод с логикой внутри).

if __name__ == '__main__':

    if settings.config.storage = FileStorage:
        storage = FileStorage()

    if settings.config.storage = DatabaseStorage:
        storage = DatabaseStorage()

    user = User(storage=storage)
    user.create_with_name(name=...)

Как работать с интерфейсами с помощью библиотеки


Если класс имплементирует интерфейс, класс должен содержать все методы интерфейса. В примере ниже интерфейс «HumanInterface» содержит метод «eat», а класс «Human» его имплементирует, но не реализовывает метод «eat».

from accessify import implements


class HumanInterface:

    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        pass

Скрипт завершит работу со следующей ошибкой:

Traceback (most recent call last):
  File "examples/interfaces/single.py", line 18, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 66, in decorator
    interface_method_arguments=interface_method.arguments_as_string,
accessify.errors.InterfaceMemberHasNotBeenImplementedException: class Human does not implement interface member HumanInterface.eat(food, args, allergy, kwargs)

Если класс имплементирует интерфейс, класс должен содержать все методы интерфейса, включая все входящие аргументы. В примере ниже интерфейс «HumanInterface» содержит метод «eat», который на вход принимает 4 аргумента, а класс «Human» его имплементирует, но реализовывает метод «eat» только с 1 аргументом.

from accessify import implements


class HumanInterface:

    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        @staticmethod
        def eat(food):
            pass

Скрипт завершит работу со следующей ошибкой:

Traceback (most recent call last):
  File "examples/interfaces/single_arguments.py", line 16, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 87, in decorator
    interface_method_arguments=interface_method.arguments_as_string,
accessify.errors.InterfaceMemberHasNotBeenImplementedWithMismatchedArgumentsException: class Human implements interface member HumanInterface.eat(food, args, allergy, kwargs) with mismatched arguments

Если класс имплементирует интерфейс, класс должен содержать все методы интерфейса, включая входящие аргументы и модификаторы доступа. В примере ниже интерфейс «HumanInterface» содержит приватный метод «eat», а класс «Human» его имплементирует, но не реализовывает приватный модификатор доступа к методу «eat».

from accessify import implements, private


class HumanInterface:

    @private
    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        @staticmethod
        def eat(food, *args, allergy=None, **kwargs):
            pass

Скрипт завершит работу со следующей ошибкой:

Traceback (most recent call last):
  File "examples/interfaces/single_access.py", line 18, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 77, in decorator
    interface_method_name=interface_method.name,
accessify.errors.ImplementedInterfaceMemberHasIncorrectAccessModifierException: Human.eat(food, args, allergy, kwargs) mismatches HumanInterface.eat() member access modifier.

Класс может имплементировать несколько (количество неограниченно) интерфейсов. Если класс имплементирует несколько интерфейсов, класс должен содержать все методы всех интерфейсов, включая входящие аргументы и модификаторы доступа. В примере ниже класс «Human» реализовывает метод «eat» интерфейса «HumanBasicsInterface», но не реализовывает метод «love» интерфейса «HumanSoulInterface».

from accessify import implements


class HumanSoulInterface:

    def love(self, who, *args, **kwargs):
        pass


class HumanBasicsInterface:

    @staticmethod
    def eat(food, *args, allergy=None, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanSoulInterface, HumanBasicsInterface)
    class Human:

        def love(self, who, *args, **kwargs):
            pass

Скрипт завершит работу со следующей ошибкой:

Traceback (most recent call last):
  File "examples/interfaces/multiple.py", line 19, in <module>
    @implements(HumanSoulInterface, HumanBasicsInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 66, in decorator
    interface_method_arguments=interface_method.arguments_as_string,
accessify.errors.InterfaceMemberHasNotBeenImplementedException: class Human does not implement interface member HumanBasicsInterface.eat(food, args, allergy, kwargs)

Киллер фича — метод интерфейса может «заявить» какие ошибки должен «бросить» метод класса, который его имплементирует. В примере ниже «заявлено», что метод «love» интерфейса «HumanInterface» должен выбрасывать исключение «HumanDoesNotExistError» и
«HumanAlreadyInLoveError», но метод «love» класса «Human» не «бросает» одно из них.

from accessify import implements, throws


class HumanDoesNotExistError(Exception):
    pass


class HumanAlreadyInLoveError(Exception):
    pass


class HumanInterface:

    @throws(HumanDoesNotExistError, HumanAlreadyInLoveError)
    def love(self, who, *args, **kwargs):
        pass


if __name__ == '__main__':

    @implements(HumanInterface)
    class Human:

        def love(self, who, *args, **kwargs):

            if who is None:
                raise HumanDoesNotExistError('Human whom need to love does not exist')


Скрипт завершит работу со следующей ошибкой:

Traceback (most recent call last):
  File "examples/interfaces/throws.py", line 21, in <module>
    @implements(HumanInterface)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 103, in decorator
    class_method_arguments=class_member.arguments_as_string,
accessify.errors.DeclaredInterfaceExceptionHasNotBeenImplementedException: Declared exception HumanAlreadyInLoveError by HumanInterface.love() member has not been implemented by Human.love(self, who, args, kwargs)

Подводя итоги, с помощью библиотеки:

  • Можно имплементировать один или несколько интерфейсов.
  • Интерфейсы комбинируются с модификаторами доступа.
  • Вы получите разделение интерфейсов и абстрактных классов (модуль abc в Python), теперь не надо использовать абстрактные классы как интерфейсы, если вы это делали (я делал).
  • К сравнению с абстрактными классами. Если вы не определили все аргументы метода из интерфейса — получите ошибку, используя абстрактный класс — нет.
  • К сравнению с абстрактными классами. Используя интерфейсы, вы получите ошибку во время создания класса (когда вы написали класс и вызвали файл *.py). В абстрактных классах вы получите ошибку уже на этапе вызова метода объекта класса.

Как это работает:

  1. В декораторе implements с помощью встроенной библиотеки inspect достаются все методы класса и его интерфейсов — inspect.getmembers(). За уникальный индекс метода принимается комбинация его имени и типа (staticmethod, property, и так далее).
  2. А с помощью inspect.signature() — аргументы метода.
  3. Проходим в цикле по всем методам интерфейса, и смотрим: есть ли такой метод (по уникальному индексу) в классе, который реализовывает интерфейс, одинаковые ли входящие аргументы, одинаковые ли модификаторы доступа, реализовывает ли метод объявленные ошибки в методе интерфейса.

Спасибо за внимание к статье. Ссылка на проект на Github.
Теги:
Хабы:
Всего голосов 33: ↑26 и ↓7 +19
Просмотры 38K
Комментарии Комментарии 73

Работа

Data Scientist
128 вакансий
Python разработчик
223 вакансии