We want to expose an attribute on a facade object, while it is implemented by a child object. Python allows us to do this by declaring a read/write property.
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
In this code, the var1
property in Bar
is a proxy to the var1
attribute of the foo
instance.
With this implementation, each time we add an attribute in Foo
and want to expose it in Bar
, we have to declare 6 lines of codes. This boilerplate will grow and make the class more difficult to read and maintain.
We will implement a descriptor
proxy_property
to reduce this boilerplate to a single line. Here is the reimplemented Bar
class.
class Bar:
var1 = property_proxy("foo", "var1")
def __init__(self):
self.foo = Foo()
A descriptor
in python is a class in which we implement the dunder methods __get__
, __set__
and __delete__
.
Descriptors are a powerful, general purpose protocol. They are the mechanism behind properties, methods, static methods, class methods, and super().
Here is the implementation of the property_proxy
descriptor.
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)
To conclude
In this post, we found that exposing a child object's attribute in a facade would take a large boilerplate from us and interfere with reading and maintaining our facade.
We implemented a property_proxy
descriptor to make this declaration in one line and improve code consistency. The gain in readability is significant.
class Bar:
var1 = property_proxy("foo", "var1")
def __init__(self):
self.foo = Foo()
Limit
The property_proxy
declaration has 2 flaws which affect the maintainability of the code. This declaration does not allow a linter like mypy
to check the existence and typing of attributes. The IDE's refactoring of an attribute name may miss this declaration because it will consider the attributes in the property_proxy
declaration as plain text.
Top comments (0)