Инкапсуляция – это упаковка данных в единый компонент. Сокрытие – это механизм, позволяющий ограничить доступ к данным вне какой-то области. Это немного разные принципы, хотя обычно о них говорят как об одном: мы что-то упаковали в класс, а потом сокрыли от нежелательного доступа из-вне. Можете козырнуть где-нибудь на собеседовании.
Скрывать от доступа из-вне имеет смысл, когда в вашем классе есть данные только для внутреннего использования, и вам нужно защитить от их изменения (будь-то случайного или преднамеренного) из других участков кода.
В Python мы вроде бы доверяем друг другу, и все члены класса по-умолчанию доступны из-вне. Остается лишь соглашение о том, что кто-то их не будет трогать без необходимости.
class PrivateClass: def __init__(self): self.secret_private_data = 5 c = PrivateClass() c.secret_private_data = 10 print(c.secret_private_data)
Никаких вам private/protected.
Ладно, разработчики договорились, что если добавляем подчеркивание перед именем переменной или функции, то она становится как-бы приватной, только для внутренного использования классом. «Как-бы» потому что вы запросто по-прежнему можете менять ее.
class PrivateClass: def __init__(self): self._secret_private_data = 5 c = PrivateClass() c._secret_private_data = 10 print(c._secret_private_data)
Максимум, что вам грозит – недовольство со стороны вашей IDE и коллег на code-review.Давайте добавим два знака подчеркивания перед именем. Будем более убедительными. Может, это немного контр-интуитивно, но этот трюк сработает. Такой атрибут останется видим внутри определения класса, но как-бы «пропадет» из видимости снаружи класса:
class PrivateClass: def __init__(self): self.__secret_private_data = 5 def how_are_you(self): assert self.__secret_private_data == 5 print('OK => ', self.__secret_private_data) c = PrivateClass() c.__secret_private_data = 10 print(c.__secret_private_data) c.how_are_you()
Попытались обмануть, даже что-то там присвоили, но не нанесли урона классу! Но я знаю колдунство посильнее:
class PrivateClass: def __init__(self): self.__secret_private_data = 5 def how_are_you(self): assert self.__secret_private_data == 5 print('OK => ', self.__secret_private_data) c = PrivateClass() c._PrivateClass__secret_private_data = 20 print(c._PrivateClass__secret_private_data) c.how_are_you()
Python не смог окончательно спрятать атрибут.
Работает это так: если имя атрибута начинается с двух подчеркиваний, то Python автоматически переименует его по шаблону _ИмяКласса__ИмяАтрибута. Но! Внутри класса он будет доступен по старому имени: __ИмяАтрибута.
Эта смешная игра в прятки в действительности имеет смысл. От злонамеренного изменения мы не сможем защититься таким образом, а от случайного – да. Вообразим ситуацию с наследованием:
class Base: def __init__(self): self.__x = 5 class Derived(Base): def __init__(self): super().__init__() self.__x = 10 # не поменяет __x из класса Base
В производном классе мы можем и не знать, что в нашем базовом есть переменная с таким же именем, но при совпадении – мы не нарушим работы базового класса, так как внутри это будут переменные с именами _Base__x и _Derived__x.
Тот же эффект будет и с методами, и с переменными на уровне класса (а не экземпляра).
class Foo: __hidden_var = 1 def __hidden_method(self, x): print(x) def __init__(self): self.__hidden_method(self.__hidden_var)
__setattr__
В одном из примеров мы умудрились установить классу атрибут, которого изначально не было в нем. Если такое поведение нежелательно, то можно определить магический метод __setattr__, который будет проверять, какой атрибут мы хотим установить и кидать исключение, если кто-то хочет записать в класс то, что мы не хотим у себя видеть.
class Foo: def __init__(self): self.field1 = 0 def __setattr__(self, attr, value): if attr == 'field1': # можно писать только в field1 self.__dict__[attr] = value else: raise AttributeError f = Foo() f.field1 = 10 # это можно f.field2 = 20 # это нельзя
Но сразу скажу, обычно такую защиту никто не делает. Давайте без фанатизма…
Ограничение экспорта из модулей
Еще один механизм сокрытия – сокрытие сущностей (классов, функций и т. п.) на уровне модулей от бездумного импорта.
Напишем модуль. То есть создаем папку с именем testpack, туда кладем файл __init__.py
def foo(): print('foo') def bar(): print('bar') __all__ = ['foo']
Далее в другом файле (из основной директории) мы попытаемся импортировать (все – *) из модуля testpack.
from testpack import * foo() bar() # будет ошибка! NameError: name 'bar' is not defined
Имя foo было импортировано, так как мы упомянули его в переменной __all__, а bar – нет. Эта все та же защита от дурака, потому что если мы намеренно импортируем bar – ни ошибок, ни предупреждений не последует.
from testpack import foo, bar foo() bar()
Еще одно замечание: если имя переменной (класса, функции) начинается с подчеркивания, то оно так же не будет импортировано при import *, даже если мы не применяем __all__. И как всегда, вы можете импортировать его вручную.
Специально для канала PyWay.