简体   繁体   中英

Declaring decorator method inside of a mixin class

I have written a class like so:

class AbstractBigHandler(AbstractHandler, ActionTimer):
    def __init__(self, path, data_container):
        
        AbstractHandler.__init__(self)
        ActionTimer.__init__(self, data_container)

    @time_action
    def handle(self, request):
        # Do something

I wanted to time handle() method in it, so I came up with an idea to write a mixin class, which would provide a decorator class in it, as in my mind it would provide a nice separation of concerns in the code. Moreover, I wanted this mixin class to be reusable for other classes I will write later:

class ActionTimer:

    def __init__(self, data_container):
        self.data_container = data_container
        self.class_name = self.__class__.__name__

    def time_action(self, func):
        def wrapper(self, func, *args, **kwargs):
            start_time = datetime.now()
            func_output = func.handle(args, kwargs)
            end_time = datetime.now()

            execution_time = end_time - start_time
            print(self.__class__.__name__, " execution time: ", execution_time)

            self.data_container.add_data(self.class_name, execution_time)

            return func_output

        return wrapper

The data_container argument in it, is just a singleton I intend to have floating around, and collect data in a its dictionary attribute (through the .add_data() method).

Now, when I try to run tests for the AbstractBigHandler class, it fails immediately on instantiation with the following error stack:

C:\project\tests>python -m unittest test_abstract_big_handler
Traceback (most recent call last):
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\unittest\__main__.py", line 18, in <module>
    main(module=None)
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\unittest\main.py", line 100, in __init__
    self.parseArgs(argv)
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\unittest\main.py", line 147, in parseArgs
    self.createTests()
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\unittest\main.py", line 159, in createTests
    self.module)
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\unittest\loader.py", line 220, in loadTestsFromNames
    suites = [self.loadTestsFromName(name, module) for name in names]
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\unittest\loader.py", line 220, in <listcomp>
    suites = [self.loadTestsFromName(name, module) for name in names]
  File "C:\Users\***\AppData\Local\Continuum\anaconda3\lib\unittest\loader.py", line 154, in loadTestsFromName
    module = __import__(module_name)
  File "C:\project\tests\test_abstract_big_handler.py", line 5, in <module>
    from project.common.handlers.AbstractBigHandler import AbstractBigHandler
  File "C:\project\common\handlers\AbstractBigHandler.py", line 17, in <module>
    class AbstractBigHandler(AbstractHandler, ActionTimer):
  File "C:\project\common\handlers\AbstractBigHandler.py", line 69, in AbstractBigHandler
    @time_action
NameError: name 'time_action' is not defined

I am not sure why this is happening - since ActionTimer class is added as a second inherited class in AbstractBigHandler class, its method time_action() should be (and is, when I check with PDB) present in the methods of the AbstractBigHandler.

Help?

Because time_action is the decorator, you don't need to pass func again in wrapped . Since the container is Singleton, you can employ classmethod and reference it directly.

from datetime import datetime
import random


class AbstractHandler(object):
    pass


class ActionTimer(object):
    data_container = []

    def __init__(self, data_container):
        self.data_container = data_container
        self.class_name = self.__class__.__name__

    @classmethod
    def time_action(cls, func):
        def wrapper(*args, **kwargs):
            start_time = datetime.now()
            func_output = func(*args, **kwargs)
            end_time = datetime.now()
            execution_time = end_time - start_time

            # just some random value to show results (remove)
            execution_time = execution_time.total_seconds() + random.randrange(1, 10, 1)

            print(cls.__name__, " execution time: ", execution_time)

            cls.data_container.append((cls.__name__, execution_time))

            return func_output

        return wrapper


class AbstractBigHandler(AbstractHandler, ActionTimer):
    def __init__(self, path, data_container):
        AbstractHandler.__init__(self)
        ActionTimer.__init__(self, data_container)

    @classmethod
    @ActionTimer.time_action
    def handle(cls, request):
        print(request, "CONTAINER:", cls.data_container)


a = AbstractBigHandler("path", [])
for i in range(5):
    a.handle("some-request-" + str(i))
print("FINAL:", ActionTimer.data_container)

Produces the following:

some-request-0 CONTAINER: []
ActionTimer  execution time:  9.0
some-request-1 CONTAINER: [('ActionTimer', 9.0)]
ActionTimer  execution time:  6.0
some-request-2 CONTAINER: [('ActionTimer', 9.0), ('ActionTimer', 6.0)]
ActionTimer  execution time:  9.0
some-request-3 CONTAINER: [('ActionTimer', 9.0), ('ActionTimer', 6.0), ('ActionTimer', 9.0)]
ActionTimer  execution time:  3.0
some-request-4 CONTAINER: [('ActionTimer', 9.0), ('ActionTimer', 6.0), ('ActionTimer', 9.0), ('ActionTimer', 3.0)]
ActionTimer  execution time:  5.0
FINAL: [('ActionTimer', 9.0), ('ActionTimer', 6.0), ('ActionTimer', 9.0), ('ActionTimer', 3.0), ('ActionTimer', 5.0)]

Unfortunately, inherited namespaces are handled only after a class body has been fully defined. There's no easy way for a child class like AbstractBigHandler to reference attributes of its parent classes at class definition time. A much simpler example of the issue would be:

class A:
    foo = 1

class B(A):         # inherit the foo class attribute
    bar = foo + 1   # but looking it up here fails, with the same NameError you're seeing

To fix this, you'll either need to reference the parent class explicitly (eg A.foo from within B ) or move the value you wanted to inherit out of the parent class to be a global variable instead. This often is the best approach for decorators that need to be applied at definition time of the child class, not when there are instances to be passed as self to a method.

Indeed, this is the second issue I see with your code, that your decorator has some messed up arguments (both in the decorator itself, and in the wrapper function inside). I'd suggest a structure like this:

class ActionTimer:
    def __init__(self, data_container):
        self.data_container = data_container
        self.class_name = self.__class__.__name__

def time_action(func):                     # top-level function, no self arg here
    def wrapper(self, *args, **kwargs):    # no func arg here, but we *do* have a self arg
        start_time = datetime.now()
        func_output = func(self, args, kwargs)   # don't call handle, *we're* handle!
        end_time = datetime.now()

        execution_time = end_time - start_time
        print(self.__class__.__name__, " execution time: ", execution_time)

        self.data_container.add_data(self.class_name, execution_time)

        return func_output

    return wrapper

Now the AbstractBigHandler class will work as you've written it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM