© Jacob Zimmerman 2018
Jacob ZimmermanPython Descriptorshttps://doi.org/10.1007/978-1-4842-3727-4_11

11. Reusing the Wheel

Jacob Zimmerman1 
(1)
New York, USA
 

Whenever possible and sensible, one should try to avoid reinventing the wheel. This chapter goes over a set of classes to use as superclasses and strategies to help build new descriptors a little faster. Only barebones code is presented here; the full code examples are in the library.

Storage Solutions

The first code examples cover storage “strategies” (which I’m calling “solutions”) that a descriptor can use for its storage. These strategies can be hard-coded into new descriptors or be passed into the descriptor’s initializer to be chosen on a case-by-case basis. Only two basic strategies will be shown here; the rest can be found in the library.
class OnDescriptorStorageSolution:
    def __init__(self):
        self.storage = DescriptorStorage()
    def get(self, instance):
        return self.storage[instance]
    def set(self, instance, value):
        self.storage[instance] = value
    def delete(self, instance):
        del self.storage[instance]
class NameGetter:
    def __init__(self, name_lookup_strategy):
        self.lookup_strategy = name_lookup_strategy
        self.name = None
    def __call__(self, instance, descriptor):
        if self.name is None:
            self.name = self.lookup_strategy(instance, descriptor)
        return self.name
    def set(self, name):
        self.name = name
class OnInstanceStorageSolution:
    def __init__(self, name_lookup_strategy):
        self.name = NameGetter(name_lookup_strategy)
    def get(self, instance):
        return instance.__dict__[self.name(instance, self)]
    def set(self, instance, value):
        instance.__dict__[self.name(instance, self)] = value
    def delete(self, instance):
        del instance.__dict__[self.name(instance, self)]
    def set_name(self, name):
        self.name.set(name)

Clearly, these storage solutions are designed for per-instance storage. This is due to two reasons: per-class storage is trivial and therefore doesn’t need pre-built solutions; and per-instance storage is much more common.

The NameGetter class and its use might be just a little confusing. As stated in the chapter about storage, the most difficult thing about storing on the instances is figuring out how to find the name of where to store, so the OnInstanceStorageSolution class takes in a name_lookup_strategy. This strategy is just a function that accepts instance and the descriptor and returns the name to store at. The strategy accepts those two parameters because those are the only pieces of information guaranteed that can be used for the lookup, and they’re also required for doing lookup via name_of(), as mentioned earlier in the book. If the name is already decided, the lookup strategy can simply be None, and you call set(). The set() method is also useful for being called from __set_name__(), which is why OnInstanceStorageSolution also has a set_name() method to be called from the descriptor.

NameGetter isn’t technically required to do the work necessary, but is used to cache the name after the name has been calculated. That way, the lookup method doesn’t need to be called more than once; it’s called once, then stored for quick returns on subsequent lookups.

Now that storage solutions have been shown, here are some example descriptors using or prepared to be supplied with a storage solution object (delete methods are omitted for simplicity’s sake).
class ExampleWithHardCodedStrategy:
    def __init__(self):
        self.storage = OnDescriptorStorageSolution()
    def __get__(self, instance, owner):
        # any pre-fetch logic
        value = self.storage.get(instance)
        # any post-fetch logic
        return value
    def __set__(self, instance, value):
        # any pre-set logic
        self.storage.set(instance, value)
class ExampleWithOpenStrategy:
    def __init__(self, storage_solution):
        self.storage = storage_solution
    def __get__(self, instance, owner):
        # any pre-fetch logic
        value = self.storage.get(instance)
        # any post-fetch logic
        return value
    def __set__(self, instance, value):
        # any pre-set logic
        self.storage.set(instance, value)
These strategies could also be subclassed, making the strategy methods more like template-called methods. For example:
class ExampleWithSuperclassStrategy(OnDescriptorStorageSolution):
    def __get__(self, instance, owner):
        # any pre-fetch logic
        value = self.get(instance) # calls the solution method on itself
        # any post-fetch logic
        return value
    def __set__(self, instance, value):
        # any pre-set logic
        self.set(instance, value) # same here

Using the storage solutions like this is a cleaner way of hard-coding the solution.

Read-Only Solutions

Another utility class that can be built is a wrapper that can turn any other descriptor into a read-only descriptor. Here’s an example using the set-once style.
class ReadOnly:
    def __init__(self, wrapped):
        self.wrapped = wrapped
        self.setInstances = set()
    def __set__(self, instance, value):
        if instance in self.setInstances:
            raise AttributeError("Cannot set new value on read-only property")
        else:
            self.setInstances.add(instance)
            self.wrapped.__set__(instance, value)
    def __getattr__(self, item):
        # redirects any calls other than __set__ to the wrapped descriptor
        return getattr(self.wrapped, item)
def readOnly(deco):  # a decorator for wrapping other decorator descriptors
    def wrapper(func):
        return ReadOnly(deco(func))
    return wrapper

It even includes a decorator decorator for decorating descriptors being used as decorators. (Yo dawg; I heard you like decorators, so I put decorators in your decorators.) This isn’t meant for wrapping just any decorators; it’s only meant for wrapping decorators that produce descriptors. It’s not likely to be used often, since most descriptors that are created from decorators are non-data descriptors, making the ReadOnly wrapping not very useful. But it doesn’t hurt to have it anyway, just in case; especially after claiming it can wrap any other descriptor.

It can be noted that ReadOnly only implements the __set__() method of the descriptor protocol. This is because it’s the only one that it covers. It uses __getattr__() in order to redirect calls to potential __get__() and __delete__() methods because it doesn’t know which ones might be implemented. Unfortunately, this doesn’t work. When calling “magic” methods implicitly, Python doesn’t look up the methods normally. For the sake of speed, it directly checks just the dictionary on the classes and no further.

This unfortunately makes using the object-oriented decorator pattern extremely difficult to do correctly. Essentially, you need to implement the methods in such a way as to mimic __getattribute__() itself. In descriptor_tools.decorators.DescriptorDecoratorBase, you can see what I mean. It checks what methods the wrapped descriptor has and decides whether to delegate to the wrapped descriptor, to the instance, or to raise errors you’d otherwise get.

An alternative is to design your descriptors to take strategies at creation, but this only works with your own descriptors and doesn’t allow you to extend descriptors that are out of your control.

Simple Unbound Attributes

Reusable code can be created for making the __get__() method return unbound attributes when instance isn’t provided rather than returning the descriptor, too. It can be done via a wrapper class (assuming it’s designed to handle the correct methods), via inheritance, or even a method decorator:
def binding(get):
    @wraps(get)
    def wrapper(self, instance, owner):
        if instance is None:
            return UnboundAttribute(self, owner)
        else:
            return get(self, instance, owner)
    return wrapper
This simple decorator can be used inside a descriptor easily:
class Descriptor:
    # other implementation details
    @binding
    def __get__(self, instance, owner):
        # implementation that assumes instance is not None

By simply adding the call to the decorator, you can simplify the code you have to write, ignoring writing anything that has to deal with the possibility of instance being None, other than the decorator.

There’s also an object decorator (i.e., a Gang of Four decorator) version in the library so that any existing descriptor can be transformed to return unbound attributes. For example, if users want to use attribute binding with an existing descriptor that doesn’t provide them, they could do something like this:
class MyClass:
    @Binding
    @property
    def myProp(self):
        # gets the actual property
Binding is a class that wraps an entire descriptor. Now property can be used with unbound attributes. (With some caveats: if you continue and define a setter for myProp, myProp will be replaced with a new property object; only add the @Binding call to the last method decorated with the property.) With descriptors that aren’t being used as decorators, it would look like this:
class MyClass:
    myProp = Binding(SomeDescriptor(...))

There is no version that works with inheritance since calling either of the decorators is easier than trying to create a superclass for the new descriptor to inherit from.

Summary

This is all the categories of helpful code provided in the library (other than what the entire next chapter is about), but it is by no means the only pieces of code there. There are a ton of helpful pieces there to help you build your own descriptors, to mix and match certain pieces into a cohesive whole descriptor where you need to do minimal work to add your core logic among the rest of it.

In this chapter, we’ve seen how reusable pieces can be made that can make implementing descriptors a little quicker and easier, as well as a little bit more standardized. As mentioned, all of these tools (and more) will be available in the library as well as on GitHub. Hopefully, they will help make your lives easier when you try to create your own descriptors.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset