Home > Mobile >  Python-setattr pass function with args
Python-setattr pass function with args

Time:12-02

I'm trying to set methods of a class programmatically by calling setattr in a loop, but the reference I pass to the function that is called by this method defaults back to its last value, instead of what was passed at the time of the setattrcall. Curiously, I'm also setting the __doc__ attribute and this assignment actually works as expected:

class Foo2:

    def do_this(self, pass_this: str):
        print(pass_this)


class Foo:

    def __init__(self):
        self.reference = "ti-hihi"
        self.foo2 = Foo2()
        for (method_name, pass_this) in [("bar", "passed-for-bar"), ("bar2", "passed-for-bar2")]:
            my_doc = f"""my_custom_docstring: {pass_this}"""

            def some_func():
                self.foo2.do_this(pass_this=pass_this)
            some_func.__doc__ = my_doc
            setattr(self, method_name, some_func)


if __name__ == '__main__':
    f = Foo()
    f.bar()  # prints "pass-for-bar2" instead of "pass-for-bar"
    f.bar.__doc__  # prints "pass-for-bar" as expected

I already tried a few things but couldn't figure it out.

Things I tried:

lambda -- my best bet, tbh

def some_func(reference):
    self.foo2.do_this(pass_this=reference)
some_func.__doc__ = my_doc
setattr(self, method_name, lambda: some_func(pass_this))

deepcopy

import copy

def some_func():
    self.foo2.do_this(pass_this=copy.deepcopy(pass_this))
some_func.__doc__ = my_doc
setattr(self, method_name, some_func)

another deepcopy variant which feels dangerous if I think about the place I want to put this:

import copy

def some_func():
    self.foo2.do_this(pass_this=pass_this)
some_func.__doc__ = my_doc
setattr(self, method_name, copy.deepcopy(some_func))

... and a few combinations of those but I'm missing some crucial piece.

CodePudding user response:

Methods are class attributes, so some_func needs to be attached to type(self), not self itself.

class Foo:

    def __init__(self):
        self.reference = "ti-hihi"
        self.foo2 = Foo2()
        for (method_name, pass_this) in [("bar", "passed-for-bar"), ("bar2", "passed-for-bar2")]:
            my_doc = f"""my_custom_docstring: {pass_this}"""

            def some_func(self):
                self.foo2.do_this(pass_this=pass_this)
            some_func.__doc__ = my_doc
            setattr(type(self), method_name, some_func)

As such, the __init_ method is not really the proper place to do this, as you'll be repeatedly attaching the (effectively same) methods to the class every time you instantiate the class. A class decorator would be more appropriate:

def add_methods(cls):
    for (method_name, pass_this) in [("bar", "passed-for-bar"), ("bar2", "passed-for-bar2")]:
        my_doc = ...
        def some_func(self):
            self.foo2.do_this(pass_this=pass_this)
        some_func.__doc__ = my_doc
        setattr(cls, method_name, some_func)
    return cls


@add_methods
class Foo:
    def __init__(self):
        self.reference = "ti-hihi"
        self.foo2 = Foo2()

add_methods doesn't really require any special information about Foo. Indeed, add_methods can be applied to any class that

  1. Defines a foo2 attribute for its instances, and
  2. foo2 has a type that provides an appropriate do_this method.

Even that assumption can be made more explicit by defining a suitable base class.

def FooBase:
    def __init__(self):
        self.foo2 = Foo2()

@add_methods
def Foo(FooBase):
    pass

Now you can simply state that add_methods is designed to work with subclass of FooBase.

CodePudding user response:

Thanks to @AndrewAllaire for this tip, using functools.partial solved it for me

some_func = partial(self.foo2.do_this, pass_this=pass_this)
  • Related