Functions and methods

The most resonating case of an object that is a descriptor is probably a function. Functions implement the __get__ method, so they can work as methods when defined inside a class.

Methods are just functions that take an extra argument. By convention, the first argument of a method is named "self", and it represents an instance of the class that the method is being defined in. Then, whatever the method does with "self", would be the same as any other function receiving the object and applying modifications to it.

In order words, when we define something like this:

class MyClass:
def method(self, ...):
self.x = 1

It is actually the same as if we define this:

class MyClass: pass

def method(myclass_instance, ...):
myclass_instance.x = 1

method(MyClass())

So, it is just another function, modifying the object, only that it's defined inside the class, and it is said to be bound to the object.

When we call something in the form of this:

instance = MyClass()
instance.method(...)

Python is, in fact, doing something equivalent to this:

instance = MyClass()
MyClass.method(instance, ...)

Notice that this is just a syntax conversion that is handled internally by Python. The way this works is by means of descriptors.

Since functions implement the descriptor protocol (see the following listing) before calling the method, the __get__() method is invoked first, and some transformations happen before running the code on the internal callable:

>>> def function(): pass
...
>>> function.__get__
<method-wrapper '__get__' of function object at 0x...>

In the instance.method(...) statement, before processing all the arguments of the callable inside the parenthesis, the "instance.method" part is evaluated.

Since method is an object defined as a class attribute, and it has a __get__ method, this is called. What this does is convert the function to a method, which means binding the callable to the instance of the object it is going to work with.

Let's see this with an example so that we can get an idea of what Python might be doing internally.

We will define a callable object inside a class that will act as a sort of function or method that we want to define to be invoked externally. An instance of the Method class is supposed to be a function or method to be used inside a different class. This function will just print its three parameters—the instance that it received (which would be the self parameter on the class it's being defined in), and two more arguments. Notice that in the __call__() method, the self parameter does not represent the instance of MyClass, but instead an instance of Method. The parameter named instance is meant to be a MyClass type of object:

class Method:
def __init__(self, name):
self.name = name

def __call__(self, instance, arg1, arg2):
print(f"{self.name}: {instance} called with {arg1} and {arg2}")

class MyClass:
method = Method("Internal call")

Under these considerations and, after creating the object, the following two calls should be equivalent, based on the preceding definition:

instance = MyClass()
Method("External call")(instance, "first", "second")
instance.method("first", "second")

However, only the first one works as expected, as the second one gives an error:

Traceback (most recent call last):
File "file", line , in <module>
instance.method("first", "second")
TypeError: __call__() missing 1 required positional argument: 'arg2'

We are seeing the same error we faced with a decorator in Chapter 5, Using Decorators to Improve Our Code. The arguments are being shifted to the left by one, instance is taking the place of selfarg1 is going to be instance, and there is nothing to provide for arg2.

In order to fix this, we need to make Method a descriptor.

This way, when we call instance.method first, we are going to call its __get__(), on which we bind this callable to the object accordingly (bypassing the object as the first parameter), and then proceed:

from types import MethodType

class Method:
def __init__(self, name):
self.name = name

def __call__(self, instance, arg1, arg2):
print(f"{self.name}: {instance} called with {arg1} and {arg2}")

def __get__(self, instance, owner):
if instance is None:
return self
return MethodType(self, instance)

Now, both calls work as expected:

External call: <MyClass object at 0x...> called with fist and second
Internal call: <MyClass object at 0x...> called with first and second

What we did is convert the function (actually the callable object we defined instead) to a method by using MethodType from the types module. The first parameter of this class should be a callable (self, in this case, is one by definition because it implements __call__), and the second one is the object to bind this function to.

Something similar to this is what function objects use in Python so they can work as methods when they are defined inside a class.

Since this is a very elegant solution, it's worth exploring it to keep it in mind as a Pythonic approach when defining our own objects. For instance, if we were to define our own callable, it would be a good idea to also make it a descriptor so that we can use it in classes as class attributes as well.

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

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