Как в питоне реализованы public private static методы
Перейти к содержимому

Как в питоне реализованы public private static методы

  • автор:

Режимы доступа public, private, protected. Сеттеры и геттеры

На прошлых занятиях мы научились с вами создавать экземпляры классов и объявлять в них атрибуты и методы. Пришла пора сделать следующий шаг и познакомиться с механизмом ограничения доступа к данным и методам класса извне. Это основа механизма инкапсуляции.

Давайте предположим, что мы описываем класс представления точки на плоскости:

class Point: def __init__(self, x=0, y=0): self.x = x self.y = y

Когда создается экземпляр этого класса:

pt = Point(1, 2)

то имеется полный доступ ко всем его локальным атрибутам:

print(pt.x, pt.y)

а, значит, их всегда можно изменить через ссылку pt:

pt.x = 200 pt.y = "coord_y"

и присвоить любые значения, в том числе и недопустимые (например, строку).

  • attribute (без одного или двух подчеркиваний вначале) – публичное свойство (public);
  • _attribute (с одним подчеркиванием) – режим доступа protected (служит для обращения внутри класса и во всех его дочерних классах)
  • __attribute (с двумя подчеркиваниями) – режим доступа private (служит для обращения только внутри класса).
class Point: def __init__(self, x=0, y=0): self._x = x self._y = y

Так реализуется режим protected в Python. Если кто из вас программирует на других языках, например, С++ или Java, то сейчас ожидают, что мы не сможем обращаться к свойствам _x и _y через ссылку pt, так как они определены как защищенные (protected). Давайте проверим и попробуем вывести их в консоль:

print(pt._x, pt._y)

Как видим, никаких ошибок не возникает и все работает так, словно это публичные свойства экземпляра класса. Но тогда зачем нам писать это нижнее подчеркивание, если оно не играет никакой роли? Одна роль у этого префикса все-таки есть: нижнее подчеркивание должно предостерегать программиста от использования этого свойства вне класса. Впоследствии это может стать причиной непредвиденных ошибок. Например, изменится версия класса и такое свойство может перестать существовать, т.к. никто не предполагал доступа к нему извне. Так что, к таким атрибутам лучше не обращаться напрямую – одно нижнее подчеркивание указывает нам, что это внутренние, служебные переменные. Давайте теперь посмотрим, как работает режим доступа private. Пропишем у локальных свойств два подчеркивания:

class Point: def __init__(self, x=0, y=0): self.__x = x self.__y = y

и также попробуем обратиться к ним напрямую:

print(pt.__x, pt.__y)

После запуска программы видим ошибку, что такие свойства не определены. Это говорит о том, что извне, через переменную pt мы не можем напрямую к ним обращаться. А вот внутри класса доступ к ним открыт. Пропишем метод set_coord, который будет менять локальные свойства __x и __y экземпляра класса:

def set_coord(self, x, y): self.__x = x self.__y = y

А ниже, вызовем его для экземпляра pt:

pt.set_coord(1, 2)

Как видите, никаких ошибок не возникает и чтобы убедиться в изменении локальных приватных свойств, определим еще один метод:

def get_сoord(self): return self.__x, self.__y

И вызовем его:

print(pt.get_сoord())

После запуска программы видим измененные координаты точки. В результате, мы с вами определили два вспомогательных метода: set_coord и get_coord, через которые предполагается работа с защищенными данными класса. Такие методы в ООП называются сеттерами и геттерами или еще интерфейсными методами. Зачем понадобилось в классах создавать приватные атрибуты да еще и определять дополнительно методы для работы с ними извне. Я об этом уже говорил на самом первом занятии по ООП, когда объяснял принцип инкапсуляции. Но, скажу еще раз. Класс в ООП следует воспринимать как некое единое целое, и чтобы случайно или намеренно не нарушить целостность работы алгоритма внутри этого класса, то следует взаимодействовать с ним только через публичные свойства и методы. В этом суть принципа инкапсуляции. Опять же, представьте автомобиль, в котором согласованно работают тысячи узлов. А управление им предполагается только через разрешенные интерфейсы: руль, коробка передач, педали газа и тормоза и т.п. Если во время движения вмешиваться напрямую в его узлы, например, будем на ходу спускать воздух из шин, то, наверное, ничего хорошего не получится. То же самое, можно сказать и о программисте, который намеренно обходит запрет и обращается к скрытым атрибутам класса напрямую, а не через сеттеры или геттеры. Так делать не нужно. Назначение интерфейсных методов не только передавать значения между приватными атрибутами класса, но и проверять их корректность. Например, в нашем случае координаты должны быть числами. Поэтому, прежде чем обновлять значения переменных, следует проверить их тип данных. Для этого можно воспользоваться функцией type и записать сеттер следующим образом:

def set_coord(self, x, y): if type(x) in (int, float) and type(y) in (int, float): self.__x = x self.__y = y else: raise ValueError("Координаты должны быть числами")

Здесь мы проверяем, что обе переданные переменные x и y должны иметь тип int или float и только после этого приватным атрибутам экземпляра класса присваиваются новые значения. Иначе, генерируется исключение ValueError. Об исключениях мы с вами еще будем говорить. Теперь, если передавать недопустимые значения координат:

pt.set_coord('1', 2)

то увидим ошибку ValueError. Продолжим совершенствовать наш класс Point и добавим приватный метод для проверки корректности координат. Приватный метод объявляется также как и приватная переменная – двумя подчеркиваниями и, кроме того, сделаем его методом уровня класса (о декораторе classmethod мы с вами говорили на предыдущем занятии):

@classmethod def __check_value (cls, x): return type(x) in (int, float)

Соответственно, в сеттере и в инициализаторе воспользуемся этим методом:

def __init__(self, x=0, y=0): self.__x = self.__y = 0 if self.__check_value (x) and self.__check_value (y): self.__x = x self.__y = y def set_coord(self, x, y): if self.__check_value (x) and self.__check_value (y): self.__x = x self.__y = y else: raise ValueError("Координаты должны быть числами")

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

print(dir(pt))

то среди прочих мы увидим, следующие: ‘_Point__x’, ‘_Point__y’ Это и есть кодовые имена приватных атрибутов, к которым мы можем обратиться через ссылку pt:

print(pt._Point__x, pt._Point__y)

и менять их. Однако, так делать крайне не рекомендуется и двойное подчеркивание должно сигнализировать программисту, что работать с такими атрибутами нужно только через разрешенные интерфейсные методы. Иначе, возможны непредвиденные ошибки. Если у вас появилась необходимость лучше защитить методы класса от доступа извне, то это можно сделать с помощью модуля accessify. Для его установки нужно выполнить команду: pip install accessify И, затем, импортировать из него два декоратора:

from accessify import private, protected

Далее, нужный декоратор просто применяем к методу и он становится либо приватным (private), либо защищенным (protected):

@private @classmethod def check_value(cls, x): return type(x) in (int, float)

Все, теперь мы можем обратиться к check_value только внутри класса, но не извне:

pt.check_value(5)

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

Привет, 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 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.
  • Перекладываете ответственность с человека на интерпретатор.

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

Интерфейсы

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

Пример

Имеем класс 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 @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 @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 @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 @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 @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. Проходим в цикле по всем методам интерфейса, и смотрим: есть ли такой метод (по уникальному индексу) в классе, который реализовывает интерфейс, одинаковые ли входящие аргументы, одинаковые ли модификаторы доступа, реализовывает ли метод объявленные ошибки в методе интерфейса.

Приватность методов в Python

Считается, что в языке программирования Python есть возможность создавать «приватные» методы и переменные внутри класса, добавляя к их названию двойные подчеркивания, например, __myPrivateMethod() . Однако, в действительности, эти «приватные» методы и переменные не являются полностью приватными в традиционном понимании этого термина.

В качестве примера возьмем следующий код:

class MyClass: def myPublicMethod(self): print('public method') def __myPrivateMethod(self): print('this is private!!') obj = MyClass() obj.myPublicMethod()

Здесь мы создали класс с публичным методом myPublicMethod() и приватным методом __myPrivateMethod() . При попытке вызвать приватный метод, мы столкнемся с ошибкой AttributeError , которая говорит о том, что у экземпляра класса MyClass нет атрибута __myPrivateMethod .

obj.__myPrivateMethod()

На первый взгляд, все выглядит ожидаемо — мы не можем вызвать приватный метод. Однако, при использовании функции dir() на объекте, мы обнаружим новый «магический» метод, который Python создает автоматически для всех ваших «приватных» методов.

dir(obj)

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

obj._MyClass__myPrivateMethod()

Таким образом, «приватность» методов в Python является относительной. Несмотря на то, что Python предоставляет возможность в некоторой степени инкапсулировать методы и переменные, полностью сделать их приватными не удается. На самом деле, Python использует механизм под названием «name mangling» для изменения имени «приватного» метода, чтобы предотвратить его случайное вызывание.

Этот механизм не предназначен для создания секретности или безопасности, а служит для предотвращения ошибок при работе с методами и переменными, которые должны быть использованы только внутри класса.

Статический метод Python

Статический метод в Python — это метод, который связан с классом, а не с его экземплярами. Чтобы быть вызванным, он не требует создания экземпляра класса и не имеет доступа к экземпляру. Статические методы в Python объявляются с использованием декоратора @staticmethod . Этот декоратор указывает интерпретатору Python, что метод является статическим, и следует вызывать его через класс, а не через экземпляр класса.

Таким образом, статические методы в Python могут использоваться для выполнения операций, не требующих доступа к экземпляру класса или его атрибутам, то есть, по сути, они являются вспомогательными функциями. Чтобы было понятнее, о чём речь, перейдем сразу к практике.

Пример №1

Представим, что у нас есть класс MathUtils , который содержит статический метод для вычисления факториала:

class MathUtils: 
@staticmethod
def factorial(n):
if n == 0:
return 1
else:
return n * MathUtils.factorial(n-1)

Давайте введем далее:

print(MathUtils.factorial(5))
120

Получили факториал 5, то есть 120. Здесь статический метод factorial не использует никакие атрибуты экземпляра класса, а только входной аргумент n . И мы вызвали его, используя синтаксис MathUtils.factorial(n) и не создавая экземпляр класса MathUtils .

Далее отметим, что статические методы в Python могут использоваться не только в классах, но также в модулях и пакетах. Если вы определяете функцию в модуле и не хотите, чтобы она была связана с экземпляром класса или модулем, вы можете использовать декоратор @staticmethod для объявления этой функции как статической.

Пример №2

Допустим, у нас есть модуль StringUtils , содержащий статический метод для проверки, является ли строка палиндромом. Пишем такой код:

def is_palindrome(string): 
return string == string[::-1]

Здесь функция is_palindrome не связана с каким-либо экземпляром класса или объектом модуля, поэтому мы можем использовать декоратор @staticmethod для объявления ее как статической. Для этого расширим наш код таким образом:

class StringUtils: 
@staticmethod
def is_palindrome(string):
return string == string[::-1]

Далее введем для проверки:

print(StringUtils.is_palindrome("топот"))
True
print(StringUtils.is_palindrome("топор"))
False

Всё верно, первое слово является палиндромом, поэтому интерпретатор вывел True , а второе нет, и мы получили False .

Таким образом, мы можем вызвать метод is_palindrome через класс StringUtils , используя синтаксис StringUtils.is_palindrome(string) вместо того, чтобы импортировать функцию is_palindrome и вызывать ее напрямую.

Еще одно важное отличие статических методов от методов экземпляров класса в Python заключается в том, что статические методы не могут изменять состояние экземпляра. Это означает, что они не могут изменять значения атрибутов, что логично, так как они не имеют доступа к экземпляру. Если вы хотите изменить состояние экземпляра класса, вам нужно использовать методы экземпляров.

Пример №3

Рассмотрим еще один пример. Допустим, у нас есть класс Person , имеющий атрибут age и статический метод is_adult , проверяющий значение на соответствие возрасту совершеннолетия:

class Person: 
def __init__(self, age):
self.age = age

@staticmethod
def is_adult(age):
return age >= 18

Далее давайте создадим переменную age со значением 21, вызовем статический метод is_adult из класса Person с этим значением и сохраним его результат в переменную is_adult , вот так:

age = 21
is_adult = Person.is_adult(age)

Теперь для проверки давайте введем:

print(is_adult)
True

Поскольку возраст соответствует заданному в статическом методе условию, мы получили True . В примере выше статический метод is_adult принимает аргумент age , но не имеет доступа к атрибуту age экземпляра класса Person , выступая в качестве вспомогательной функции.

Подведем итоги

В завершение отметим, что статические методы улучшают читабельность кода и дают возможность повторно использовать его. А еще они удобнее, если сравнивать их со стандартными функциями Python. Удобство статических методов в том, что они не требуют отдельного импорта, как это нужно делать для функций. Таким образом, использование статических методов класса Python может существенно упростить код и работу с ним. И, как вы наверняка убедились на примерах выше, освоить их довольно просто.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *