【问题标题】:Find class in which a method is defined查找定义方法的类
【发布时间】:2014-11-03 01:53:10
【问题描述】:

我想从方法本身中找出定义某个方法的类的类型(本质上是方法的封闭静态范围),而不是明确指定它,例如

class SomeClass:
    def do_it(self):
        cls = enclosing_class() # <-- I need this.
        print(cls)

class DerivedClass(SomeClass):
    pass

obj = DerivedClass()
# I want this to print 'SomeClass'.
obj.do_it()

这可能吗?

【问题讨论】:

  • enclosing_class = lambda: SomeClass ;-)
  • 您可能只需要搜索mro
  • @reddish -- 但你没有说明为什么。这闻起来像 XY problem
  • @reddish -- 当然不是,但我真的 am 想提供帮助。但是,你所要求的是一件很难实现的事情。它可能归结为检查堆栈帧和代码对象——可能会遍历 MRO 并检查那里定义的每个方法的源代码,直到找到所需的内容。如果您让我们知道这样的功能有什么实用程序,我们可能会为您指出一个更容易/更整洁/更清洁的替代方案来实现相同的目标。
  • 您要的是哪个版本的 Python?因为,虽然这几乎可以肯定是一件非常糟糕的事情,但至少有一些实现和版本的方法,但它们不一定相同,我不想回答每个可能的版本…

标签: python


【解决方案1】:

您可以按照@mgilson 的建议进行操作,也可以采取其他方法。

class SomeClass:
    pass

class DerivedClass(SomeClass):
    pass

这使得SomeClass 成为DerivedClass 的基类。
当您通常尝试获取 __class__.name__ 时,它将引用派生类而不是父类。

当您调用do_it() 时,它实际上将DerivedClass 作为自我传递,这就是您最有可能得到DerivedClass 的原因。

试试这个:

class SomeClass:
    pass

class DerivedClass(SomeClass):
    def do_it(self):
        for base in self.__class__.__bases__:
            print base.__name__
obj = DerivedClass()
obj.do_it() # Prints SomeClass

编辑:
多读几遍你的问题后,我想我明白你想要什么了。

class SomeClass:
    def do_it(self):
        cls = self.__class__.__bases__[0].__name__
        print cls

class DerivedClass(SomeClass):
    pass

obj = DerivedClass()
obj.do_it() # prints SomeClass

【讨论】:

  • 我明白这一切。您提出的解决方案没有解决我的问题。
  • @reddish,我编辑了我认为你想要的答案。让我知道它是否按您的预期工作
  • 这种方法不能满足我的需要。您的方法假定我从一个比 SomeClass 低一个继承级别的类中调用“do_it”。如果我从直接 SomeClass 实例或从派生自“DerivedClass”的类的实例调用“do_it”,它将中断。
【解决方案2】:

[已编辑] 一个更通用的解决方案:

import inspect

class Foo:
    pass

class SomeClass(Foo):
    def do_it(self):
        mro = inspect.getmro(self.__class__)
        method_name = inspect.currentframe().f_code.co_name
        for base in reversed(mro):
            if hasattr(base, method_name):
                print(base.__name__)
                break

class DerivedClass(SomeClass):
    pass

class DerivedClass2(DerivedClass):
    pass

DerivedClass().do_it()
>> 'SomeClass'

DerivedClass2().do_it()
>> 'SomeClass'

SomeClass().do_it()
>> 'SomeClass'

当堆栈中的某个其他类具有属性“do_it”时,这将失败,因为这是停止行走 mro 的信号名称。

【讨论】:

  • 这不只是打印第一个基类名称吗?如果SomeClass 是从另一个类派生的,它将不起作用。
  • 是的,它打印继承堆栈中的第一个类,但 OP 没有指定他的类在堆栈中的位置,在他的示例中它是第一个......
  • 我指定我想要“方法的封闭静态范围”。你的方法没有做到这一点。
  • 更新后还包括基类。
  • 我不确定为什么另一个链接的答案和这个都反转了 MRO。你需要按照给定的顺序搜索它,找到函数的最后一个最新定义,不是吗?
【解决方案3】:

首先,这几乎可以肯定是个坏主意,而不是您想要解决的任何问题但拒绝告诉我们的方式......

话虽如此,有一种非常简单的方法可以做到这一点,至少在 Python 3.0+ 中是这样。 (如果您需要 2.x,请参阅我的其他答案。)

请注意,Python 3.x 的super 几乎必须能够以某种方式做到这一点。 super() 怎么可能意味着 super(THISCLASS, self),而 THISCLASS 正是您所要求的?*

现在,super可以通过多种方式实现……但PEP 3135 详细说明了如何实现它:

每个函数都有一个名为 __class__ 的单元格,其中包含定义函数的类对象。

这不是 Python 参考文档的一部分,所以其他一些 Python 3.x 实现可以用不同的方式来做……但至少从 3.2+ 开始,它们仍然必须在函数上使用 __class__,因为 @ 987654323@ 明确表示:

这个类对象将被super() 的零参数形式引用。 __class__ 是编译器创建的隐式闭包引用,如果类主体中的任何方法引用 __class__super。这使得super() 的零参数形式可以根据词法范围正确识别正在定义的类,而用于进行当前调用的类或实例是根据传递给方法的第一个参数来识别的。

(而且,不用说,这正是至少 CPython 3.0-3.5 和 PyPy3 2.0-2.1 实现 super 的方式。)

In [1]: class C:
   ...:     def f(self):
   ...:         print(__class__)
In [2]: class D(C):
   ...:     pass
In [3]: D().f()
<class '__main__.C'>

当然,这会获取实际的类对象,而不是类的名称,这显然是您所追求的。但这很容易;你只需要确定你的意思是__class__.__name__ 还是__class__.__qualname__(在这个简单的例子中它们是相同的)并打印出来。


* 事实上,这是反对它的论据之一:在不改变语言语法的情况下,唯一可行的方法是为每个函数添加一个新的闭包单元,或者需要一些可怕的帧黑客这在 Python 的其他实现中甚至可能不可行。您不能只使用编译器魔法,因为编译器无法判断某些任意表达式将在运行时评估为 super 函数……

【讨论】:

  • 感谢您的详细回答。这确实是我提出的问题的一个干净的解决方案。对于我的具体情况,如果我可以从实际的 'enclosure_scope()' 函数中到达封闭类会更方便,但这个解决方案可以达到我需要的 95%。
  • 至于我的问题的背景:这是关于向代码库添加检测以能够生成方法调用计数的报告,以检查某些近似的运行时不变量(例如“数字方法 ClassA.x() 的执行次数大约等于方法 ClassB.y() 在运行复杂程序的过程中执行的次数。但是不管那个特定的用例,我认为提出的问题很有趣,值得回答,这就是为什么我更愿意远离背景故事。
  • @reddish:但听起来有一个更好的方法来解决这个问题:在你检测类的时候,你知道你正在检测的类,因此你不需要首先动态恢复该信息。 (请注意,这与编译器在编译时添加 __class__ 的原因完全相同:因此当您调用 super 时不必动态恢复它。)
  • 是的,但是我将不得不手动检测许多类,并且为了防止错误,我想避免在任何地方输入类名。本质上,这就是为什么键入 super() 比键入 super(ClassX, self) 更可取的原因。
  • @reddish:这就是为什么您应该编写一个函数来为您执行检测而不是手动执行。 (事实上​​,您是静态而不是动态地执行此操作,这意味着该函数可能应该是一个装饰器,而不是它不应该存在。)
【解决方案4】:

如果你可以使用@abarnert的方法,那就去做吧。

否则,你可以使用一些硬核内省(对于python2.7):

import inspect
from http://stackoverflow.com/a/22898743/2096752 import getMethodClass

def enclosing_class():
    frame = inspect.currentframe().f_back
    caller_self = frame.f_locals['self']
    caller_method_name = frame.f_code.co_name
    return getMethodClass(caller_self.__class__, caller_method_name)

class SomeClass:
    def do_it(self):
        print(enclosing_class())

class DerivedClass(SomeClass):
    pass

DerivedClass().do_it() # prints 'SomeClass'

显然,如果出现以下情况,这可能会引发错误:

  • 从常规函数/静态方法/类方法调用
  • 调用函数对self 有不同的名称(正如@abarnert 恰当指出的那样,这可以通过使用frame.f_code.co_varnames[0] 来解决)

【讨论】:

  • 你可以通过使用f.f_code.co_varnames[0]而不是self来解决最后一个问题,至少在CPython和PyPy中。或者,也许更好(但我忘记添加了哪个版本)inspect.getargs(f.f_code).args[0].
  • 检查文档后,getargs 没有记录在案,它只是用于实现记录功能的内部结构的一部分,并且(至少)在 CPython 2.6-2.7 中不存在,所以......在这里没有帮助(如果你有 Python 3.x,只需使用__class__),所以我想你必须使用co_varnames
【解决方案5】:

如果您在 Python 3.x 中需要此功能,请参阅我的另一个答案——闭包单元 __class__ 就是您所需要的。


如果您需要在 CPython 2.6-2.7 中执行此操作,RickyA 的答案很接近,但它不起作用,因为它依赖于该方法不会覆盖任何其他同名方法这一事实。尝试在他的答案中添加Foo.do_it 方法,它会打印出Foo,而不是SomeClass

解决的方法是找到代码对象与当前帧的代码对象相同的方法:

def do_it(self):
    mro = inspect.getmro(self.__class__)
    method_code = inspect.currentframe().f_code
    method_name = method_code.co_name
    for base in reversed(mro):
        try:
            if getattr(base, method_name).func_code is method_code:
                print(base.__name__)
                break
        except AttributeError:
            pass

(请注意,base 没有名为 do_it 的东西,或者 base 有名为 do_it 的东西,但它不是函数,因此没有func_code。但我们不在乎哪个;无论哪种方式,base 都不是我们正在寻找的匹配项。)

可能适用于其他 Python 2.6+ 实现。 Python 不需要框架对象存在,如果它们不存在,inspect.currentframe() 将返回None。而且我很确定它也不需要存在代码对象,这意味着func_code 可能是None

同时,如果您想在 2.7+ 和 3.0+ 中使用它,请将 func_code 更改为 __code__,但这会破坏与早期 2.x 的兼容性。


如果您需要 CPython 2.5 或更早版本,您只需将 inpsect 调用替换为特定于实现的 CPython 属性即可:

def do_it(self):
    mro = self.__class__.mro()
    method_code = sys._getframe().f_code
    method_name = method_code.co_name
    for base in reversed(mro):
        try:
            if getattr(base, method_name).func_code is method_code:
                print(base.__name__)
                break
        except AttributeError:
            pass

请注意,mro() 的这种用法不适用于经典类;如果你真的想处理那些(你真的不应该想......),你将不得不编写你自己的 mro 函数,它只是走层次老派......或者只是从 2.6 inspect 复制它来源。

这仅适用于向后弯曲以与 CPython 兼容的 Python 2.x 实现……但至少包括 PyPy。 inspect 应该更便携,但是如果一个实现要定义 framecode 具有与 CPython 相同属性的对象,因此它可以支持所有 inspect,没有太多的理由不制作它们属性并首先提供sys._getframe...

【讨论】:

    【解决方案6】:

    很抱歉写了另一个答案,但这里是你真正想要做的事情,而不是你要求做的事情:

    这是关于向代码库添加检测以能够生成方法调用计数的报告,以检查某些近似的运行时不变量(例如“方法 ClassA.x() 的执行次数大约为等于方法 ClassB.y() 在运行复杂程序的过程中执行的次数)。

    这样做的方法是让您的检测功能静态注入信息。毕竟,它必须知道将代码注入的类和方法。

    我将不得不手动检测许多类,为了防止错误,我想避免在任何地方输入类名。本质上,这也是为什么键入 super() 比键入 super(ClassX, self) 更可取的原因。

    如果您的检测功能是“手动执行”,那么您首先要将其转换为实际功能,而不是手动执行。由于您显然只需要静态注入,因此在类(如果您想检测每个方法)或每个方法(如果您不这样做)上使用装饰器会使它变得美观且可读。 (或者,如果你想检测每个类的每个方法,你可能想定义一个元类并让你的根类使用它,而不是装饰每个类。)

    例如,下面是一种检测类的每个方法的简单方法:

    import collections
    import functools
    import inspect
    
    _calls = {}
    def inject(cls):
        cls._calls = collections.Counter()
        _calls[cls.__name__] = cls._calls
        for name, method in cls.__dict__.items():
            if inspect.isfunction(method):
                @functools.wraps(method)
                def wrapper(*args, **kwargs):
                    cls._calls[name] += 1
                    return method(*args, **kwargs)
                setattr(cls, name, wrapper)
        return cls
    
    @inject
    class A(object):
        def f(self):
            print('A.f here')
    
    @inject
    class B(A):
        def f(self):
            print('B.f here')
    
    @inject
    class C(B):
        pass
    
    @inject
    class D(C):
        def f(self):
            print('D.f here')
    
    d = D()
    d.f()
    B.f(d)
    
    print(_calls)
    

    输出:

    {'A': Counter(), 
     'C': Counter(), 
     'B': Counter({'f': 1}), 
     'D': Counter({'f': 1})}
    

    正是你想要的,对吧?

    【讨论】:

    • 不完全是,但这是一种有趣的方法。它确实让我怀念 Python 实际上是一门简单语言的日子。
    • @reddish:我从 1.5 开始就一直在使用 Python。我能想到的最后一个真正使语言更复杂的变化是描述符(2.2)和生成器(2.3)。从那以后,肯定有一些事情让它变得更简单(不再有int/long,两种类,cmp 与丰富的比较,不完全适用于 Unicode 但不适用的 stdlib 模块t 给出错误,...),当然大多数错误在 3.0 中同时发生。
    • 我可能的意思是,要理解你提议的代码是如何工作的,需要相当多的努力和背景知识;这与 Python 作为一种用于编写易于阅读的代码的语言的基本思想(“可执行伪代码”思想)形成鲜明对比。达到干净的装饰器语法所需的相当复杂的脚手架让我感到不舒服。但我很欣赏最终的结果。
    • @reddish:我真的不明白这里有什么复杂的。您正在尝试动态修改类的所有方法;这可能是多么容易——甚至应该是有限度的。更重要的是,我的做法——通过猴子修补类——与我在 Python 1.6 中的做法完全相同。唯一的区别是我可以使用inspectfunctools 方法而不是访问和设置准未记录的属性,并且我可以将其包装在装饰器中,因此我不必为每个类编写C = inspect(C)。 (与 2.2-2.7 不同,我不必处理绑定方法。)
    • @reddish:除非你的论点是“没有人会在 Python 1.6 中尝试这样做,因此它会有点复杂”……在这种情况下答案不是 Python 变得更复杂,而是你的需求和要求变得更复杂了,因为你同时学到了一些强大的思想(比如方面),这些思想不是 Python 内置的,必须手动构建。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多