如何在Python 3.1类构造期间找到绑定方法的类?

7 投票
3 回答
2541 浏览
提问于 2025-04-15 18:48

我想写一个装饰器,让类的方法可以被其他地方看到。不过,我要说的问题和这个细节没有关系。代码大概是这样的:

def CLASS_WHERE_METHOD_IS_DEFINED( method ):
  ???

def foobar( method ):
  print( CLASS_WHERE_METHOD_IS_DEFINED( method ) )

class X:

  @foobar
  def f( self, x ):
    return x ** 2

我在这里遇到的问题是,当装饰器 foobar() 看到这个方法的时候,它还不能被调用;它看到的是一个未绑定的版本。也许可以通过在类上使用另一个装饰器来解决这个问题,这样可以处理绑定方法所需的事情。接下来我打算做的是,在方法通过装饰器时给它加一个属性,然后使用类装饰器或 metaclass 来进行后续处理。如果我能做到这一点,那我就不需要解决这个让我困惑的难题了:

有没有人能在上面的代码中填充 CLASS_WHERE_METHOD_IS_DEFINED 下的有意义的代码行,这样装饰器就能在 f 被定义的瞬间打印出它所在的类?或者在 Python 3 中,这种可能性是被排除的?

3 个回答

0

这是一篇很老的帖子,不过用反射来解决这个问题并不是最佳方法,因为其实可以通过一个元类和一些巧妙的类构造逻辑,结合描述符来更简单地解决。

import types

# a descriptor as a decorator
class foobar(object):

    owned_by = None

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        # a proxy for `func` that gets used when
        # `foobar` is referenced from by a class
        return self.func(*args, **kwargs)

    def __get__(self, inst, cls=None):
        if inst is not None:
            # return a bound method when `foobar`
            # is referenced from by an instance
            return types.MethodType(self.func, inst, cls)
        else:
            return self

    def init_self(self, name, cls):
        print("I am named '%s' and owned by %r" % (name, cls))
        self.named_as = name
        self.owned_by = cls

    def init_cls(self, cls):
        print("I exist in the mro of %r instances" % cls)
        # don't set `self.owned_by` here because 
        # this descriptor exists in the mro of
        # many classes, but is only owned by one.
        print('')

让这个方法有效的关键在于元类——它会在创建的类中查找定义的属性,寻找foobar描述符。一旦找到,它就会通过描述符的init_selfinit_cls方法,将相关类的信息传递给它们。

init_self只会在定义了描述符的类上被调用。这是修改foobar的地方,因为这个方法只会被调用一次。而init_cls则会在所有可以访问被装饰方法的类上被调用。这是应该修改类foobar的地方。

import inspect

class MetaX(type):

    def __init__(cls, name, bases, classdict):
        # The classdict contains all the attributes
        # defined on **this** class - no attribute in
        # the classdict is inherited from a parent.
        for k, v in classdict.items():
            if isinstance(v, foobar):
                v.init_self(k, cls)

        # getmembers retrieves all attributes
        # including those inherited from parents
        for k, v in inspect.getmembers(cls):
            if isinstance(v, foobar):
                v.init_cls(cls)

示例

# for compatibility
import six

class X(six.with_metaclass(MetaX, object)):

    def __init__(self):
        self.value = 1

    @foobar
    def f(self, x):
        return self.value + x**2

class Y(X): pass

# PRINTS:
# I am named 'f' and owned by <class '__main__.X'>
# I exist in the mro of <class '__main__.X'> instances

# I exist in the mro of <class '__main__.Y'> instances

print('CLASS CONSTRUCTION OVER\n')

print(Y().f(3))
# PRINTS:
# 10
4

正如我在一些其他 回答中提到的,从Python 3.6开始,解决这个问题变得非常简单,这要归功于object.__set_name__这个特性,它会在定义类的时候被调用。

我们可以用它来定义一个装饰器,这个装饰器可以访问到类,方法如下:

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with "owner" (i.e. the class)
        print(f"decorating {self.fn} and using {owner}")

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

然后我们就可以像使用普通装饰器一样使用它:

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
9

当装饰器被调用时,它接收到的是一个函数作为参数,而不是一个方法。因此,装饰器想要检查和了解这个方法是没用的,因为它只是一段函数代码,根本没有关于它所在类的任何信息。我希望这能解答你的“谜题”,虽然是从反面来讲的!

你可以尝试其他方法,比如深入检查嵌套的调用栈,但这些方法都比较复杂、不稳定,而且在其他Python 3的实现(比如pynie)中也不一定能用。因此,我强烈建议你还是使用你已经考虑的类装饰器方案,这样更简洁、更可靠。

撰写回答