python中如何动态获得调用栈中类的类名?

我们都知道,python有一定的反射能力,比如动态获得函数名:

def my_caller():
    my_callee()
    
def my_callee():
    import inspect
    frame = inspect.stack()[1].frame
    print('the caller is \'{}\'.'.format(frame.f_code.co_name))
    
my_caller()

以上代码可以打印出:

the caller is 'my_caller'.

但是当caller是一个类时:

class Caller(object):
    def run1(self):
        my_callee()
        
    @classmethod
    def run2(clazz):
        my_callee()
        
    @staticmethod
    def run3():
        my_callee()
        
caller = Caller()
caller.run1()
caller.run2()
Caller.run3()

以上代码可以打印出方法名,却打印不出类名:

the caller is 'run1'.
the caller is 'run2'.
the caller is 'run3'.

那么,有没有办法,动态地获得调用栈中类的类名呢?

阅读 3.3k
3 个回答

我自己倒是找到了一个不很干净的方法,而且对于staticmethod就无能为力了:

def my_callee():
    import inspect
    frame = inspect.stack()[1].frame
    method_name = frame.f_code.co_name
    c = frame.f_code
    clazz_name = None
    if c.co_argcount > 0:
        first_arg = frame.f_locals[c.co_varnames[0]]
        if hasattr(first_arg, method_name) and getattr(first_arg, method_name).__code__ is c:
            if inspect.isclass(first_arg):
                clazz_name = first_arg.__qualname__
            else:
                clazz_name = first_arg.__class__.__qualname__
    if clazz_name is None:
        print('the caller is \'{}\'.'.format(method_name))
    else:
        print('the caller is \'{}.{}\'.'.format(clazz_name, method_name))
    del frame


class Caller(object):
    def run1(self):
        print('\nCaller.run1()')
        my_callee()
    @classmethod
    def run2(clazz):
        print('\nCaller.run2()')
        my_callee()
    @staticmethod
    def run3():
        print('\nCaller.run3()')
        my_callee()

def run4():
    print('\nrun4()')
    my_callee()


def run():
    c = Caller()
    c.run1()
    c.run2()
    c.run3()
    run4()
    
run()

以上代码将输出:


Caller.run1()
the caller is 'Caller.run1'.

Caller.run2()
the caller is 'Caller.run2'.

Caller.run3()
the caller is 'run3'.

run4()
the caller is 'run4'.

可以看到,这种方法是无法区分普通函数与staticmethod的。


自己事后又分析了一下,感觉python其实并不是纯粹的面向对象语言,到了执行层面,都转化成函数调用了,也就是code对象,而解释器有没有把这个code与其对应的函数对象关联起来(其实是有关联的,只不过是单向的,每个function和method都有一个__code__属性,就是code对象),所以导致无法从code反向逆推出是来自哪个function或method。

所以如果要想反向定位,也就只能自己来维护 code -> function/method 的关系了。其实实现起来也简单,只要把所有的类都扫描一遍就可以了(性能会有点低,真有需要可以把映射关系缓存下来,不用每次都全类扫描),例如:

def my_callee():

    import inspect
    
    # 取得frame和code
    frame = inspect.stack()[1].frame
    code = frame.f_code
    
    # 取得缓存
    if not hasattr(my_callee, 'code_cache')
        cache = {}
        setattr(my_callee, 'code_cache', cache)
    else:
        cache = getattr(my_callee, 'code_cache')
    
    # 检查缓存是否命中
    code_id = id(code)
    if code_id in cache:
        print('the caller is \'{}\'.'.format(cache[code_id]))

    # 通过第一个参数self(实例方法)或cls(类方法)进行判定,速度最快
    method_name = frame.f_code.co_name
    clazz_name = None
    if code.co_argcount > 0:
        first_arg = frame.f_locals[code.co_varnames[0]]
        if hasattr(first_arg, method_name) and getattr(first_arg, method_name).__code__ is code:
            if inspect.isclass(first_arg):
                clazz_name = first_arg.__qualname__
            else:
                clazz_name = first_arg.__class__.__qualname__
            qualname = method_name if clazz_name is None else '{}.{}'.format(clazz_name, method_name)
            print('the caller is \'{}\'.'.format(qualname))
            cache[code_id] = qualname
            return

    # 扫描f_globals中的所有类的方法和所有的函数,并在缓存中记录下来它们的id和名称
    for k,v in frame.f_globals.items():
        if inspect.isclass(v):
            for kk in v.__dict__:
                vv = getattr(v, kk)
                if inspect.isfunction(vv):
                    cache[id(vv.__code__)] = vv.__qualname__
        elif inspect.isfunction(v):
            cache[id(v.__code__)] = v.__qualname__
            
    # 把所有的类和函数都扫描过一遍后,再回过头来定位code
    if code_id in cache:
        print('the caller is \'{}\'.'.format(cache[code_id]))
    else:
        print('impossible: {}'.format(code_id))
        
    # 释放
    del frame

self.__class__

def my_callee():
    import inspect
    frame = inspect.stack()
    print(frame)
    print(frame[1].code_context)
    print(frame[2].code_context)
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题