ComparisonDifficulty: Intermediate2 min

Proxy Classes vs __getattr__ in Python

One method forwards every attribute the proxy does not define, instead of one method per call.

Python can write a proxy without listing every method: __getattr__ is called whenever normal lookup fails to find an attribute, so one method can forward everything the proxy does not define itself. This card compares that form with the explicit class of the template, and shows the two things it does not forward.

A method per call, and one method for all of them

An explicit proxy classeach method written out

py
class LoggingInbox(Inbox):
    def __init__(self, target):
        self._target = target

    def unread(self):
        print("proxy: forwarding unread")
        return self._target.unread()

Every method of the interface needs a forwarding method, and a type checker sees all of them.

A __getattr__ proxyforwarding by name

py
class LoggingProxy:
    def __init__(self, target):
        self._target = target

    def __getattr__(self, name):
        print(f"proxy: forwarding {name}")
        return getattr(self._target, name)

One method forwards any attribute, for any target, including methods added later.

py
class Inbox:
    def __init__(self) -> None:
        self._messages = ["hello", "invoice"]

    def unread(self) -> int:
        return len(self._messages)

    def __len__(self) -> int:
        return len(self._messages)


class LoggingProxy:
    def __init__(self, target: object) -> None:
        self._target = target

    def __getattr__(self, name: str):
        print(f"proxy: forwarding {name}")
        return getattr(self._target, name)


inbox = LoggingProxy(Inbox())
print(inbox.unread())
print(inbox.__len__())
try:
    print(len(inbox))
except TypeError as error:
    print(f"TypeError: {error}")
OutputRan with python:3.12-slim (Python 3.12.13)
proxy: forwarding unread
2
proxy: forwarding __len__
2
TypeError: object of type 'LoggingProxy' has no len()
`unread` and an explicit `__len__()` are forwarded; `len(inbox)` is not.

The first two calls go through __getattr__, because LoggingProxy has no unread or __len__ of its own. The third fails: len() looks up __len__ on the object's class, not the object, and so never triggers __getattr__. Every special method behaves this way, __iter__, __eq__ and __getitem__ included, so a __getattr__ proxy over a list cannot be iterated or indexed until those methods are written out.

The second loss is type checking. A type checker cannot know what __getattr__ will return, so a LoggingProxy is not a Subject to it, and misspelt method names are found only when the code runs. Use __getattr__ for a quick, general proxy such as logging in a test, and an explicit class when the proxy is part of a typed interface or must support special methods.