Accessing function attribute created in a decorato

2019-07-14 07:09发布

问题:

I want to count the number of times a given function has been called.

So, I made a countcalls decorator to give my functions a __callcount attribute which gets incremented on each call. Simple enough.

My issue is getting the __callcount value back out later.

Here's my code:

import functools

def countcalls(f):
    f.__callcount = 0

    @functools.wraps(f)
    def _countcalls(*args, **kwds):
        f.__callcount += 1
        print('  Called {0} time(s).'.format(f.__callcount))
        return f(*args, **kwds)
    return _countcalls

@countcalls
def fib(n):
    if n < 0:
        raise ValueError('n must be > 0')
    if n == 0 or n == 1:
        return 1

    return fib(n-1) + fib(n-2)

if __name__ == '__main__':
    print('Calling fib(3)...')
    x = fib(3)
    print('fib(3) = {0}'.format(x))

    print('Calling fib(3) again...')
    x = fib(3)
    print('fib(3) = {0}'.format(x))

    print('fib was called a total of {0} time(s).'.format(fib.__callcount)) 

Which generates the following output (Python v3.3.0):

Calling fib(3)...
  Called 1 time(s).
  Called 2 time(s).
  Called 3 time(s).
  Called 4 time(s).
  Called 5 time(s).
fib(3) = 3
Calling fib(3) again...
  Called 6 time(s).
  Called 7 time(s).
  Called 8 time(s).
  Called 9 time(s).
  Called 10 time(s).
fib(3) = 3
fib was called a total of 0 time(s).

Why does fib.__callcount equal 0 on the last line? As the output shows, __callcount gets incremented, and persists between calls of fib.

What am I missing?

回答1:

f.__callcount = [0]

..........


f.__callcount[0] = f.__callcount[0] + 1

......


print('fib was called a total of {0} time(s).'.format(fib.__callcount[0]))

It works.
Maybe ther's something more pythonic



回答2:

This does what you want. I got it here- https://wiki.python.org/moin/PythonDecoratorLibrary#Alternate_Counting_function_calls

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
      self.__doc__ = f.func_doc
      self.__name__ = f.func.func_name

   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

   @staticmethod
   def counts():
      "Return a dict of {function: # of calls} for all registered functions."
      return dict([(f.__name__, countcalls.__instances[f].__numcalls) for f in countcalls.__instances])

@countcalls
def fib(n):
    if n < 0:
        raise ValueError('n must be > 0')
    if n == 0 or n == 1:
        return 1

    return fib(n-1) + fib(n-2)

if __name__ == '__main__':
    print('Calling fib(3)...')
    x = fib(3)
    print('fib(3) = {0}'.format(x))
    print('fib was called a total of {0} time(s).'.format(fib.count())) 

    print('Calling fib(3) again...')
    x = fib(3)
    print('fib(3) = {0}'.format(x))

    print('fib was called a total of {0} time(s).'.format(fib.count())) 


回答3:

The function object you are adding an attribute to is a different object than the 'original' function. Try this:

import functools

def countcalls(f):
    f.__callcount = 0

    @functools.wraps(f)
    def _countcalls(*args, **kwds):
        f.__callcount += 1
        print 'id(f):', id(f)
        print('  Called {0} time(s).'.format(f.__callcount))
        return f(*args, **kwds)
    return _countcalls


@countcalls
def fib(n):
    """fibinacci"""
    if n < 0:
        raise ValueError('n must be > 0')
    if n == 0 or n == 1:
        return 1

    return fib(n-1) + fib(n-2)


if __name__ == '__main__':
    print('Calling fib(3)...')
    x = fib(3)
    print 'id(fib):', id(fib)

"""
>>> 
Calling fib(3)...
id(f): 45611952
  Called 1 time(s).
id(f): 45611952
  Called 2 time(s).
id(f): 45611952
  Called 3 time(s).
id(f): 45611952
  Called 4 time(s).
id(f): 45611952
  Called 5 time(s).
id(fib): 45612016
>>>
"""


回答4:

Well, here's the reason, after a bit of help. Thanks guys!

The issue is that functions are immutable. e.g.

>>> def f(func):
...     return func()
...
>>> def g():
...     return 'sunflower seeds'
...
>>> id(g)
139636515497336
>>> g = f(g)
>>> id(g)
139636515515112

So, the only way to get the function f we assigned the __callcount attribute to in the definition of countcalls is to return that function from callcount. But we're already returning the inner function _countcalls. We can return both f and _countcalls but that messes up the @countcalls decorator syntax.

You can still do it this way, it's just not as pretty.

import functools

def countcalls(f):
    f.__callcount = 0

    @functools.wraps(f)
    def _countcalls(*args, **kwds):
        f.__callcount += 1
        print('  Called {0} time(s).'.format(f.__callcount))
        return f(*args, **kwds)
    return f, _countcalls

def fib(n):
    if n < 0:
        raise ValueError('n must be > 0')
    if n == 0 or n == 1:
        return 1

    return fib(n-1) + fib(n-2)

if __name__ == '__main__':
    counter, fib = countcalls(fib)

    print('Calling fib(3)...')
    x = fib(3)
    print('fib(3) = {0}'.format(x))

    print('Calling fib(3) again...')
    x = fib(3)
    print('fib(3) = {0}'.format(x))

    print('fib was called a total of {0} time(s).'.format(counter.__callcount))

Long story short, just use the class from the Python Decorator Library. :D