python setattr for dynamic method creator with dec

2020-07-22 03:02发布

问题:

I have a class which has multiple methods defined.

import mat
class Klass(object):

    @mat.sell(mat.CanSet):
    def method1(self):
        return None

    @mat.sell(mat.CanSet):
    def method2(self):
        return 'value2'

Imagine I have 10 methods that I need to populate for this 'Klass'. I want to generate these methods without explicitely writing them all. So I want to do a factory that does setattr for each method. Problem is that I do following and the last method has the last value. Each do not get its related value but value10. Also below solution does not implement the decorator, I have no idea how to do assign the decorator

class Klass(object):
    pass

list1 = [('method1', 'value1'), ('method2', 'value2').....('method10', 'value10')]

for each in list1:
    method_name, method_value = each
    setattr(Klass, method_name, lambda self: method_value)

So when I do following....

k = Klass()
print k.method1(), method2()...., method10()

it all results in value10 for each method. Do not understand why ? Plus, can anyone help on how to implement the decorator with one attribute ? PS: if you have suggestion that does not use 'setattr', that would be welcomed as well.

回答1:

When you use the lambda to create each method, you are binding it to the currently-local scope. That scope has a single instance of a variable named method_value, and it is being set to a new value after each loop. Because each lambda refers to this one local variable, they all see the same value (e.g., the last value that was set).

If you create the lambda in a different method, it will have a different scope, and you can therefore get the desired behavior:

class Klass(object):
        pass

list1 = [('method1', 'value1'), ('method2', 'value2'), ('method10', 'value10')]

def make_attr(value):
    return lambda self: value

for method_name, method_value in list1:
    setattr(Klass, method_name, make_attr(method_value))

c = Klass()
print c.method1(), c.method2(), c.method10()  # "value1, value2, value10"

Here, make_attr creates a new scope, and so there are unique instances of the variable value, one for each lambda created.

You can also use a nice trick to create a lambda-scoped variable inline, as follows:

lambda self, val=method_value: val 

Here, val is assigned the value of method_value at the time of the lambda declaration, giving each instance of lambda its own value. This lets you use the more compact:

for method_name, method_value in list1:
    setattr(Klass, method_name, lambda self, val=method_value:val))

Finally, Marti points out in a different answer how to modify my make_attr function to apply the desired decorator:

def make_attr(value):
    return mat.sell(mat.CanSet)(lambda self: value))


回答2:

Myk Willis answer explains the problem with the scope of the method_value variable. I'll try to expand on it to answer your other doubts.

Regarding the decorator, and how to apply it when producing the methods programatically: it becomes easier once you understand how decorators work in Python. The thing is, they are just functions. A decorator is a function that receives a function and returns a function. The @ syntax is just syntactic sugar that applies the indicated decorator to the decorated function in a more convenient way.

So this:

@my_decorator
def my_function():
    ...

Will do exactly the same as this:

def my_function():
    ...

my_function = my_decorator(my_function)

The same applies to your original example. This:

@mat.sell(mat.CanSet):
def method1(self):
    return None

Is equivalent to this:

def method1(self):
    return None

method1 = mat.sell(mat.CanSet)(method1)

This one is a bit more complicated, because mat.sell is not a decorator, but rather a function that returns a decorator; hence the double call: first we call mat.sell, passing it the mat.CanSet parameter, and that call returns a decorator that we use to wrap method1.

Knowing this, modifying Myk's answer to apply your decorator is simple:

class Klass(object):
    pass

list1 = [('method1', 'value1'), ('method2', 'value2'), ('method10', 'value10')]

def make_attr(value):
    return mat.sell(mat.CanSet)(lambda self: value)

for method_name, method_value in list1:
    setattr(Klass, method_name, make_attr(method_value))

Note the change in make_attr(); instead of returning the fabricated method straight away, we pass it first through the decorator and return its return value instead.

As for this:

Plus, can anyone help on how to implement the decorator with one attribute ? PS: if you have suggestion that does not use 'setattr', that would be welcomed as well.

I'm not sure I understand what you are asking. Do you perhaps want to turn your methods into properties? Can you tell us what mat.sell does? It might help us to get a clearer picture of what you are trying to do.