【问题标题】:After changing the __init__ class function code, not allow to "get source code" anymore修改 __init__ 类功能代码后,不再允许“获取源代码”
【发布时间】:2020-12-23 15:40:46
【问题描述】:

我创建了一个元类,它在继承类的__init__ 函数参数中添加 args 和 kwargs,然后为初始化继承类实例增压它们
示例:

class A():
    def __init__(self, a:int, taunt = None):
        #print('init a')
        self.a = a
        self.test = None

class B(A, metaclass=MagicMeta):
    def __init__(self, b:int):
        #print('init b')
        self.b = b

class Foo(B,metaclass=MagicMeta):
    def __init__(self,yolo, name ='empty', surname = None):
        self.name = name
        self.surname= surname
        #print(self.test)

    def __str__(self):
        return str(self.__class__) + ": " + str(self.__dict__)

x =Foo(yolo=1,a=2,b=3, name='name!')
print(x.a)
print(x.b)
print(x.name)
print(str(x))
print(inspect.getsourcelines(A.__init__))
inspect.getsourcelines(Foo.__init__)

> 2
> 3
> name!
> "<class '__main__.Foo'>: {}"
> (['    def __init__(self, a:int, taunt = None):\n', "        print('init a')\n", '        self.a = a\n', '        self.test = None\n'], 2)
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
 in 
      4 print(x.name)
      5 print(str(x))
----> 6 inspect.getsourcelines(Foo.__init__)

~/opt/anaconda3/lib/python3.8/inspect.py in getsourcelines(object)
    965     raised if the source code cannot be retrieved."""
    966     object = unwrap(object)
--> 967     lines, lnum = findsource(object)
    968 
    969     if istraceback(object):

~/opt/anaconda3/lib/python3.8/inspect.py in findsource(object)
    796         lines = linecache.getlines(file)
    797     if not lines:
--> 798         raise OSError('could not get source code')
    799 
    800     if ismodule(object):

OSError: could not get source code

第一个问题,Foo实例的self不能为空,修改__init__Foo类函数的代码后,我不能再读了

这里是 MagicMeta 代码:



import re
from inspect import Parameter

# get arg and kwargs of a function
def get_args(f):
    args = list()
    kwargs = dict()
    for param in inspect.signature(f).parameters.values():
        if (param.kind == param.POSITIONAL_OR_KEYWORD):
            if param.default ==Parameter.empty:
                args.append(param.name)
            else:
                kwargs[param.name]= param.default 
    return args, kwargs 

def  compileKwargs(dct):
    string =""
    poke = False
    for k, o  in dct.items():
        if type(o) == str:
            string+= k+"='"+o+"', "
        else:           
            string+= k+"="+str(o)+", "

    return string

def stringArgs(liste):
    return " ".join([e+"," for e in liste])

def compileArgs(liste1,liste2):
    liste1.extend([e for e in liste2 if e not in liste1])
    return liste1

def editFuncName(actual: str, replace:str):
    #print('EDITFUNCNAME')
    #print(actual)
    string = re.sub('(?<=def ).*?(?=\()',replace, actual)
    #print('string', string)
    return string

import inspect
from textwrap import dedent, indent
# indent the string code
def processCode(code : list):
    string=""
    #print('processcode')
    for i,e  in enumerate(code):
        #print('row', e)
        #print('dedent', e)
        if i != 0:
            string+=indent(dedent(e),'\t')
        else :
            string+=dedent(e)
    return string

import types
class MagicMeta(type):
    def __init__(cls, name, bases, dct):
        
        setattr(cls,'_CODE_', dict())
        func = cls.__init__
        cls._CODE_[func.__name__]= inspect.getsourcelines(func)
        args2 =get_args(cls.__bases__[0].__init__)
        
        setattr(cls,'_ARGS_', dict())
        cls._ARGS_[func.__name__]=[get_args(func), args2]

        lines = cls._CODE_['__init__']
        string= lines[0][0]
        
        arg, kwarg = cls._ARGS_['__init__'][0]
        arg2, kwarg2 = cls._ARGS_['__init__'][1]
        
        comparg = stringArgs(compileArgs(arg, arg2))

        dct = {**kwarg ,**kwarg2}
        #print(dct)
        newargs = comparg + compileKwargs(dct)
        string = re.sub('(?<=\().*?(?=\))',newargs, string)

        superarg =stringArgs(arg2) + compileKwargs(kwarg2)
        #print(superarg)
        superx = "super({},self).{}({})\n".format(cls.__name__, func.__name__, superarg)

        code = lines[0]
        #print('LINE DEF', code[0])
        code[0]= editFuncName(string, 'tempo')
        code.insert(1, superx)
 
        #print('code:',code)
        codestr  = processCode(code)
        #print('précompile', codestr)
        comp = compile(codestr, '<string>','exec')
        #print(comp)
        
        #exec the code to define the 'tempo' function which will replace __init__
        exec(comp)
        cls.__init__ = types.MethodType(eval('tempo'), cls)
        #print(eval('tempo.__code__'))


【问题讨论】:

  • 这可能是 XY 问题。即使您解决了当前的问题,我也可以在代码中看到其他(可能的)问题。例如,我希望您添加的super() 是错误的。您正在创建的 init 函数通过在生成的 init 函数范围内不可用的名称引用一个类。您想在这里实现什么目标?
  • 我尝试从继承的类中传递 arg,这允许我从继承的最后一个类中初始化继承的类实例,这就像 go with structure 中的继承。当我要更新一个类时,每个继承这个类的类都可以在不改变每个类的代码的情况下初始化它

标签: python python-3.x inheritance metaclass


【解决方案1】:

getsourcelines 不会神奇地对传入的函数进行反编译和逆向工程以重新创建将编译回等效对象的源代码行。

它的作用是检查传入函数及其模块中的属性,以检索源、物理、文件(通常是“.py”文件)内的实际文本,并获取字节码本身中的注释以获取实际的行号。

如果您只是使用编译后的.pyc 文件运行一些代码,从文件夹中删除源.py,它将以同样的方式失败。

在您的情况下,.__init__ 函数的源代码不在文件上,而是在动态构建的字符串上,在元类中的 __init__ 方法退出后甚至不再存在。

但它是可修复的 - 您只需将用于生成 __init__ 方法的字符串保存为文件,并在编译该字符串的过程中添加该文件的路径。

如果你传递exec 一个字符串,就像你做的那样,它不会工作 - 但是如果你用你的字符串调用compile,在调用 exec 之前创建一个代码对象,compile 调用可以采取filename(实际上是一个路径)参数 - 将作为源文件嵌入到代码对象中。然后你可以像你一样调用exec,但是传递compile的返回而不是源代码字符串。

只要该文件存在于磁盘上,getsourcelines() 就会以适当的偏移量向您返回其内容。

In [xxx]: import inspect
...

In [104]: bla = "def bla(): return 1"                                                                         

In [105]: open("testx.py", "wt").write(bla)                                                                   
Out[105]: 19

In [106]: b = compile(bla, "testx.py", "exec")                                                                

In [107]: exec(b)                                                                                             

In [108]: bla()                                                                                               
Out[108]: 1

In [109]: inspect.getsourcelines(bla)                                                                         
Out[109]: (['def bla(): return 1\n'], 1)

In [110]: !rm testx.py                                                                                        

In [111]: inspect.getsourcelines(bla)                                                                         
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
<ipython-input-111-3459b1636cc6> in <module>
----> 1 inspect.getsourcelines(bla)
[...]

OSError: could not get source code

【讨论】:

  • 根据需要的来源,您还可以使用functools.wraps 来装饰生成的__init__。这意味着getsourcelines() 将遵循__wrapped__ 属性并获取原始__init__ 的源而不是生成的__init__
  • 第二种解决方案更适合我,但我无法正确开发它,我更新了cls.__init__ = functools.wraps(types.MethodType(eval('tempo'), cls)) but get this error : TypeError: update_wrapper() got multiple values for argument 'wrapped'`` after init Foo 的一个实例
  • 不需要调用 "MethodType" 来分配方法 - 只需将函数对象分配给类,就像在 cls.__init__ = tempo 中一样。其他棕褐色,通过调用“exec”将“tempo”变量创建为全局变量 - 您可以通过将全局字典传递给 exec 来做得更好,以便它存在于那里。除此之外,我没有逐行解析您的元类 - 这有点令人费解。将代码作为字符串处理并使用 eval 根本不是最好的方法——一个合适的装饰器就可以。处理额外的参数。这意味着重写
  • 好吧,我明白你的意思了,但我必须像以前一样使用eval exec,节奏不存在,所以我收到一个错误,因为节奏没有定义。而且我必须使用MethodType,否则我得到一个TypeError。我使用了您的第一个解决方案,但在打印时将selfinstance 的问题保留为空。应该重新提出问题还是在这里给我一个快速的答案?
【解决方案2】:

我认为这是一个 XY 问题。如果您使用的是比 3.7 更旧的 Python 版本,则使用 dataclasses 模块或关键字参数更容易解决您尝试完成的问题。

from dataclasses import dataclass

@dataclass
class A:
    a: int = 0

@dataclass
class B(A):
    b: int = 1

    def __post_init__(self):
        self.c = self.a + self.b

from dataclasses import field
from typing import List
@dataclass
class C(B):
    foo: List[int] = field(default_factory=list)

# __repr__ is thrown in for free
assert str(C(a=2, b=3, foo=[1, 2, 3])) == 'C(a=2, b=3, foo=[1, 2, 3])'
# retain default args
assert C().a == 0
# using positional args and post_init
assert C(2, 3).c == 5
# mutable defaults
assert C().foo == [] and C().foo is not C().foo

使用 kwargs,您可以使用命名参数弹出每个类所需的参数,然后将其余的 kwargs 传递给父类的 init 函数,例如。

class A:
    def __init__(self, a: int = 0):
        self.a = a

class B(A):
    def __init__(self, *, b:int, **kwargs):
        super().__init__(**kwargs)
        self.b = b

class C(B):
    def __init__(self, *, c: int = 2, **kwargs):
        super().__init__(**kwargs)
        self.c = c

obj = C(a=1, b=2, c=3)
assert vars(obj) == dict(a=1, b=2, c=3)

# retain defaults
obj2 = C(b=2)
assert obj2.a == 0

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-17
    • 2018-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多