6

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?

ritratt
  • 1,703
  • 4
  • 25
  • 45
  • one of my favorites: http://stackoverflow.com/q/739654/1025391 – moooeeeep Jun 26 '12 at 20:15
  • Quick style guide, don't use double underscore for private variables, use only a single one. – Chinmay Kanchi Jun 26 '12 at 20:16
  • 1
    @ChinmayKanchi: double underscore has a special meaning in Python; [it causes names to be mangled](http://docs.python.org/reference/expressions.html#atom-identifiers). – Fred Foo Jun 26 '12 at 20:17
  • Why do you want the function's behavior to change depending on the number of time it's been called? If you want state, wouldn't you be better off using an object? – James Jun 26 '12 at 20:30
  • Nevertheless, convention dictates that only a single underscore be used to declare a private variable. The double underscore should only be used when you don't want a subclass to inherit the property or in the case of special methods (`__eq__` etc.). – Chinmay Kanchi Jun 27 '12 at 22:15

4 Answers4

5

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.

Mike Graham
  • 73,987
  • 14
  • 101
  • 130
  • one more question: How is `self.__f = f` different from `self.f=f`? I know that the `__` converts it to a private variable but is that what is happening here? – ritratt Jun 27 '12 at 03:54
  • 1
    Attributes that are named like `__something` are name-mangled, meaning then when stored, rather than being "__f"`, they are actually stored as `_countcalls__f`. It doesn't make them actually private, just spells them a bit different. This feature is not very often useful and can make code harder to work with, so many of us avoid it altogether. – Mike Graham Jun 27 '12 at 04:08
  • pardon me, i got one more doubt. suppose we use a function as decorator instead of a class. Then it takes the OBJECT of the function it is decorating right? Now, if it's an object I should be able to access its variables,attributes etc. Does this mean I can access a variable declared inside the function to be decorated from the decorator function? – ritratt Jun 28 '12 at 16:37
  • 1
    In both cases, the decorator receives the function object. You can access the attributes of the function but not the variables inside the function in any meaningful way. You can access the arguments, defaults, and code at a low level, but not anything that is very often useful. Remember, those variables don't refer to objects until the function is called, and it isn't called when you get the object. – Mike Graham Jun 28 '12 at 18:09
3

When we prefix the decorator to a function, does that mean that we are creating an object of the decorator class right there?

There's a very easy way to find out.

>>> @countcalls
... def f(): pass
>>> f
<a.countcalls object at 0x3175310>
>>> isinstance(f, countcalls)
True

So, yes.

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

Almost. It's equivalent to that and then assigning the result to the function's name, i.e.

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

How can I get better at decorators?

  1. Practice
  2. Read other people's code that uses them
  3. Study the relevant PEPs
Fred Foo
  • 355,277
  • 75
  • 744
  • 836
2
  1. It is equivalent to the following:

    f = countcalls(f)

    In other words: yes, we are creating a countcalls object and passing f to its constructor.

  2. These are the function arguments. In your case, f takes no arguments but suppose f was called like this:

    f(3, 4, keyword=5)

    then *args would contain 3 and 4 and **kwargs would contain the key/value pair keyword=5. For more information about *args and **kwargs, see this question.

  3. Practice makes perfect.

Community
  • 1
  • 1
Simeon Visser
  • 118,920
  • 18
  • 185
  • 180
0
  1. Yes, with the caveat that it also binds that object to the name given in the def.

  2. Read about them, read examples, play with some.

Marcin
  • 48,559
  • 18
  • 128
  • 201