Сокрытие в Python

Руки прочь от деталей реализации!

cmd410 · June 2, 2021

Python, как многим наверное известно является объектно-ориентированным языком программирования. Всё в python это объекты, даже функции. Но несмотря на вездесущность объектов в этом языке, есть люди которые упражняются в ментальной гимнастике, отрицая объектно-ориентированность этого языка.

По их мнению, python, якобы не соблюдает некие правила, принятые у труЪ ООП языков. В частности, говорят что питон не труЪ ООП язык потому что в нем нет сокрытия (которое иногда почему то называют инкапсуляцией). Если вкратце, это когда у полей класса, могут быть разные модификаторы доступа как правило public и private - поля, доступные всем, и поля, доступные только владельцу этих самых полей.

В языках поддерживающих сокрытие это выглядит как то так:

class Capitalist {
    
    public:

        Capitalist(double starting_capital) {
            capital = starting_capital;
        }

        void set_capital(double value) {
            // Опционально: Какая-нибудь валидация здесь
            capital = value;
        }

        double get_capital(void) {
            // Опционально: Какие-нибудь трансформации тут
            return capital;
        }
    
    private:
        double capital;
}

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

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

Как же дела с этим обстоят в python?

Что-ж в мире питона есть определенная конвенция, как отделять детали реализации от интерфейса через именование переменных начиная с нижнего подчеркивания _, так:

  • variable = 42 – публичное поле
  • _variable = 42 – “защищенное” поле
  • __variable = 42 – “приватное” поле

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

class Capitalist:
    __capital: float

    def __init__(self, starting_capital: float):
        self.__capital = starting_capital
    
    def get_capital(self) -> float:
        # Опционально: Какая-нибудь валидация здесь
        return self.__capital
    
    def set_capital(self, value: float):
        # Опционально: Какие-нибудь трансформации тут
        self.__capital = value

Пример с одним подчеркиванием приводить не буду, так как оно лишь сигнализирует другим разработчикам, что хоть это поле и доступно извне, лучше бы это поле не трогать.

Ситуация же с двумя подчеркиваниями, несколько более интересная.

Если в питоне действительно нет сокрытия, то мы можем напрямую обратиться к __capital, так? Что-ж давайте попробуем:

cap = Capitalist(9999)
print(cap.__capital)

Упс!!

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-16-f102899f3e8c> in <module>
      1 cap = Capitalist(9999)
----> 2 print(cap.__capital)

AttributeError: 'Capitalist' object has no attribute '__capital'

АГА, ПОПАВСЯ, ПАРШИВЫЙ ОБМАНЩИК!! В ПИТОНЕ ЕСТЬ СОКРЫТИЕ!!

Однако, не спешите с выводами. То что вы видите перед собой не настоящее сокрытие, а лишь подделка.

Видите ли, хоть одно подчеркивание в начале имени не делает ничего особенного, с двумя подчеркиваниями связанна одна занимательная механика, называемая name mangling, в простонародии “искажение имени”. То есть, питон, прямо как учитель в школе, когда видит в списке аттрибутов такое необычное имя, нещадно его коверкает. Если конкретнее, превращает его в имя вида _classname__variable, где classname это очевидное имя класса которому принадлежит аттрибут, а variable - имя переменной.

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

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

Однако же, это не является полноценным сокрытием, потому что мы все еще можем получить прямой доступ к этому полю, просто под другим именем:

cap = Capitalist(9999)
cap._Capitalist__capital -= cap._Capitalist__capital
print(cap._Capitalist__capital)

Вот так легко и просто, мы раскулачили буржуазию.

Но не стоит отчаиваться, сейчас я научу вас черной магии, которая защитит вас и ваши поля от такого произвола.

Решение проблемы

Как же мы будем решать проблему сокрытия в языке, где все аттрибуты на виду? Всё просто, надо использовать объекты, стейт которых не доступен извне.

Функции

def obj():
    x = 42
    return 0

Как бы вы ни старались, вы никак не получите из этой функции переменную x.

Но в функциях невозможно хранить стейт, так?

Well, you’re wrong! I just did it!

from types import SimpleNamespace as SN

def Capitalist(starting_capital: float):
    private = SN(
        capital=starting_capital
    )
    
    def set_capital(value):
        private.capital = value
    
    def get_capital():
        return private.capital
    
    public = SN(
        set_capital=set_capital,
        get_capital=get_capital
    )
    return public

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

И мы получаем следующее:

>>> cap = Capitalist(9999)
>>> cap.capital
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-40-f47e1c54c240> in <module>
    ----> 1 cap.capital
    
    AttributeError: 'types.SimpleNamespace' object has no attribute 'capital'
    
>>> cap.get_capital()
    9999
>>> cap.set_capital(12415)
>>> cap.get_capital()
    12415

И так, мы теперь не можем напрямую менять капитал, лишь через его геттеры и сеттеры.

Что?

У вас может возникнуть резонный вопрос: А где же хранится приватный стейт?

Когда мы создаем функцию внутри функции и используем переменные из внешнего скоупа, все эти переменные записываются в аттрибут функции __closure__, который представляет собой кортеж cell объектов. Cell объекты используются чтобы создавать ссылки на переменные которые используются в разных скоупах. Для каждой переменной создается соответствующий cell объект чтобы хранить их когда их скоуп уже ВСЁ. Так, для нашего private так же создался cell.

>>> cap.get_capital.__closure__
    (<cell at 0x00000145AA9CBBE0: types.SimpleNamespace object at 0x00000145AA9CBB80>,)
>>> cap.get_capital.__closure__[0].cell_contents
    namespace(capital=12415)
>>> cap.get_capital.__closure__[0].cell_contents.capital
    12415

так что это тоже не совсем тру, но кому придет в голову лезть в __closure__?