pythonpython-decoratorsclass-methodatexit

Register class method at program exit using chain of decorators


I have a class, one of its attributes is a class method that I want to run at program exit. This immediate idea:

import atexit

class Foo:
    @atexit.register
    @classmethod
    def foo(cls):
        pass

raises the following exception:

Traceback (most recent call last):
  File "test.py", line 3, in <module>
    class Foo:
  File "test.py", line 5, in Foo
    @classmethod
TypeError: the first argument must be callable

Another immediate idea (notice the changed evaluation order of decorators):

import atexit

class Foo:
    @classmethod
    @atexit.register
    def foo(cls):
        pass

raises the following exception:

Error in atexit._run_exitfuncs:
TypeError: foo() missing 1 required positional argument: 'cls'

I am quite new to the concept of decorators.


Solution

  • First you should read the answer for How does a classmethod object work?

    But it would be interesting to see the output of the following code:

    def mydeco(func):
        print(repr(func))
        return func
    
    class Foo:
        @mydeco
        @classmethod
        def foo(cls):
            pass
    
    print(repr(Foo.foo))
    

    If you run this, you will see

    <classmethod object at 0x6ffffd1dc18>
    <bound method Foo.foo of <class '__main__.Foo'>>
    

    So the first line is from the mydeco() decorator and the second line is from the print() statement at the bottom. You see that they are different. The reason they are different is because classmethod is not like the function decorator you expect. It does not give you back a function, but a classmethod object, and it is not callable. However, at the same time, the class Foo that encloses it remember that in Foo.__dict__ and when you recall the decorated class method, it give you back a callable method.

    Just because the classmethod object is not callable. You cannot wrap it with atexit.register. Similar case for staticmethod.

    So now you should realize, to register that into atexit, you can do it outside the class, like this:

    import atexit
    
    class Foo:
        @classmethod
        def foo(cls):
            pass
    
    atexit.register(Foo.foo)