Strange AttributeError only when assignment

Viewed 127

I'm implementing the decorator described in What is the Python equivalent of static variables inside a function?. In the answer, the decorator is put on a normal function and it worked also in my environment.

Now I'd like to put the decorator onto a class method.

Source Code:

#decorator
def static_variabls(**kwargs):
    def new_func(old_func):
        for key in kwargs:
            setattr(old_func, key, kwargs[key])
        return old_func
    return new_func

class C:
    @static_variabls(counter = 1)
    def f_(self) -> None:
        print(self.f_.counter)
        self.f_.counter += 1

c1 = C()
c1.f_()
c1.f_()
c1.f_()

Expected Result:

1
2
3

Actual Result:

1
Traceback (most recent call last):
  File "1.py", line 16, in <module>
    c1.f_()
  File "1.py", line 13, in f_
    self.f_.counter += 1
AttributeError: 'method' object has no attribute 'counter'

I don't understand why this code doesn't work. According to the error message, self.f_.counter doesn't exist but print(self.f_.counter) works. What's happening here?

2 Answers

c1.f_() is equivalent to C.f_.__get__(c1, C)() (due to the descriptor protocol). __get__ returns the method object that actually gets called. You attached counter to the original function object, which isn't what the method wraps: it wraps the function created by def new_func.

Note that you have the same problem with a much simpler decorator, that hard-codes the attribute and its initial value.

def add_counter(f):
    f.counter = 1
    return f


class C:
    @add_counter
    def f_(self) -> None:
        print(self.f_.counter)
        self.f_.counter += 1

or even with no decorator:

class C:
    def f_(self) -> None:
        print(self.f_.counter)
        self.f_.counter += 1

    f_.counter = 1

You can't define/use, variable, function or an objects, as a static member, and then use self to access them.

In case of self.f_.counter, I guess print() function works, as, it tries to access memory address (of the class itself) directly, in your code f_.counter, not binded with self address., it's binded with ,C.f_ adderss itself.

Solution - 1

def static_variabls(**kwargs):
  
  def new_func(old_func):
    
    for key in kwargs:
      setattr(old_func, key, kwargs[key])

    return old_func

  return new_func

class C:
    @static_variabls(counter = 1)
    def f_(self) -> None:
        print(C.f_.counter) # OR self.f_.counter
        C.f_.counter += 1

Solution - 2 (Without using decorators)

class C:
  
  counter = 0 # static variable

  def __init__(self):
    C.counter += 1
    print(self.counter)

c1 = C()
c1 = C()
c1 = C()
Related