Search code examples
python-3.xdynamicmetaprogramming

Conflicts when find and "replace" a method dynamically


I have a class consisting of a "list" of static methods, A. I want to change its behavior with a class-decorator, Meta, which acts on a specific static method, in this example content, by performing the method m.

My original attempt, CASE=2, didn't work as expected, so I started I case study. I introduced a new class B, which has slightly different implementation of an other method, info but raised a funny error, and a new class C just without the method, info.

  • case 2: the greedy case

d[i] = classmethod(lambda cls, *args: mcs.m( getattr(target_cls, i)(*args)) ) it doesn't work properly, maybe too many nested dynamic expressions?

  • case 1: it essentially case 2 but the expression is divided in two lines, and it works
   o = getattr(target_cls, i)
   d[i] = classmethod(lambda cls, *args: mcs.m(o(*args)))

Here the code

class Meta:

    def __new__(mcs, target_cls):
        if CASE == 1:
            print('case 1')
            d = {}
            for i in dir(target_cls):
                 if i == 'content':
                    o = getattr(target_cls, i)
                    d[i] = classmethod(lambda cls, *args: mcs.m(o(*args)))

        if CASE == 2:
            print('case 2')
            d = {}
            for i in dir(target_cls):
                 if i == 'content':
                    d[i] = classmethod(lambda cls, *args: mcs.m( getattr(target_cls, i)(*args)) )

        return type('AAA', (target_cls,), d)

    @classmethod
    def m(mcs, p):

        return '--> ', p


class A:

    @staticmethod
    def content(response):
        return 'static_method', response
    @staticmethod
    def info(response):
        return response


class B:

    @staticmethod
    def content(response):
        return 'static_method', response
    @staticmethod
    def info(response):
        response.sort()
        return response

class C:

    @staticmethod
    def content(response):
        return 'static_method', response

# call the "content" class-method of each class for all different cases
for cls in (A, B, C):
    print(cls.__name__)
    for case in range(1,3):
        CASE = case
        R = Meta(cls)
        try:
            print(R.content('ppp'))
        except Exception as e: print(e)
    
    print()

Output

A
case 1
('--> ', ('static_method', 'ppp'))
case 2
('--> ', 'ppp')        # no decoration

B
case 1
('--> ', ('static_method', 'ppp'))
case 2
'str' object has no attribute 'sort'  # <- complained about the other method

C              # <- this is ok BUT I removed the other method!
case 1
('--> ', ('static_method', 'ppp'))
case 2
('--> ', ('static_method', 'ppp'))  # <- here the decoration took place

The question is why case 2 doesn't work, if it is a limitation of the language then of what kind?

Extra question: how to explain the error of class B case 2


Solution

  • I guess that the issue is caused by the loop and the origin is the fact that each statement has not its own scope (in the loop). By passing i as a key parameter of the lambda fixed the problem.

    class Meta:
    
        def __new__(mcs, target_cls):
            d = {}
            for i in dir(target_cls):
                 if i == 'content':
                    d[i] = classmethod(lambda cls, *args, m_name=i: mcs.m( getattr(target_cls, m_name)(*args)) )
        
            return type('AAA', (target_cls,), d)
    
        @classmethod
        def m(mcs, p):
            return '--> ', p
    
    class A:
    
        @staticmethod
        def content(response):
            return 'static_method', response
    
        @staticmethod
        def info(response):
            return response
    
    print(A.content)
    print(Meta(A).content)
    print(Meta(A).content('a'))
    print(Meta(A).info)
    

    Output

    <function A.content at 0x7f04500740d0> # original static method
    <bound method Meta.__new__.<locals>.<lambda> of <class '__main__.AAA'>> # class method
    ('--> ', ('static_method', 'a'))
    <function A.info at 0x7f0450074040>