3

I'm very new to Python and I don't understand how functions themselves can seemingly have attributes. In the code below, there is a function called f, and later in the code, something by the name of f.count is referenced. How can a function, namely f, have a .count? I'm getting an error message of: 'NoneType' object has no attribute 'count' on that line, so it obviously doesn't have that attribute yet. How do I give it that attribute?

def fcount(n):
    print n.__name__


@fcount
def f(n):
    return n+2

for n in range(5):
    print n
    #print f(n)

print 'f count =',f.count #THE LINE CAUSING THE ERROR MENTIONED ABOVE

@fcount
def g(n):
    return n*n

print 'g count =',g.count
print g(3)
print 'g count =',g.count

Edit: Added fcount(), which doesn't do anything much, and details about error.

Jamie Taylor
  • 4,709
  • 5
  • 44
  • 66
user3402743
  • 553
  • 2
  • 8
  • 12
  • "on that line" on which line? – zhangxaochen Mar 10 '14 at 17:19
  • 1
    Before "that line", What is ``fcount``? – YS-L Mar 10 '14 at 17:20
  • 1
    `'NoneType' object has no attribute 'count'` means `f` or `g` is `None`, not the attribute. Python functions are objects; they can have attributes just like many other object types. – Martijn Pieters Mar 10 '14 at 17:22
  • Added some details to answer those questions. – user3402743 Mar 10 '14 at 17:23
  • Is this just a roundabout way of asking, "how do I construct a decorator that counts how many times a function has been called, and how do I access that number?"? – Kevin Mar 10 '14 at 17:23
  • Actually, given that `fcount` definition, the `f(n)` call in the loop should already cause an error (NoneType object is not callable). Please post a *complete* program that actually displays the error you see. –  Mar 10 '14 at 17:23
  • @Kevin, yes, that is the ultimate objective, but I want to understand how it works, too. – user3402743 Mar 10 '14 at 17:25
  • @delnan, made a slight change, so it will actually display the error I see. – user3402743 Mar 10 '14 at 17:26

3 Answers3

5

Let’s start with the definition of f:

@fcount
def f(n):
    return n+2

This defines a f as the return value of a call to the function fcount, which is used as a decorator (the leading @) here.

This code is roughly equivalent with

def f(n):
    return n+2

f = fcount(f)

since the decorator – fcount – does not return anything, f is None and not a function at the call site.

In your case fcount should return some function and add a count attribute to that returned function. Something useful (?) might be

def fcount(fn):
    def wrapper(n):
        wrapper.count += 1
        return fn(n)
    wrapper.count = 0
    return wrapper

EDIT

As @jonrsharpe pointed out, a generalized decorator can forward positional and keyword arguments by capturing them with *args and **kwargs in the signature and expanding them in the same way when calling another function. The names args and kwargs are used by convention.

Python also has a helper function (a decorator itself) that can transfer information (name, docstring and signature information) from one function to another: functools.wraps. A complete example looks like this:

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@decorator
def f(a, b, c=None):
   "The useful f function"
   pass

print f.__name__ # `f` rather than `wrapper`
print help(f) # `f(*args, **kwargs) The useful f function` rather than `wrapper(*args, **kwargs)`
David Aurelio
  • 484
  • 2
  • 11
4

The problem here is with your "decorator function", fcount. A Python decorator function should return a function:

@decorates
def func(...):
    ...

is effectively:

func = decorates(func)

In your case, the "decorator" fcount only prints, and won't return anything; hence using it will assign f = None.

The answer to your more general question is that functions, being Python objects like more-or-less everything else, certainly can have attributes. To actually implement what you want, a decorator that counts how many times the decorated function is called, you could do:

def fcount(f):
    """Decorator function to count how many times f is called."""
    def func(*args, **kwargs):
        func.count += 1
        return f(*args, **kwargs)
    func.count = 0
    return func
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • Oh, so the decorator function has to return a function, which then becomes the value of the function that called the decorator. And you make another function inside the decorator function, with the *args and **kwargs, that does the actual work you want done, in this case, incrementing a counter? – user3402743 Mar 10 '14 at 17:47
  • That's right - you create a new function (`func`) inside the decorator function (`fcount`), which in turn (usually) calls the argument function (`f`) with whatever arguments it is passed (`args, kwargs`). This new function is returned by the decorator to replace the original function. – jonrsharpe Mar 10 '14 at 17:53
  • What if I wanted to put it in a class, rather than a standalone function? Would I just put the whole thing directly inside a header like: class fcount2(object): – user3402743 Mar 10 '14 at 20:47
  • I'm not sure exactly what you're asking. You can decorate class or instance methods, but the decorator would still be a standalone function, not itself a class. – jonrsharpe Mar 10 '14 at 20:51
  • Yeah, I guess I was asking if classes can be decorated, too, or just functions. – user3402743 Mar 10 '14 at 21:02
  • On the same basis, you can create a function that takes a class argument and returns a class; see [this question](http://stackoverflow.com/questions/681953/python-class-decorator) for more. – jonrsharpe Mar 10 '14 at 21:08
0

Python functions are first-class objects.

They can have attributes.

This feature is rarely seen in the literature, but it has its uses, such as a simplified closure.

By the way, the kind of counter code you asked about is explained in a Pycon 2014 video about decorators.

Reference

zx81
  • 41,100
  • 9
  • 89
  • 105