Мы хотим раскрыть атрибут на фасадном объекте, в то время как он реализуется дочерним объектом. Python позволяет нам сделать это, объявив свойство для чтения/записи.
class Foo:
def __init__(self):
self.var1 = "hello"
class Bar:
def __init__(self):
self.foo = Foo()
@property
def var1(self):
return self.foo.var1
@var1.setter
def var1(self, value):
self.foo.var1 = value
В этом коде свойство var1
в Bar
является прокси для атрибута var1
экземпляра foo
.
При такой реализации каждый раз, когда мы добавляем атрибут в Foo
и хотим раскрыть его в Bar
, мы должны объявить 6 строк кода. Эта кодовая таблица будет расти и сделает класс более сложным для чтения и сопровождения.
Мы реализуем descriptor
proxy_property
, чтобы сократить эту кодовую таблицу до одной строки. Вот переделанный класс Bar
.
class Bar:
var1 = property_proxy("foo", "var1")
def __init__(self):
self.foo = Foo()
Дескриптор descriptor
в python — это класс, в котором мы реализуем методы dunder __get__
, __set__
и __delete__
.
Дескрипторы — это мощный протокол общего назначения. Они являются механизмом, стоящим за свойствами, методами, статическими методами, методами класса и super().
Python Doc — Дескриптор HowTo Guide
Вот реализация дескриптора property_proxy
.
class property_proxy(object):
"""
A descriptor based recipe that makes it possible to write shorthands
that forward attribute access from one object onto another.
>>> class B:
>>> def __init__(self):
>>> self.foo = 12
>>> self.bar = 12
>>>
>>> class A:
>>> foo: int = property_proxy("b", "foo")
>>> bar: int = property_proxy("b", "bar")
>>>
>>> def __init__(self):
>>> self.b = B()
>>>
>>> a = A()
>>> print(a.foo)
This descriptor avoids writing the code below to establish a proxy
with a child instance
>>> class B:
>>> def __init__(self):
>>> self.foo = 12
>>>
>>> class A:
>>>
>>> def __init__(self):
>>> self.b = B()
>>>
>>> @property
>>> def foo(self):
>>> return self.b.foo
>>>
>>> @foo.setter
>>> def foo(self, value):
>>> self.b.foo = value
>>>
"""
def __init__(self, objectName, attrName):
self.objectName = objectName
self.attrName = attrName
def __get__(self, instance, owner=None):
proxy = getattr(instance, self.objectName)
if proxy is None:
raise ValueError(f"{self.objectName} does not exists on {instance}")
return getattr(proxy, self.attrName)
def __set__(self, instance, value):
proxy = getattr(instance, self.objectName)
if proxy is None:
raise ValueError(f"{self.objectName} does not exists on {instance}")
setattr(proxy, self.attrName, value)
В заключение
В этой заметке мы обнаружили, что раскрытие атрибута дочернего объекта в фасаде потребовало бы от нас большого кодового шаблона и мешало бы чтению и обслуживанию нашего фасада.
Мы реализовали дескриптор property_proxy
, чтобы сделать это объявление в одну строку и улучшить согласованность кода. Выигрыш в читабельности значителен.
class Bar:
var1 = property_proxy("foo", "var1")
def __init__(self):
self.foo = Foo()
Ограничение
Объявление property_proxy
имеет 2 недостатка, которые влияют на сопровождаемость кода. Это объявление не позволяет линтеру типа mypy
проверить существование и типизацию атрибутов. Рефакторинг имени атрибута в IDE может пропустить это объявление, потому что будет рассматривать атрибуты в объявлении property_proxy
как обычный текст.
Подробнее
- Python Doc — Дескриптор HowTo Guide