2016-04-21

Python's Attribute Descriptors

This is a take on Python's attribute descriptors, a very elegant part of the Python language. I'd like to take a view on attribute descriptors by looking at functions and methods in Python.

Let's define a simple class with a method f.

class ClassWithMethod:
    def f(self, value):
        self.value = value

Let's have a look at the method we just defined.

ClassWithMethod.f
<function __main__.ClassWithMethod.f>

The output tells us that ClassWithMethod.f is a function. No big surprise so far. Let's instantiate an object of the class and look at it's f attribute again.

instance_of_class_with_method = ClassWithMethod()
instance_of_class_with_method.f
<bound method ClassWithMethod.f of <__main__.ClassWithMethod object at 0x7f50cc3e93c8>>

The f attribute of the object is a "bound method". So how did f get converted from a function to a bound method? The special method __get__ every function in Python has is at the core of the magic. Let's have a look at it.

We first define a very simple function which we call function.

def function(value):
    return value

This function has a method __get__:

function.__get__
<method-wrapper '__get__' of function object at 0x7f50cc452c80>

As we see this method is a "method-wrapper". To have a closer look at __get__ we use the inspect module to display its signature.

import inspect
inspect.signature(function)
<Signature (value)>

The signature of our function function just gets value as input. No surprise here. Let's look at __get__:

inspect.signature(function.__get__)
<Signature (instance, owner, /)>

The __get__ method gets an instance and an owner. Any object with such a method is a so called attribute descriptor. Now we're going to implement our own function class, which can also be bound to an object.

class Function:
    def __get__(self, instance, owner):
        if instance is None:  # called on the class
            return self
        return BoundMethod(instance, self)  # called in the instance

    def __call__(self, instance, value):
        instance.value = value

    def __repr__(self):
        return "<Function>"

The really special part is the __get__ method which allows to bind an instance of Function to an object. We also create a BoundMethod class:

class BoundMethod:
    def __init__(self, instance, function):
        self.instance = instance
        self.function = function

    def __call__(self, *args, **kwargs):
        return self.function(self.instance, *args, **kwargs)

    def __repr__(self):
        return "<BoundMethod of {instance}>".format(instance=self.instance)

It only forwards the __call__ from the Function. Moreover it stores a reference to an instance and to the function. We also need a simple class which uses that function

class MyClass:
    a = Function()

    def __init__(self):
        self.value = 0

    def __repr__(self):
        return "<MyClass value={}>".format(self.value)

Nothing has happened yet:

my_instance = MyClass()
my_instance
<MyClass value=0>

But we can no call a on my_instance.

my_instance.a(3)
my_instance
<MyClass value=3>

As we see, it actually modifies my_instance. Moreover, as attribute of the class it is a function:

MyClass.a
<Function>

As attribute of the instance, it is a bound method:

my_instance.a
<BoundMethod of <MyClass value=3>>

In summary, it pretty much behaves like a function/method we would have defined conventionally in Python. The magic part is really the __get__ method of Function. Accessing MyClass.a actually calls Function.__get__(a, None, MyClass). Accessing it on my_instance calls actually Function.__get__(a, my_instance, MyClass). So depending on whether instance is None or not, we can decide whether it has been accessed on the class or on an instance. Accordingly we can return the function itself or a bound method.

Really beautiful language design! Isn't it?