Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to understand this python decorator

I am new to python decorators. I have understood the basic concepts with the help of simple examples. But when I tried to read this more practical decorator, I feel lost. Given below is the code followed by my questions:

class countcalls(object):
   "Decorator that keeps track of the number of times a function is called."

   __instances = {}

   def __init__(self, f):
      self.__f = f
      self.__numcalls = 0
      countcalls.__instances[f] = self

   def __call__(self, *args, **kwargs):
      self.__numcalls += 1
      return self.__f(*args, **kwargs)

   def count(self):
      "Return the number of times the function f was called."
      return countcalls.__instances[self.__f].__numcalls

@countcalls
def f():
   print 'f called'

f()
f()
f()
print f.count() # prints 3

My doubts:

  1. When we prefix the decorator to a function, does that mean that we are creating an object of the decorator class right there? In our case, when it says:

    @countcalls def f(): print 'f called'

Is @countcalls equivalent to creating a countcalls object and passing the function below to its __init__ method?

  1. The __call__ is taking three arguments. self is fine as far as the question above is answered. What the hell are the two other arguments: *args, **kwargs and what are they achieving?

  2. How can I get better at decorators?

like image 523
ritratt Avatar asked Jun 26 '12 20:06

ritratt


People also ask

What is the decorator in Python?

A decorator is a design pattern in Python that allows a user to add new functionality to an existing object without modifying its structure. Decorators are usually called before the definition of a function you want to decorate.

How do decorators work in Python?

Decorators provide a simple syntax for calling higher-order functions. By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

Why do we use decorators in Python?

When to Use a Decorator in Python. You'll use a decorator when you need to change the behavior of a function without modifying the function itself. A few good examples are when you want to add logging, test performance, perform caching, verify permissions, and so on.

Is Python decorator a decorator pattern?

Decorator is a structural pattern that allows adding new behaviors to objects dynamically by placing them inside special wrapper objects, called decorators. Using decorators you can wrap objects countless number of times since both target objects and decorators follow the same interface.


1 Answers

This code seems to have some oddness. Let's talk about the slightly-simpler code

class countcalls(object):

    def __init__(self, f):
        self._f = f
        self._numcalls = 0

    def __call__(self, *args, **kwargs):
        self._numcalls += 1
        return self._f(*args, **kwargs)

    def count(self):
        return self._numcalls

@countcalls
def f():
    print 'f called'

f()
f()
f()
print f.count() 

# output:
#   f called
#   f called
#   f called
#   3

Remember

@countcalls
def f():
    print 'f called'

is the same thing as

def f():
    print 'f called'
f = countcalls(f)

so when we use the decorator, the function is stored using the _f attribute.

So f is a countcalls instance. When you do f(...) you call f.__call__(...)—that's how you implement () syntax for your own instances. So when you call f, what happens?

    def __call__(self, *args, **kwargs):
        self._numcalls += 1
        return self._f(*args, **kwargs)

First, you use *args and **kwargs, which in the definition condense all positional and keyword arguments into a tuple and dict, and later in the call expand a sequence and a dict into arguments (see 4.7.4 in the official tutorial for more information). Here's a partial example

>>> def f(*args): print args
... 
>>> f(1, 2)
(1, 2)
>>> f()
()
>>> def add(a, b): return a + b
... 
>>> add(*[4, 3])
7
>>> add(**{'b': 5, 'a': 9})
14

so def f(*args, **kwargs): return g(*args, **kwargs) just does a passthrough on all arguments.

Aside from that, you're just keeping track of how many times you've been in __call__ for this instance (how many times you've called f).

Just remember that @dec def f(...): ... is the same as def f(...): ... f = dec(f) and you should be able to figure out most decorators fine, given enough time. Like all things, practice will help you do this quicker and easier.

like image 143
Mike Graham Avatar answered Sep 23 '22 06:09

Mike Graham