更改__init__ class 功能代码后,不再允许"get source code"

After changing the __init__ class function code, not allow to "get source code" anymore

我创建了一个元 class,它在 __init__ 函数参数中添加 args 和 kwargs 来自 inherited class 然后为 init inherited class instance
示例:

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 class函数的代码后,无法读取不再

这里是 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__'))


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

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

如果您像 运行 一些代码一样简单地使用编译的 .pyc 文件,从文件夹中删除源代码 .py,它也会以同样的方式失败。

在您的例子中,.__init__ 函数的源代码不在文件中,它在动态构建的字符串中,在 __init__ 方法之后甚至不再存在元类退出。

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

如果像您一样传递 exec 一个字符串,它将不起作用 - 但如果您使用字符串调用 compile,要在调用 exec 之前创建一个代码对象,compile 调用可以采用 filename (实际上是路径)参数 - 它将作为源文件嵌入到代码对象中。然后你可以像你一样调用 exec,但是将 compile 的 return 传递给它而不是 source-code 字符串。

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

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

我认为这是一个 XY 问题。如果您使用的 Python 版本比 3.7.

旧版本,则使用 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,您可以使用命名参数弹出每个 class 所需的参数,然后将其余的 kwargs 传递给父 classes 初始化函数,例如

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