classmethod 和 instancemethod 同名

Same name for classmethod and instancemethod

我想做这样的事情:

class X:

    @classmethod
    def id(cls):
        return cls.__name__

    def id(self):
        return self.__class__.__name__

现在为 class 或其实例调用 id()

>>> X.id()
'X'
>>> X().id()
'X'

显然,这个确切的代码不起作用,但有没有类似的方法让它起作用?

或者任何其他解决方法来获得这种行为而没有太多“hacky”的东西?

Class 和实例方法位于同一个命名空间中,您不能重复使用这样的名称; id 的最后定义将在这种情况下获胜。

class 方法将继续在实例上工作,但是,不需要 创建一个单独的实例方法;只需使用:

class X:
    @classmethod
    def id(cls):
        return cls.__name__

因为该方法继续绑定到 class:

>>> class X:
...     @classmethod
...     def id(cls):
...         return cls.__name__
... 
>>> X.id()
'X'
>>> X().id()
'X'

这是明确记录的:

It can be called either on the class (such as C.f()) or on an instance (such as C().f()). The instance is ignored except for its class.

如果您确实需要区分绑定到 class 和实例

如果您需要一种方法根据其使用位置不同来工作;在 class 上访问时绑定到 class,在实例上访问时绑定到实例,您需要创建一个自定义 描述符对象 .

descriptor API is how Python causes functions to be bound as methods, and bind classmethod objects to the class; see the descriptor howto.

您可以通过创建具有 __get__ 方法的对象来为方法提供自己的描述符。这是一个简单的方法,它根据上下文切换方法绑定的内容,如果 __get__ 的第一个参数是 None,则描述符被绑定到 class,否则它正在绑定到一个实例:

class class_or_instancemethod(classmethod):
    def __get__(self, instance, type_):
        descr_get = super().__get__ if instance is None else self.__func__.__get__
        return descr_get(instance, type_)

这重用了 classmethod 并且仅重新定义了它处理绑定的方式,委托了 instance is None 的原始实现,否则委托给了标准函数 __get__ 实现。

请注意,在方法本身中,您可能需要测试它绑定的内容。 isinstance(firstargument, type) 是一个很好的测试:

>>> class X:
...     @class_or_instancemethod
...     def foo(self_or_cls):
...         if isinstance(self_or_cls, type):
...             return f"bound to the class, {self_or_cls}"
...         else:
...             return f"bound to the instance, {self_or_cls"
...
>>> X.foo()
"bound to the class, <class '__main__.X'>"
>>> X().foo()
'bound to the instance, <__main__.X object at 0x10ac7d580>'

另一种实现可以使用两个函数,一个用于绑定到class,另一个用于绑定到实例:

class hybridmethod:
    def __init__(self, fclass, finstance=None, doc=None):
        self.fclass = fclass
        self.finstance = finstance
        self.__doc__ = doc or fclass.__doc__
        # support use on abstract base classes
        self.__isabstractmethod__ = bool(
            getattr(fclass, '__isabstractmethod__', False)
        )

    def classmethod(self, fclass):
        return type(self)(fclass, self.finstance, None)

    def instancemethod(self, finstance):
        return type(self)(self.fclass, finstance, self.__doc__)

    def __get__(self, instance, cls):
        if instance is None or self.finstance is None:
              # either bound to the class, or no instance method available
            return self.fclass.__get__(cls, None)
        return self.finstance.__get__(instance, cls)

这是一个带有可选实例方法的class方法。像使用 property 对象一样使用它;用 @<name>.instancemethod:

装饰实例方法
>>> class X:
...     @hybridmethod
...     def bar(cls):
...         return f"bound to the class, {cls}"
...     @bar.instancemethod
...     def bar(self):
...         return f"bound to the instance, {self}"
... 
>>> X.bar()
"bound to the class, <class '__main__.X'>"
>>> X().bar()
'bound to the instance, <__main__.X object at 0x10a010f70>'

就个人而言,我的建议是谨慎使用;基于上下文改变行为的完全相同的方法使用起来可能会造成混淆。但是,有一些用例,例如 SQLAlchemy 区分 SQL 对象和 SQL 值,其中模型中的列对象会像这样切换行为;查看他们的 Hybrid Attributes documentation。此实现遵循与我上面的 hybridmethod class 完全相同的模式。

我不知道你的实际用例是什么,但你可以使用描述符来做这样的事情:

class Desc(object):

    def __get__(self, ins, typ):
        if ins is None:
            print 'Called by a class.'
            return lambda : typ.__name__
        else:
            print 'Called by an instance.'
            return lambda : ins.__class__.__name__

class X(object):
    id = Desc()

x = X()
print x.id()
print X.id()

输出

Called by an instance.
X
Called by a class.
X

可以非常简洁地完成,方法是将方法的实例绑定版本显式绑定到实例(而不是class)。 Python 将在调用 Class().foo() 时调用在 Class().__dict__ 中找到的实例属性(因为它在 class' 之前搜索实例的 __dict__),并且 [=调用 Class.foo() 时在 Class.__dict__ 中找到 35=] 绑定方法。

这有很多潜在的用例,尽管它们是否是反模式还有待商榷:

class Test:
    def __init__(self):
        self.check = self.__check

    @staticmethod
    def check():
        print('Called as class')

    def __check(self):
        print('Called as instance, probably')

>>> Test.check()
Called as class
>>> Test().check()
Called as instance, probably

或者...假设我们希望能够滥用 map():

class Str(str):
    def __init__(self, *args):
        self.split = self.__split

    @staticmethod
    def split(sep=None, maxsplit=-1):
        return lambda string: string.split(sep, maxsplit)

    def __split(self, sep=None, maxsplit=-1):
        return super().split(sep, maxsplit)

>>> s = Str('w-o-w')
>>> s.split('-')
['w', 'o', 'w']
>>> Str.split('-')(s)
['w', 'o', 'w']
>>> list(map(Str.split('-'), [s]*3))
[['w', 'o', 'w'], ['w', 'o', 'w'], ['w', 'o', 'w']]

在您的示例中,您可以简单地完全删除第二个方法,因为静态方法和 class 方法做同样的事情。

如果您希望他们做不同的事情:

class X:
    def id(self=None):
       if self is None:
           # It's being called as a static method
       else:
           # It's being called as an instance method

"types" 提供了自 Python 3.4 以来非常有趣的内容:DynamicClassAttribute

它并没有 100% 实现您的想法,但它似乎是密切相关的,您可能需要稍微调整一下我的 metaclass 但是,大体上,您可以拥有它;

from types import DynamicClassAttribute

class XMeta(type):
     def __getattr__(self, value):
         if value == 'id':
             return XMeta.id  # You may want to change a bit that line.
     @property
     def id(self):
         return "Class {}".format(self.__name__)

这将定义您的 class 属性。对于实例属性:

class X(metaclass=XMeta):
    @DynamicClassAttribute
    def id(self):
        return "Instance {}".format(self.__class__.__name__)

这可能有点矫枉过正,尤其是如果您想远离 metaclasses。这是我想探索的技巧,所以我只是想分享这个隐藏的宝石,以防你能打磨它并让它发光!

>>> X().id
'Instance X'
>>> X.id
'Class X'

瞧...

(Python 3 only) 阐述a pure-Python implementation of @classmethod, we can declare an @class_or_instance_method as a decorator, which is actually a class implementing the attribute descriptor protocol的想法:

import inspect


class class_or_instance_method(object):

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

    def __get__(self, instance, owner):
        if instance is not None:
            class_or_instance = instance
        else:
            class_or_instance = owner

        def newfunc(*args, **kwargs):
            return self.f(class_or_instance, *args, **kwargs)
        return newfunc

class A:
    @class_or_instance_method
    def foo(self_or_cls, a, b, c=None):
        if inspect.isclass(self_or_cls):
            print("Called as a class method")
        else:
            print("Called as an instance method")