Python Decorator for Simplifying Delegate Pattern
Recently, I posted about how you can use composition instead of inheritance, and in that article, I mentioned the Delegate Pattern. As a quick description, the pattern has you inherit from the parent interface (or, in Python, you can just implement the protocol), but all the methods either redirect to calling the method on an injected implementation of interface/protocol, possibly surrounding the call in its own code, or it completely fills the method with its own implementation.
For the most part, it is like manually implementing inheritance, except that the superclass in injectable.
Anyway, one of the more annoying parts of implementing the Delegate Pattern is having to write all of the delegated calls. So I decided to look into the possibility of of making a decorator that will do that for you for all the methods you don’t explicitly implement.
Standard Attribute Delegation
First, we need a standard way of delegating an attribute to a delegate object’s attribute. We’ll use a descriptor for this. Its constructor takes in the name of the delegate stored on the object as well as the name of the attribute to look up. Here’s the descriptor:
class DelegatedAttribute: def __init__(self, delegate_name, attr_name): self.attr_name = attr_name self.delegate_name = delegate_name def __get__(self, instance, owner): if instance is None: return self else: # return instance.delegate.attr return getattr(getattr(instance, self.delegate_name), self.attr_name) def __set__(self, instance, value): # instance.delegate.attr = value setattr(getattr(instance, self.delegate_name), self.attr_name, value) def __delete__(self, instance): delattr(getattr(instance, self.delegate_name), self.attr_name) def __str__(self): return "<delegated attribute, '" + self.attr_name + "' at " + str(id(self)) + '>'
I added a nice __str__
method to make it look a little less like a boring object and more like a feature when people do some REPL diving. Normally, I don’t bother implementing the __delete__
method, but I felt it was appropriate here. I doubt it’ll ever be used, but if someone wants to use del
on an attribute, I want it to be effective.
Hmm, all those main methods have a repetitive part for getting the delegate object. Let’s extract a method and hopefully make them a little bit more legible; nested getattr
s, setattr
s, and delattr
s aren’t the easiest thing to look at.
class DelegatedAttribute: def __init__(self, delegate_name, attr_name): self.attr_name = attr_name self.delegate_name = delegate_name def __get__(self, instance, owner): if instance is None: return self else: # return instance.delegate.attr return getattr(self.delegate(instance), self.attr_name) def __set__(self, instance, value): # instance.delegate.attr = value setattr(self.delegate(instance), self.attr_name, value) def __delete__(self, instance): delattr(self.delegate(instance), self.attr_name) def delegate(self, instance): return getattr(instance, self.delegate_name) def __str__(self): return "<delegated attribute, '" + self.attr_name + "' at " + str(id(self)) + '>'
There, that’s a little easier to read. At the very least, we’ve removed some repetition. I wish we didn’t need the self
reference to call it, as that would make it even easier to look at, but trying to get around it would likely be a lot of work, just as ugly, or both.
Let’s see an example of this in action:
class Foo: def __init__(self): self.bar = 'bar in foo' def baz(self): return 'baz in foo' class Baz: def __init__(self): self.foo = Foo() bar = DelegatedAttribute('foo', 'bar') baz = DelegatedAttribute('foo', 'baz') x = Baz() print(x.bar) # prints 'bar in foo' print(x.baz()) # prints 'baz in foo'
It’s as simple as making sure you have an object to delegate to and adding a DelegatedAttribute
with the name of that object and the attribute to delegate to. As you can see, it properly delegates both functions and object fields as we would want. It’s not only useful for the Delegate pattern but really any situation in composition where a wrapper call simply delegates the call to the wrapped object.
This is good, but we don’t want to do this by hand. Plus, I mentioned a decorator. Where’s that? Hold your horses; I’m getting there.
Starting the Decorator
See? Now we’ve moved to the decorator. What is the decorator supposed to do? How should it work? First thing you should know is that we created the DelegatedAttribute
class for a reason. We’ll utilize that, assigning all the class attributes of the delegate object to the new class.
To do that, we’ll need a reference to the class. We’ll also need to know the name of the delegate object, so it can be given to the DelegatedAttribute
constructor. For now, we’ll hard code that. From this, we know that we need a parameterized decorator, whose outermost signature looks like delegate_as(delegate_cls)
. Let’s get it started.
def delegate_as(delegate_cls): # gather the attributes of the delegate class to add to the decorated class attributes = delegate_cls.__dict__.keys() def inner(cls): # create property for storing the delegate setattr(cls, 'delegate', SimpleProperty()) # set all the attributes for attr in attributes: setattr(cls, attr, DelegatedAttribute('delegate', attr)) return cls return inner
You may have noticed the creation of something called a SimpleProperty
, which won’t be shown. It is simply a descriptor that holds and returns a value given to it, the most basic of properties.
The first thing it does is collect all the attributes from the given class so it knows what to add to the decorated class, preparing it for the inner function that does the real work. The inner function is the real decorator, and it takes that sequence and adds those prepared attributes to the decorated class using DelegatedAttribute
s.
This may all seem alright, but it doesn’t actually work. The problem is that some of those attributes that we try to add on to the decorated class are ones that come with every class and are read only.
Skipping Some Attributes
So we need to skip over some attributes in the list. I had originally done this by creating a “blacklist” set of attribute names that come standard with every class. But then I realized that implementing a different requirement of the decorator would take care of it already. That requirement is that the decorator not write over any of the attributes that the class had explicitly implemented.
So, if the decorated class defines its own version of a method that the delegate has, the decorator should not add that method. Ignoring those attributes works the same as ignoring the ones I would put into the blacklist, since they’re already defined on the class.
So here’s what the code looks like with that requirement added in.
def delegate_as(delegate_cls): # gather the attributes of the delegate class to add to the decorated class attributes = set(delegate_cls.__dict__.keys()) def inner(cls): # create property for storing the delegate setattr(cls, 'delegate', SimpleProperty()) # don't bother adding attributes that the class already has attrs = attributes - set(cls.__dict__.keys()) # set all the attributes for attr in attrs: setattr(cls, attr, DelegatedAttribute('delegate', attr)) return cls return inner
As you can see, I turned the original collection of keys into a set
and did the same with the attributes from cls
. This is because we want to remove one set of keys from the other, and a really simple way to do that is with set
‘s subtraction. It could be done with filter
or or similar, but I like the cleanliness of this. It’s short, and its meaning is quite clear. I had to store the final result in attrs
instead of attributes
because the outer call could be reused, so we don’t want to change attributes
for those cases.
Some More Parameters
Okay, so we’re now able to automatically delegate any class-level attributes from a base implementation to the decorated class, but that’s often not the only public attributes that an object of a class has. We need a way to include those attributes. We could get an instance of the class and copy all the attributes that are preceded by a single underscore, or we could have the user supply the list of attributes wanted. I personally prefer to take in a user-supplied list. So, we’ll add an additional parameter to the decorator called include
, which takes in a sequence of strings that are the names of attributes to delegate to.
Next, what if there are some attributes that will be automatically copied over that we don’t actually want? We’ll add another parameter called ignore
to our decorator. It’s similar to include
, but is a blacklist instead of a whitelist.
Lastly, we want to get rid of that hard-coded attribute name, "delegate"
. Why? Because you should almost never hard code anything like that, and because there’s a chance that someone might try to double up on the delegate pattern, delegating to two different objects of different classes. We’d end up with trying to store two different delegates in the same attribute. So we need to give the user the option to make the name whatever they want. We’ll still default it to "delegate"
though, so the user won’t have to set it if they don’t need to.
All this leaves us with the final product!
def delegate_as(delegate_cls, to='delegate', include=frozenset(), ignore=frozenset()): # turn include and ignore into a sets, if they aren't already if not isinstance(include, Set): include = set(include) if not isinstance(ignore, Set): ignore = set(ignore) delegate_attrs = set(delegate_cls.__dict__.keys()) attributes = include | delegate_attrs - ignore def inner(cls): # create property for storing the delegate setattr(cls, to, SimpleProperty()) # don't bother adding attributes that the class already has attrs = attributes - set(cls.__dict__.keys()) # set all the attributes for attr in attrs: setattr(cls, attr, DelegatedAttribute(to, attr)) return cls return inner
I also made include
and ignore
have default values of an empty set
. They’re frozenset
s specifically, despite the fact that the code doesn’t try to mutate them, as a precautionary measure to prevent any future changes from doing so. You might have noticed that the checking of the default parameters is a little different than is typical. That’s because I didn’t make the default parameters default to None
. I made them default to something I can use.
Also, I generally expect that people will be passing in list
s or tuple
s more often than set
s, so I just made sure that they ended up being a subclass of Set
(the ABC, since neither frozenset
nor set
are subclasses of the other, and users may use some nonstandard Set
) so I can do the union and subtraction with them later. It also ensures no doubles anywhere.
Moving Forward
While I’ve shown the final working product, there’s still some refactoring that could be done to make the code cleaner. For instance, you could take all those comments and use Extract Method to make the actual code say such things. If I were to do that, I’d probably change the whole thing to a callable decorator class, rather than a decorator function.
Outro
That was a relatively long article. I could have just shown the final product to you and explained it, but I thought that going through a thought process with it would be nice, too. Are there any changes that you would recommend? What do you guys think of this? Is it worth it?
Reference: | Python Decorator for Simplifying Delegate Pattern from our WCG partner Jacob Zimmerman at the Programming Ideas With Jake blog. |