Declare a proxy property to a child object

farcellier

Fabien Arcellier

Posted on May 15, 2022

Declare a proxy property to a child object

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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().

Python Doc - Descriptor HowTo Guide

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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.

Read more

đź’– đź’Ş đź™… đźš©
farcellier
Fabien Arcellier

Posted on May 15, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related