1

I know Python supports object-oriented structure, which uses dot notation. However, I feel confused about the code below where dot notation appears in a function definition with no class defined. Is that some feature defined as function attributes [I guess] in Python?

def count(f):
    def counted(*args):
        counted.call_count += 1
            return f(*args)
        counted.call_count = 0
        return counted

The second question: is there an alternative that the code above could be rewritten using the nonlocal statement instead of the dot notation to record the call_count?

H.Li
  • 33
  • 6
  • Yes. Functions are also objects that can have attributes – rdas Nov 21 '20 at 18:15
  • Function attributes were added with [PEP 232](https://www.python.org/dev/peps/pep-0232/) and function the same as any class. To use nonlocal, simply create a `call_count` variable and put `nonlocal call_count` at the beginning of the `counted` function. – rassar Nov 21 '20 at 18:16
  • Cool, this is helpful, thank both of you! – H.Li Nov 21 '20 at 21:41

2 Answers2

3

A closure would be more robust than a function attribute. You could, conceivably, bind counted to something else, and then counted.call_count would no longer be the attribute you want.

def count(f):
    call_count = 0
    def counted(*args):
        nonlocal call_count
        call_count += 1
        return f(*args)
    return counted

Each time count is called, a new variable call_count is created. After count returns, the only reference to this variable is inside the body of counted, rather than being (easily) visible to anything that has a reference to counted.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I am having trouble in fetching the call_count information after calling count(f). Could I know how to get call_count after the execution? – H.Li Nov 21 '20 at 18:35
0

If you as the caller didn't need direct access to call_count (because, say, it was only used internally by the decorator), what @chepner shows would be the way to go.

The problem with it is, call_count isn't accessible from the outside. Some options:

  1. Keep it as a attribute of the function.

    • This isn't a great idea though, since there's only one function object shared everywhere. If you wanted to count the calls of the same function in two separate places independently, you'd have problems.
  2. Pass is a mutable container that gets mutated by the decorator:

    def count(count_container):
        count_container[0] = 0
    
        def inner(f):
            def counted(*args):
                count_container[0] += 1
                return f(*args)
            return counted
        return inner
    
    
    l = [0]
    
    
    @count(l)
    def f():
        print("Test")
    
    >>> l
    [0]
    >>> f()
    Test
    >>> f()
    Test
    >>> l
    [2]
    

    Of course, the downside of this is, eww. Even if you replaced the list with a specialized object like a custom dataclass, this would still be bad.

  3. Abandon the idea of using it as a decorator using @ notation. That gives you a few more options.

    • You can keep your current code, and pass in the function manually:

       def count(f):
           def counted(*args):
               counted.call_count += 1
               return f(*args)
           counted.call_count = 0
           return counted
      
       >>> counted = count(f)
       >>> counted()
       Test
       >>> counted()
       Test
       >>> counted.call_count
       2
      

      This is much better, because each explicit call to count returns a new function, instead of the single global function being given the attribute like before. If you wanted to track two separate instances of calls at once, you would just need to call count twice, then hold onto each returned function.

    • You could also return a getter. Using a modified version of @chepner's code:

      def count(f):
          call_count = 0
          def counted(*args):
              nonlocal call_count
              call_count += 1
              return f(*args)
          return counted, lambda: call_count
      
      >>> counted, counter_getter = count(f)
      >>> counted()
      Test
      >>> counted()
      Test
      >>> counter_getter()
      2
      

      The major benefit here is it gives the caller access to call_count when they want to read it, but it does not give them the ability to modify it.


Sorry if I fubared some indentation somewhere. It is incredibly frustrating trying to format code inside of a nested point list.

Carcigenicate
  • 43,494
  • 9
  • 68
  • 117
  • I have tried the two different ways in your item 3 and both of them worked. The nonlocal one with a tuple of two functions as the return looks fun and it solved my problem. I don't have any trouble reading your code. Thanks a lot! – H.Li Nov 21 '20 at 23:16