0

I'm trying to find a way to dynamically add methods to a class through decorator. The decorator i have look like:

def deco(target):

    def decorator(function):
        @wraps(function)
        def wrapper(self, *args, **kwargs):
            return function(*args, id=self.id, **kwargs)

        setattr(target, function.__name__, wrapper)
        return function

    return decorator

class A:
    pass

# in another module
@deco(A)
def compute(id: str):
    return do_compute(id)

# in another module
@deco(A)
def compute2(id: str):
    return do_compute2(id)

# **in another module**
a = A()
a.compute() # this should work
a.compute2() # this should work

My hope is the decorator should add the compute() function to class A, any object of A should have the compute() method. However, in my test, this only works if i explicitly import compute into where an object of A is created. I think i'm missing something obvious, but don't know how to fix it. appreciate any help!

Hanlinl
  • 3
  • 2
  • What, exactly, should `a.compute()` return? You aren't providing a `str` argument. – chepner Sep 23 '20 at 21:48
  • That is, should `a.compute("foo")` return `"foo"`? – chepner Sep 23 '20 at 21:49
  • Is there a reason this has to be a decorator? My first thought is to do something like def compute(self, id): return compute2(id) A.compute = compute You could also just define the method on A directly. Either way, you'll need to make sure the code altering A's definition is called before A gets instantiated. – Sarah Messer Sep 23 '20 at 21:50
  • `deco` itself should probably return `target`, not `decorator`. – chepner Sep 23 '20 at 21:51
  • BTW, `def compute(id: str): return compute(id)` is most likely to cause an infinite recursion as soon as `compute` is called – DeepSpace Sep 23 '20 at 22:03
  • @SarahMesser thanks, how can i make sure the decorator code is called before A gets instantiated? In my experiment, the above code work fine when i explicitly import both A and compute() into the module where `a` is defined, but if i just import the class A the decorator change is not applied. – Hanlinl Sep 23 '20 at 22:13
  • @chepner a.compute() could just always return 1. In my initial post i made it look like compute() is calling into itself, i have updated that. – Hanlinl Sep 23 '20 at 22:17
  • @DeepSpace, i didn't mean to have compute() call itself. i have updated the post – Hanlinl Sep 23 '20 at 22:25
  • 1
    By "importing compute()" do you mean importing the module where you have the `@deco(A)` code? Without importing this module, of course `deco` will never get called and the decorator change never applied – Phu Ngo Sep 24 '20 at 05:31
  • 1
    I'm not sure the decorator serves any interesting purpose. You can just write `A.compute = compute` after the function definition to accomplish the same thing. Python 3.9 will also let you write `@lambda x: setattr(A, x.__name__, x)` in place of `@deco(A)`. – chepner Sep 24 '20 at 11:40
  • Importing a module runs it. That's why you have to import the module where `compute()` is defined and decorated. Larger projects often have a "utilities" package / module which takes care of gathering up all the "we use these nearly every time, but don't want to track them individually" imports. Then you can import utilities which imports the individual dependencies. – Sarah Messer Sep 24 '20 at 14:20

1 Answers1

0

I think this will be quite simpler using a decorator implemented as a class:

class deco:
    def __init__(self, cls):
        self.cls = cls

    def __call__(self, f):
        setattr(self.cls, f.__name__, f)
        return self.cls

class A:
    def __init__(self, val):
        self.val = val

@deco(A)
def compute(a_instance):
    print(a_instance.val)


A(1).compute()
A(2).compute()

outputs

1
2

But just because you can do it does not mean you should. This can become a debugging nightmare, and will probably give a hard time to any static code analyser or linter (PyCharm for example "complains" with Unresolved attribute reference 'compute' for class 'A')


Why doesn't it work out of the box when we split it to different modules (more specifically, when compute is defined in another module)?

Assume the following:

a.py

print('importing deco and A')

class deco:
    def __init__(self, cls):
        self.cls = cls

    def __call__(self, f):
        setattr(self.cls, f.__name__, f)
        return self.cls

class A:
    def __init__(self, val):
        self.val = val

b.py

print('defining compute')

from a import A, deco

@deco(A)
def compute(a_instance):
    print(a_instance.val)

main.py

from a import A

print('running main')

A(1).compute()
A(2).compute()

If we execute main.py we get the following:

importing deco and A
running main
Traceback (most recent call last):
    A(1).compute()
AttributeError: 'A' object has no attribute 'compute'

Something is missing. defining compute is not outputted. Even worse, compute is never defined, let alone getting bound to A.

Why? because nothing triggered the execution of b.py. Just because it sits there does not mean it gets executed.

We can force its execution by importing it. Feels kind of abusive to me, but it works because importing a file has a side-effect: it executes every piece of code that is not guarded by if __name__ == '__main__, much like importing a module executes its __init__.py file.

main.py

from a import A
import b

print('running main')

A(1).compute()
A(2).compute()

outputs

importing deco and A
defining compute
running main
1
2
DeepSpace
  • 78,697
  • 11
  • 109
  • 154
  • thanks @DeepSapce, I tried to make it as a class, but i'm having similar issue. If i move ```@deco(A) def compute(a_instance): print(a_instance.val)``` and ```A(1).compute()``` each in a separate module, the method is not available for A(1).compute() – Hanlinl Sep 23 '20 at 22:24
  • @Hanlinl I updated my answer with a thorough explanation – DeepSpace Sep 23 '20 at 22:44
  • thanks for the thorough explanation, i understand why it didn't work now. Is there a way to avoid the explicit import? The purpose of this decorator is to freely attach functions into target class, if i need to import functions from all the places i annotated in order to use A, it will not be very useful – Hanlinl Sep 23 '20 at 22:51
  • @Hanlinl No, there isn't, not without ugly hacks. If you didn't feel it yet, you should avoid doing this. Any hack you will find to make this work will be much harder than simply defining the method in the class, or once in a parent class which other classes can subclass – DeepSpace Sep 23 '20 at 22:54