-2

I am working through the Python Cookbook.

The following code defines decorators as classes:

import types
from functools import wraps

class Profiled:
    def __init__(self, func):
        wraps(func)(self)
        self.ncalls = 0

    def __call__(self, *args, **kwargs):
        self.ncalls += 1
        return self.__wrapped__(*args, **kwargs)

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

# Example

@Profiled
def add(x, y):
    return x + y

class Spam:
    @Profiled
    def bar(self, x):
        print(self, x)

if __name__ == '__main__':
    print(add(2,3))
    print(add(4,5))
    print('ncalls:', add.ncalls)

    s = Spam()
    s.bar(1)
    s.bar(2)
    s.bar(3)
    print('ncalls:', Spam.bar.ncalls)

yields:

5
9
ncalls: 2
<__main__.Spam object at 0x1187d0828> 1
<__main__.Spam object at 0x1187d0828> 2
<__main__.Spam object at 0x1187d0828> 3
ncalls: 3

It makes sense to me how the Profiled class is decorating these functions, however, I'm struggling to understand what purpose the Profiled __ get __ method serves and why it is needed?

Can anyone explain?

pablowilks2
  • 299
  • 5
  • 15

1 Answers1

0

Profiled is a class that defines a descriptor object, i.e., it defines __get__, __set__ or __delete__.

Fundamentally, a descriptor is any python type that implements any combination of __get__, __set__ or __delete__.

These will get invoked when you do some_object.some_attribute, some_object.some_attribute = value, and del some_object.some_attribute where some_attribute is a descriptor on some_object.__class__.

You can read about the descriptor protocol here.

It is also worth going through Descriptors HOWTO

Essentially, this __get__ allows your decorator to act like a method (function objects are descriptors! and the way that methods get bound to instances is that FunctionType.__get__ does essentially what is written in that Profiled.__get__)

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • thanks juanpa, i'm confused about when _ _get_ _ gets called. if i do s.bar(1) it's _ _call_ _ that gets called, right? So, what causes _ _get_ _ to be called? – pablowilks2 Mar 11 '20 at 21:12
  • @pablowilks2 no, `__get__` gets called when you do `s.bar`, in which case, it returns `self` wrapped by `MethodType`, which essentially partially applies the `self` argument to `__call__` – juanpa.arrivillaga Mar 11 '20 at 21:22
  • thanks @juanpa.arrivillaga so if I do s.bar(1), does ```__get__``` get called which in turn passes self to ```__call__``` which in turn, gets called? – pablowilks2 Mar 11 '20 at 21:53
  • @pablowilks2 basically, yes. Essentially, `s.bar` returns `MethodType(profiled_instance, instance_being_profiled)`, which basically partially applies `instance_being_profiled` to `profiled_instance`, so *then* `Profiled.__call__` gets called with `profiled_instance(instance_being_profiled, *args, **kwargs)` – juanpa.arrivillaga Mar 11 '20 at 21:59
  • thanks @juanpa.arrivillaga, am i right then that when s.bar(1) is called, Python will retrieve the bar/Profiled function by invoking __get__ in Profiled? In this particular case Profiled uses __get__ to bind Profiled/bar to the instance (in this case s) and then returns Profiled/bar to Python. At which point Python calls Profiled.__call__ as a method of s? – pablowilks2 Mar 11 '20 at 23:44
  • Again, `Profiled.__get__` is called when `s.bar` is evaluated, you can think of `s.bar(1) -> methodobject(1)`, where `__get__` returns the method object. `s` gets bound to the `profiled_instance`, just like `s` would get bound to a regular method. – juanpa.arrivillaga Mar 11 '20 at 23:45