同名类方法和实例方法

30

我想要做类似这样的事情:

class X:

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

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

现在可以为该类或其实例调用id()

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

显然,这段代码并不能正常工作,但是否有类似的方法使它能够工作呢?

或者是否存在其他的解决办法可以达到相同的效果,而不需要过多地使用“hacky”技巧呢?


3
不行,你只能拥有一个具有特定名称的属性。 - jonrsharpe
相关(非重复):*如何在Python类中检测重复的方法名?* - Peter Mortensen
6个回答

45
类方法和实例方法存在于同一个命名空间中,你不能重复使用这样的名称;在这种情况下,最后一次定义的id将会覆盖之前的定义。
类方法仍然可以在实例上正常工作,但是没有必要创建一个单独的实例方法;只需使用:
class X:
    @classmethod
    def id(cls):
        return cls.__name__

由于该方法仍然与类绑定:
>>> class X:
...     @classmethod
...     def id(cls):
...         return cls.__name__
... 
>>> X.id()
'X'
>>> X().id()
'X'

这是明确记录的:

它可以在类上调用(例如:C.f())或在实例上调用(例如:C().f())。除了类别之外,实例被忽略。

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

如果您需要根据使用位置的不同而使方法以不同方式工作;当在类上访问时绑定到类,当在实例上访问时绑定到实例,则需要创建一个自定义的描述符对象

描述符API 是Python将函数绑定为方法并将 classmethod 对象绑定到类的方式;请参阅 描述符指南

您可以通过创建一个具有`__get__`方法的对象来为方法提供自己的描述符。以下是一个简单的示例,根据上下文将方法绑定到不同的对象。如果`__get__`的第一个参数为`None`,则描述符将被绑定到一个类;否则,它将被绑定到一个实例。
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 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)

这是一个带有可选实例方法的类方法。使用它就像使用一个 `property` 对象一样;用 `@.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值的方式,其中列对象会像这样切换行为;请参阅他们的《混合属性》文档(link1)。这个实现遵循了我上面介绍的hybridmethod类的完全相同模式。
以下是根据要求进行类型提示的版本。这些版本需要您的项目已安装typing_extensions
from typing import Generic, Callable, TypeVar, overload
from typing_extensions import Concatenate, ParamSpec, Self

_T = TypeVar("_T")
_R_co = TypeVar("_R_co", covariant=True)
_R1_co = TypeVar("_R1_co", covariant=True)
_R2_co = TypeVar("_R2_co", covariant=True)
_P = ParamSpec("_P")


class class_or_instancemethod(classmethod[_T, _P, _R_co]):
    def __get__(
        self, instance: _T, type_: type[_T] | None = None
    ) -> Callable[_P, _R_co]:
        descr_get = super().__get__ if instance is None else self.__func__.__get__
        return descr_get(instance, type_)


class hybridmethod(Generic[_T, _P, _R1_co, _R2_co]):
    fclass: Callable[Concatenate[type[_T], _P], _R1_co]
    finstance: Callable[Concatenate[_T, _P], _R2_co] | None
    __doc__: str | None
    __isabstractmethod__: bool

    def __init__(
        self,
        fclass: Callable[Concatenate[type[_T], _P], _R1_co],
        finstance: Callable[Concatenate[_T, _P], _R2_co] | None = None,
        doc: str | None = 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: Callable[Concatenate[type[_T], _P], _R1_co]) -> Self:
        return type(self)(fclass, self.finstance, None)

    def instancemethod(self, finstance: Callable[Concatenate[_T, _P], _R2_co]) -> Self:
        return type(self)(self.fclass, finstance, self.__doc__)

    @overload
    def __get__(self, instance: None, cls: type[_T]) -> Callable[_P, _R1_co]: ...

    @overload
    def __get__(self, instance: _T, cls: type[_T] | None = ...) -> Callable[_P, _R1_co] | Callable[_P, _R2_co]: ...

    def __get__(self, instance: _T, cls: type[_T] | None = None) -> Callable[_P, _R1_co] | Callable[_P, _R2_co]:
        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)

2
问题在于构造函数或其他实例数据中的任何参数都无法从类方法中访问,因此您的“解决方案”非常有限。 - rwst
请参见其他答案的评论。 - rwst
@rwst:那个评论并没有告诉我太多信息。为什么你需要让ClassObject.methodname()instance.methodname()根据绑定的内容表现出不同的行为呢? - Martijn Pieters
@rwst:这是一个相当晦涩的用例,从您的拉取请求中并不清楚为什么Class._sage_()的行为与instance._sage_()不同。无论如何,问题中的用例并不需要访问实例,基本问题在于OP不理解classmethod的工作原理。 - Martijn Pieters
1
@Roelant:我现在已经在答案的末尾添加了带有类型提示注释的版本。 - Martijn Pieters
显示剩余5条评论

26

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

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

1
非常感谢!这解决了SymPy函数对象转换为Sage对象(两个开源数学软件包)的一个烦人的问题。 - rwst

13

可以通过将方法的实例绑定显式地绑定到实例(而不是类)来简洁地完成。当调用Class().foo()时,Python会查找实例的__dict__中的实例属性(因为它会先搜索实例的__dict__),并在Class.__dict__中查找类绑定方法。这样就产生了许多潜在的用例,尽管它们是否属于反模式还有待商榷:

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']]

1
请提供需要翻译的具体内容,以便我为您提供准确的翻译。 - user4698348
1
你能否解释一下为什么这种方式不如其他方式?在我看来,这种方式更加清晰简洁且无需大量模板代码。 - ipetrik
3
我之所以这么认为,主要是因为它更容易被规避,而且也不够严谨:它使用了一种 hack-y 技术来确定它是否是实例,而描述符则通过 Python 自身传递该信息。此外,你需要在每个方法中都进行 self.x = self.__x 的赋值(虽然这可以转移到元类或装饰器中自动完成,我想)... 最后,猴子补丁通常并不那么好。但这只是一个观点——如果你认为这样更清晰简洁,请随意尝试! - user4698348
1
个人而言,我喜欢这种解决方案,因为它不需要阅读或实施代码的人理解描述符。也许我有所遗漏,但它似乎不是“如果你要做类似的事情,你就应该被要求了解描述符”的情况,所以在这里简洁性的优势在我的脑海中占据了重要地位。 - mtraceur
1
但我还没有完全确定是否这种方法是最好的设计,如果是的话,那么何时更好地切换到使用描述符来实现它而不是这种hack的阈值是什么。 - mtraceur
很好的解决方案,你仍然可以通过self.class.methodname从实例中调用类方法。 - ARR

5

"types"自Python 3.4起提供了一些非常有趣的功能:DynamicClassAttribute

它虽然不能完全满足您的需求,但似乎与之密切相关,您可能需要微调我的元类,但大致上可以做到这一点;

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 X(metaclass=XMeta):
    @DynamicClassAttribute
    def id(self):
        return "Instance {}".format(self.__class__.__name__)

如果你想避免元类,这可能有点过头了。但这是一个我想探索的技巧,所以我想分享这个隐藏的宝石,以防你能够抛光并使其闪耀!

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

看这里...


你需要元类吗?从文档上看,似乎只需修改 __getattr__ 即可... - DylanYoung
@DylanYoung 元类是必要的,因为 DynamicClassAttribute 将属性访问发送到 class__getattr__,而该方法在其元类中定义。 - jirassimok

4
在你的例子中,你可以完全删除第二个方法,因为staticmethod和classmethod执行相同的操作。
如果你想让它们执行不同的操作:
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

1
我喜欢这个解决方案,但是遗憾的是,如果你想让 id 接受位置参数,它就不起作用了。 - Vedran Šego

3
(仅适用于 Python 3)扩展在 Python 中实现 @classmethod 的纯 Python 实现 的想法,我们可以将 @class_or_instance_method 声明为一个装饰器,实际上是一个实现属性描述协议的类:
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")

“wrt”是什么意思?我无论如何都无法解码这个缩写。 - mtraceur
关于(https://en.wiktionary.org/wiki/WRT#English)。例如,我们想要调用`@class_or_instance_method`的类或实例。 - DomQ
哦,我曾经想过“关于”,但是“[我们想要调用装饰函数的]关于[对象]”这种联系对我来说非常不明显。 - mtraceur
现在你已经明确了整个短语,我可以理解它的吸引力。感谢您的澄清。个人而言,我仍然会使用类似于class_or_instance这样的变量名,而不是wrt,因为像我这样的读者将难以推断出短语,从而使变量名有意义。 - mtraceur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接