Как я уверен, вы знаете, что в 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 может показаться обманом, но для того, чтобы сделать то же самое, потребуется всего три раза.
Две заметки об этом коде.
- Это (конечно) не переносимо между различными реализациями Python
- Не когда — нибудь это сделать, это только для иллюстрации!
Тем не менее, интересный взгляд на внутренности CPython с ctypes. Интересно, что я слышал об одном возможном случае использования такого кода. Утверждается, что в какой-то момент Армин Роначер использовал похожую технику в Джинджа2 для улучшения трассировки. (Обратные ссылки от языков шаблонов могут быть очень хитрыми, потому что скомпилированный код Python обычно имеет довольно отдаленное отношение к оригинальному текстовому шаблону.) Только то, что Armin делает это, не значит, что вы можете …