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

4. Descriptors in the Standard Library

Jacob Zimmerman1 
(1)
New York, USA
 

There are three basic, well-known descriptors that come with Python: property, classmethod, and staticmethod. There’s also a fourth one that you use all the time, but are less likely to know is a descriptor.

Of all the descriptors being shown in this chapter, it’s possible that you only knew of property as a descriptor. Plenty of people even learn the basics of descriptors from it, but a lot of people don’t know that classmethod and staticmethod are descriptors. They feel like super magical constructs built into the language that no one could reproduce in pure Python. Once someone has an understanding of descriptors, though, their basic implementation becomes relatively obvious. In fact, example code will be provided for all three in simplified, pure Python code.

Lastly, it will be shown that all methods are actually implemented with descriptors. Normal methods are actually done “magically,” since the descriptor creation is implicit, but it’s still not entirely magical because it’s done using a language construct the anyone could create.

What I find really interesting is that the first three are all function decorators, which are another really awesome feature of Python that deserves its own book, even though they’re way simpler.

The property Class

This book doesn’t include instructions for how to use the property class and decorator; it is focused on understanding and creating descriptors. The official documentation for using property can be found in Python’s documentation2.

Of all the descriptors out there, property is likely the most versatile. This is because it doesn’t really do anything on its own, but rather allows the users to inject their wanted functionality into it by providing their own getters, setters, and deleters.

To get a better idea of how it works, here is a simplified pure Python implementation of property.
class property:
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
    def __get__(self, instance, owner):
        if instance is None:
            return self
        elif self.fget is None:
            raise AttributeError("unreadable attribute")
        else:
            return self.fget(instance)
    def __set__(self, instance, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        else:
            self.fset(instance, value)
    def __delete__(self, instance):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        else:
            self.fdel(instance)
    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel)
    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel)
    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel)

As you can now see, the property class has almost no real functionality of its own; it simply delegates to the functions given to it. When a function is not provided for a certain method to delegate to, property assumes that it is a forbidden action and raises an AttributeError with an appropriate message.

A nice thing about the property class is that it largely just accepts methods. Even its constructor, which can be given all three methods at once, is capable of being called with just one, or even none. Because of this, the constructor and other methods can be used as decorators in a very convenient syntax. Check out the documentation2 to learn more about it.

Omitted from this code example is the doc functionality, where it sets its own __doc__ property based on what is passed in through __init__()’s doc parameter or using __doc__ from fget if nothing is given. Also omitted is the code that sets other attributes on property, such as __name__, in order to help it appear even more like a simple attribute. They did not seem important enough to worry about, since the focus was more on the main functionality.

The classmethod Descriptor

classmethod is another descriptor that can be used as a decorator, but, unlike property, there’s no good reason not to use it as one. classmethod is an interesting concept that doesn’t exist in many other languages (if any). Python’s type system, which uses classes as objects, makes classmethods easy and worthwhile to make.

Here’s the Python code for classmethod.
class classmethod:
    def __init__(self, func):
        self.func = func
    def __get__(self, instance, owner):
        return functools.partial(self.func, owner)

That’s all there is to it. classmethod is a non-data descriptor, so it only implements __get__(). This __get__() method completely ignores the instance parameter because, as “class” in the name implies, the method has nothing to do with an instance of the class and only deals with the class itself. What’s really nice is the fact that this can still be called from an instance without any issues.

Why does the __get__() method return a functools.partial object with the owner passed in, though? To understand this, think about the parameter list of a function marked as a classmethod. The first parameter is the class parameter, usually named cls. This class parameter is filled in the call to partial so that the returned function can be called with just the arguments the user wants to explicitly provide. The true implementation doesn’t use partial, but works similarly.

Again, the code that sets __name__, __doc__, etc. is omitted to show only how the main functionality works.

The staticmethod Descriptor

A method marked with staticmethod is strange in that it’s a method that is really just a function, but it is “attached” to a class. Being part of the class doesn’t do anything other than show users that it is associated with that class and giving it a more specific namespace. Also, interestingly, because staticmethod and classmethod are implemented using descriptors, they’re inherited by subclasses.

The implementation of staticmethod is even simpler than that of classmethod; it just accepts a function and then returns it when __get__() is called.
class staticmethod:
    def __init__(self, func):
        self.func = func
    def __get__(self, instance, owner):
        return self.func

Regular Methods

Remember that it was stated earlier that regular methods implicitly use descriptors as well. In fact, all functions can be used as methods. This is because functions are non-data descriptors as well as callables.

Here is a Python implementation that roughly shows how a function looks.
class function:
    def __call__(self, *args, **kwargs):
        # do something
    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        else:
            return functools.partial(self, instance)

This is not a very accurate representation; the return statements are a bit off. When you access a method from an instance without calling it, the returned object isn’t a partial object; it is a “bound method”. A “bound method” is one that has self already “bound” to it, but has yet to be called, passing in the other arguments if needed. When it’s called from the class, it only returns the function itself. In Python 2, this was an “unbound method,” which is basically the same thing. This idea of creating “unbound” versions when instance is None comes up later, so keep it in mind.

Summary

In this chapter, we’ve seen the most common built-in descriptors. Now that we’ve seen some examples, let’s get a closer, better look at how they work by digging into the real differences between data and non-data descriptors.

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

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