1

I am trying to add class members to a class based on another, and I'm running into some issues with the loop. All of the functions assigned wind up calling the last function found.

import inspect
from pprint import pp
from functools import wraps


def action(f):
    f.action = True
    return f


class x1:
    @action
    def func1(self):
        return 1

    def func2(self):
        return 2

    @action
    def func3(self):
        return 3


class x2:
    pass


funcs = inspect.getmembers(x1, predicate=inspect.isfunction)

for fn, f in funcs:
    if getattr(f, 'action', False):
        @wraps(f)
        def call(self):
            print(fn)
            return f(self)
        setattr(x2, fn, call)

a = x2()

pp({
    'funcs': [a.func1, a.func3],
    'func1': a.func1(),
    'func3': a.func3()
})

Output is:

func3
func3
{'funcs': [<bound method x1.func1 of <__main__.x2 object at 0x7f33eade1fd0>>,
           <bound method x1.func3 of <__main__.x2 object at 0x7f33eade1fd0>>],
 'func1': 3,
 'func3': 3}

...when obviously func1 should return 1, not 3.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
matt
  • 43
  • 4

2 Answers2

1

Rather than tagging functions with a boolean attribute, I would just collected the decorated functions in a list. You can then attach the functions directly to x2, rather than messing around with inspect or wrappers.

from pprint import pp


def action(lst):
    def _(f):
        lst.append(f)
        return f
    return _


class x1:
    shared = []

    @action(shared)
    def func1(self):
        return 1

    @action(shared)
    def func2(self):
        return 2

    @action(shared)
    def func3(self):
        return 3


class x2:
    pass


for f in x1.shared:
    setattr(x2, f.__name__, f)
    

a = x2()

pp({
    'funcs': [a.func1, a.func3],
    'func1': a.func1(),
    'func3': a.func3()
})

Your problem is that f in call is a free variable, not the current value of f in the loop defining the functions. Every call refers to whatever f was last set to, in this case func3.

chepner
  • 497,756
  • 71
  • 530
  • 681
1

The problem is that call prints name fn and executes the name f from the global scope, which, at the end of your loop, are 'func3' and func3:

def call(self):
    print(fn)
    return f(self)

The are a couple of ways to fix that. A popular method is too create a closure to preserve the right references in the enclosing scope:

def get_f(f):
    @wraps(f)
    def call(self):
        print(f.__name__)
        return f(self)
    return call
for fn, f in funcs:
    if getattr(f, 'action', False):
        setattr(x2, fn, get_f(f))

You could stash the reference in any other number of ways, for example:

def call(self, f=f):
    print(f.__name__)
    return f(self)

Just don't call a name in the global scope that you're using as a loop variable.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264