Статьи

Ничто не является приватным: закрытие Python (и ctypes)


Как я уверен, вы знаете, что в Python нет концепции закрытых членов.
Иногда используется одна хитрость — скрыть объект внутри замыкания Python и предоставить прокси-объект, который разрешает только ограниченный доступ к исходному объекту.

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

def hide(obj):
    class Proxy(object):
        __slots__ = ()
        def __getattr__(self, name):
            return getattr(obj, name)
    return Proxy()

Вот оно в действии:

>>> class Foo(object):
...     def __init__(self, a, b):
...         self.a = a
...         self.b = b
...
>>> f = Foo(1, 2)
>>> p = hide(f)
>>> p.a, p.b
(1, 2)
>>> p.a = 3
Traceback (most recent call last):
  ...
AttributeError: 'Proxy' object has no attribute 'a'

После того, как функция hide вернула прокси-объект, метод __getattr__ может получить доступ к исходному объекту через замыкание . Он сохраняется в методе __getattr__ как атрибут func_closure (Python 2) или атрибут __closure__ (Python 3). Это «объект ячейки», и вы можете получить доступ к содержимому ячейки, используя атрибут cell_contents :

>>> cell_obj = p.__getattr__.func_closure[0]
>>> cell_obj.cell_contents
<__main__.Foo object at 0x...>

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

Что мы не можем сделать из чистого Python, так это * установить * содержимое ячейки, но в Python нет ничего приватного — или, по крайней мере, не в CPython.

Есть две функции API Python C, PyCell_Get и PyCell_Set , которые обеспечивают доступ к содержимому замыканий. Из ctypes мы можем вызвать эти функции, а также проанализировать и изменить значения внутри объекта ячейки:

>>> import ctypes
>>> ctypes.pythonapi.PyCell_Get.restype = ctypes.py_object
>>> py_obj = ctypes.py_object(cell_obj)
>>> f2 = ctypes.pythonapi.PyCell_Get(py_obj)
>>> f2 is f
True
>>> new_py_obj = ctypes.py_object(Foo(5, 6))
>>> ctypes.pythonapi.PyCell_Set(py_obj, new_py_obj)
0
>>> p.a, p.b
(5, 6)

Как вы можете видеть, после вызова PyCell_Set прокси-объект использует новый объект, который мы помещаем в замыкание вместо исходного. Использование ctypes может показаться обманом, но для того, чтобы сделать то же самое, потребуется всего три раза.

Две заметки об этом коде.

  1. Это (конечно) не переносимо между различными реализациями Python
  2. Не когда — нибудь это сделать, это только для иллюстрации!

Тем не менее, интересный взгляд на внутренности CPython с ctypes. Интересно, что я слышал об одном возможном случае использования такого кода. Утверждается, что в какой-то момент Армин Роначер использовал похожую технику в Джинджа2 для улучшения трассировки. (Обратные ссылки от языков шаблонов могут быть очень хитрыми, потому что скомпилированный код Python обычно имеет довольно отдаленное отношение к оригинальному текстовому шаблону.) Только то, что Armin делает это, не значит, что вы можете …подмигивать