Python中是否存在三种以上的方法类型?

5
我明白在Python中至少有3种不同的方法,它们有不同的第一个参数:
  1. 实例方法 - 实例,即self
  2. 类方法 - 类,即cls
  3. 静态方法 - 无
下面的Test类中实现了这些经典方法,包括一个普通方法:
class Test():

    def __init__(self):
        pass

    def instance_mthd(self):
        print("Instance method.")

    @classmethod
    def class_mthd(cls):
        print("Class method.")

    @staticmethod
    def static_mthd():
        print("Static method.")

    def unknown_mthd():
        # No decoration --> instance method, but
        # No self (or cls) --> static method, so ... (?)
        print("Unknown method.")

在Python 3中,可以安全地调用unknown_mthd,但在Python 2中会引发错误:
>>> t = Test()

>>> # Python 3
>>> t.instance_mthd()
>>> Test.class_mthd()
>>> t.static_mthd()
>>> Test.unknown_mthd()

Instance method.
Class method.
Static method.
Unknown method.

>>> # Python 2
>>> Test.unknown_mthd()    
TypeError: unbound method unknown_mthd() must be called with Test instance as first argument (got nothing instead)

这个错误表明在Python 2中没有意图使用这种方法。也许现在允许这样做是由于Python 3中取消了未绑定方法(REF 001)。此外,unknown_mthd不接受args,并且可以像staticmethod一样由类调用,Test.unknown_mthd()。但它不是一个显式的staticmethod(没有装饰器)。
问题
  1. 在Python 3的设计中,以这种方式创建方法(没有args,而不明确地装饰为staticmethod)是有意的吗?更新
  2. 在经典方法类型中,unknown_mthd是哪种类型的方法?
  3. 为什么可以通过类调用unknown_mthd而不传递参数?

一些初步检查得出的结果并不确定:
>>> # Types
>>> print("i", type(t.instance_mthd))
>>> print("c", type(Test.class_mthd))
>>> print("s", type(t.static_mthd))
>>> print("u", type(Test.unknown_mthd))                             
>>> print()

>>> # __dict__ Types, REF 002
>>> print("i", type(t.__class__.__dict__["instance_mthd"]))
>>> print("c", type(t.__class__.__dict__["class_mthd"]))
>>> print("s", type(t.__class__.__dict__["static_mthd"]))
>>> print("u", type(t.__class__.__dict__["unknown_mthd"]))          
>>> print()

i <class 'method'>
c <class 'method'>
s <class 'function'>
u <class 'function'>

i <class 'function'>
c <class 'classmethod'>
s <class 'staticmethod'>
u <class 'function'>

第一组类型检查表明unknown_mthd类似于一个静态方法。第二组检查表明它更像是一个实例方法。我不确定这个方法是什么,也不知道为什么要使用它而不是经典的方法。我希望能得到一些关于如何检查和更好地理解它的建议。谢谢。
REF 001: Python 3中的新功能:“未绑定的方法”已被移除 REF 002: 如何在Python 3中区分实例方法、类方法、静态方法或函数? REF 003: Python中@staticmethod有什么用处?

我认为这个答案是一个很好的解释:https://dev59.com/Jmgt5IYBdhLWcg3w0wzB - taras
感谢@Taras。您链接中的OP描述了“常规方法”的方式,就像在此处定义unknown_mthd一样,但是被接受的答案实际上是指一个实例方法。然而,t.unknown_mthd()会引发错误。 - pylang
3个回答

5
一些背景:在Python 2中,“常规”实例方法可以产生两种方法对象,具体取决于您是通过实例还是类访问它们。如果您执行inst.meth(其中inst是类的实例),则会得到一个绑定的方法对象,它跟踪其附加的实例,并将其作为self传递。如果您执行Class.meth(其中Class是类),则会得到一个未绑定的方法对象,它没有固定的self值,但仍会检查在调用它时是否传递了适当类的self
在Python 3中,取消了未绑定的方法。现在只需执行Class.meth即可获得“普通”函数对象,根本没有进行任何参数检查。
“在Python 3的设计中,这种制作方法是有意的吗?”
如果你的意思是,移除未绑定方法是有意的,那么答案是肯定的。你可以从Guido在邮件列表中的讨论中看到。基本上决定移除未绑定方法是为了减少复杂性而获得很少的好处。
未知方法是哪种经典方法类型?
它是一个实例方法,但是是一个不完整的方法。当你访问它时,会创建一个绑定方法对象,但由于它不接受任何参数,因此无法接受self参数,也无法成功调用。
为什么类可以调用unknown_mthd而不传递参数?
在Python 3中,未绑定的方法已被移除,因此Test.unkown_mthd只是一个普通函数。没有包装来处理self参数,因此你可以将其作为一个不接受任何参数的普通函数进行调用。在Python 2中,Test.unknown_mthd是一个未绑定的方法对象,它有一个检查,强制要求传递适当类的self参数;由于该方法不接受任何参数,因此此检查失败。

谢谢。对于你的第一个回答,我的意思是在没有明确装饰为静态方法的情况下创建没有参数的方法是否有意义。我知道取消未绑定方法的情况。 - pylang
@pylang:这并不是一个特定的目标,但有意删除了阻止您这样做的检查类型。因此,基本上他们并没有特别想让这成为可能,但他们觉得现有机制使其不可能的复杂性不值得引入。 - BrenBarn
是的,我可以确认常规函数在类上可以正常工作。例如,如果未知方法接受参数 def unknown_mthd(foo): pass,那么 Test.unknown_mthd("bar") 就可以正常工作。但是如果不接受 self,我不确定这是否可以被称为实例方法。 - pylang
@pylang:我想这取决于你如何定义“实例方法”,是根据它的定义方式还是访问方式。从定义上来说,它是一个实例方法。如果你在实例上访问它,它将不起作用。但是,你可以在类上访问它,在这种情况下,它的行为类似于静态方法。但它并不是真正的静态方法,因为真正的静态方法也可以在实例上调用。 - BrenBarn

1

@BrenBarn做得很好,回答了你的问题。然而,这个答案添加了大量细节:

首先,绑定和非绑定方法的变化是与版本相关的,与新式类或经典类无关:

2.X经典类默认

>>> class A:
...     def meth(self): pass
... 
>>> A.meth
<unbound method A.meth>

>>> class A(object):
...     def meth(self): pass
... 
>>> A.meth
<unbound method A.meth>

3.X默认使用新式类

>>> class A:
...     def meth(self): pass
... 
>>> A.meth
<function A.meth at 0x7efd07ea0a60>

你已经在问题中提到了这一点,再次提醒并不会有任何损失。


>>> # Python 2
>>> Test.unknown_mthd()    
TypeError: unbound method unknown_mthd() must be called with Test instance as first argument (got nothing instead)

此外,unknown_mthd 不接受参数,并且可以像静态方法 staticmethod 一样绑定到一个类上,Test.unknown_mthd()。然而,它不是显式的 staticmethod(没有装饰器)。 unknown_meth 不接受参数,通常是因为您定义了该函数而不带任何参数。请注意,静态方法以及您编码的 unknown_meth 方法在通过类名引用它们时(例如,Test.unknown_meth)不会自动绑定到类。在 Python 3.X 中,Test.unknow_meth 返回一个简单的函数对象,而不是绑定到类的方法。
1 - 在 Python 3 的设计中,这种方式创建方法(不带参数,同时未明确装饰为静态方法)是否是有意为之?已更新
我不能代表CPython开发人员,也不会声称自己是他们的代表,但从我作为Python程序员的经验来看,似乎他们想要摆脱一种不好的限制,特别是考虑到Python极其动态,不是一种限制性语言。为什么要测试传递给类方法的对象类型,从而限制该方法只能用于特定类的实例?类型测试消除了多态性。当通过类获取方法时,如果只返回一个功能上表现为静态方法的简单函数,那就够好了,你可以认为unknown_meth在3.X中是静态方法,只要小心不要通过Test的实例获取它即可。

2- 在经典方法类型中,unknown_mthd属于哪种类型的方法?

在3.X中:

>>> from types import *
>>> class Test:
...     def unknown_mthd(): pass
... 
>>> type(Test.unknown_mthd) is FunctionType 
True

你可以看到在3.X中它只是一个函数。接着上一个2.X版本的会话:

>>> type(Test.__dict__['unknown_mthd']) is FunctionType
True
>>> type(Test.unknown_mthd) is MethodType
True
unknown_mthd是一个简单的函数,存在于Test__dict__中,实际上就是存在于Test的命名空间字典中的简单函数。那么,什么时候它会成为MethodType的一个实例呢?当您从类本身获取方法属性时(返回一个未绑定的方法)或其实例(返回一个绑定的方法)时,它将成为MethodType的一个实例。在3.X中,Test.unknown_mthd是一个简单的函数--FunctionType的一个实例,而Test().unknown_mthdMethodType的一个实例,它保留了类Test的原始实例,并在函数调用时隐式地将其添加为第一个参数。

3-为什么可以通过类调用unknown_mthd而不传递参数?

再次强调,因为Test.unknown_mthd只是3.X版本下的简单函数。而在2.X版本中,unknown_mthd不是一个简单的函数,在调用时必须传递一个Test实例。


你提出了一个很好的观点 - Test.method并不意味着method绑定到一个类上。这可以通过使用inspect.getmembers()来看到。也许更清楚的说法是它可以从类中访问。 - pylang
在类unknown_mthd中有一个函数,似乎仍然是一个函数,显然是移除未绑定方法的副产品。这引发了一个问题,如果它们在Python 2中无法使用,那么是否有用于这种静态方法(或无self实例方法)的实用程序?您可以简单地使用staticmethods。非常感谢。 - pylang
@pylang 你可以将这些简单的方法视为静态方法,但实例有时无法调用它们,在类“Test”中:def unknown_mthd(*args): ...适用于Test.unknown_mthd()Test().unknown_mthd()。我理解你的困惑,为什么要在Python中添加冗余?今天的Python就是这样,看看我们有多少种字符串格式化方式!我并不是指特定的功能或2.X版本中未绑定方法的删除,删除未绑定方法消除了不必要的限制。 - GIZ
我无法理解这种方法的实用性,除了引入预期的副作用之外。与每次迭代字符串格式不断改进的情况不同,这些“有限静态方法”不能在实例上工作,似乎是一种退步。如果您知道应用程序,请分享您的想法。谢谢。 - pylang
此时我可以想象使用此功能来实现既是静态方法又是实例方法的类方法,或者仅作为静态方法,但要小心不要使用实例调用它,当然这并不总是能保证的:请参见file.py - GIZ

1

Python中有超过三种类型的方法吗?

是的。除了你提到的三种内置方法(实例方法、类方法和静态方法)外,还有一种名为@property的方法,而且任何人都可以定义新的方法类型。

一旦你理解了如何进行这样的操作,就很容易解释为什么在Python 3中从类中调用unknown_mthd是可行的。

一种新的方法类型

假设我们想要创建一种新的方法类型,称之为optionalselfmethod,以便我们可以像这样使用:

class Test(object):
    @optionalselfmethod
    def optionalself_mthd(self, *args):
        print('Optional-Self Method:', self, *args)

使用方法如下:
In [3]: Test.optionalself_mthd(1, 2)
Optional-Self Method: None 1 2

In [4]: x = Test()

In [5]: x.optionalself_mthd(1, 2)
Optional-Self Method: <test.Test object at 0x7fe80049d748> 1 2

In [6]: Test.instance_mthd(1, 2)
Instance method: 1 2
optionalselfmethod在实例调用时像普通的实例方法一样工作,但在类调用时,它总是接收第一个参数的None。如果它是一个普通的实例方法,你总是必须为self参数传递一个明确的值才能使其工作。
那么这是如何工作的呢?你如何创建这样一种新的方法类型?
描述符协议
当Python查找实例的字段时,即当你执行x.whatever时,它会在几个地方进行检查。它当然会检查实例的__dict__,但它也会检查对象的类及其基类的__dict__。在实例字典中,Python只是寻找值,因此如果x.__dict__['whatever']存在,那就是该值。然而,在类字典中,Python正在寻找一个实现了描述符协议的对象。

描述器协议是所有三种内置方法的工作方式,它是@property的工作方式,也是我们的特殊optionalselfmethod的工作方式。

基本上,如果类字典中有一个正确命名的值1,Python会检查它是否有一个__get__方法,并像这样调用它:type(x).whatever.__get__(x, type(x)) 然后,从__get__返回的值被用作字段值。

例如,一个总是返回3的微不足道的描述器:

class GetExample:
    def __get__(self, instance, cls):
        print("__get__", instance, cls)
        return 3

class Test:
   get_test = GetExample()

使用方法如下:

In[22]: x = Test()

In[23]: x.get_test
__get__ <__main__.Test object at 0x7fe8003fc470> <class '__main__.Test'>
Out[23]: 3

请注意,描述符需要同时使用实例和类类型调用。它也可以在类上使用:
In [29]: Test.get_test
__get__ None <class '__main__.Test'>
Out[29]: 3

当描述符用于类而不是实例时,__get__方法的self参数为None,但仍然会得到类参数。
这允许简单实现方法:函数只需实现描述符协议。当您在函数上调用__get__时,它返回一个绑定到实例的方法。如果实例为None,则返回原始函数。您实际上可以自己调用__get__来查看这一点:
In [30]: x = object()

In [31]: def test(self, *args):
    ...:     print(f'Not really a method: self<{self}>, args: {args}')
    ...:     

In [32]: test
Out[32]: <function __main__.test>

In [33]: test.__get__(None, object)
Out[33]: <function __main__.test>

In [34]: test.__get__(x, object)
Out[34]: <bound method test of <object object at 0x7fe7ff92d890>>

@classmethod@staticmethod很相似。这些装饰器创建代理对象,具有提供不同绑定的__get__方法。类方法的__get__将方法绑定到实例上,而静态方法的__get__不会绑定到任何东西,即使在实例上调用时也是如此。

可选自我方法实现

我们可以做类似的事情来创建一个新的方法,该方法可选择地绑定到实例。

import functools

class optionalselfmethod:

  def __init__(self, function):
    self.function = function
    functools.update_wrapper(self, function)

  def __get__(self, instance, cls):
    return boundoptionalselfmethod(self.function, instance)

class boundoptionalselfmethod:

  def __init__(self, function, instance):
    self.function = function
    self.instance = instance
    functools.update_wrapper(self, function)

  def __call__(self, *args, **kwargs):
    return self.function(self.instance, *args, **kwargs)

  def __repr__(self):
    return f'<bound optionalselfmethod {self.__name__} of {self.instance}>'

当您使用optionalselfmethod修饰函数时,该函数将被我们的代理替换。该代理保存原始方法并提供一个__get__方法,该方法返回一个boudnoptionalselfmethod。当我们创建一个boundoptionalselfmethod时,我们告诉它要调用的函数和作为self传递的值。最后,调用boundoptionalselfmethod会调用原始函数,但是将实例或None插入到第一个参数中。

具体问题

在Python 3的设计中,没有显式地将方法标记为静态方法而采用这种方式(不带参数)是否是有意的?已更新

我相信这是有意为之的,然而意图应该是为了消除未绑定方法。在Python 2和Python 3中,def 始终创建一个函数(您可以通过检查类型的__dict__来看到这一点:即使Test.instance_mthd返回<unbound method Test.instance_mthd>Test.__dict__['instance_mthd']仍然是<function instance_mthd at 0x...>)。
在Python 2中,function__get__方法总是返回一个instancemethod,即使通过类访问时也是如此。当通过实例访问时,该方法将绑定到该实例。当通过类访问时,该方法是未绑定的,并包含一个机制,用于检查第一个参数是否是正确类的实例。
在Python 3中,function__get__方法在通过类访问时将返回原始函数,而在通过实例访问时将返回一个method
我不知道确切的理由,但我猜测类级函数的第一个参数的类型检查被认为是不必要的,甚至可能是有害的;毕竟Python允许鸭子类型。
在经典方法类型中,unknown_mthd 是哪种类型的方法? unknown_mthd 是一个普通的函数,就像任何普通的实例方法一样。只有在通过实例调用时才会失败,因为当 method.__call__ 尝试使用绑定的实例调用 function unknown_mthd 时,它没有足够的参数来接收 instance 参数。
为什么可以在不传递参数的情况下通过类调用 unknown_mthd?
因为它只是一个普通的function,和其他任何function一样。只是当作为实例方法使用时,它没有足够的参数可以正确工作。
你可能注意到,无论是通过实例还是类调用,classmethodstaticmethod 都是相同的,而 unknown_mthd 只有在通过类调用时才能正常工作,在通过实例调用时会失败。
如果一个名称在实例字典中有值并且在类字典中有描述符,则使用哪个取决于描述符的类型。如果描述符仅定义了__get__,则使用实例字典中的值。如果描述符还定义了__set__,那么它是数据描述符,描述符总是胜出。这就是为什么您可以覆盖方法但不能覆盖@property的原因;方法只定义了__get__,所以您可以将东西放入实例字典中的相同命名槽中,而@properties定义了__set__,所以即使它们是只读的,您也永远不会从实例__dict__中获取值,即使您先前绕过属性查找并使用例如x.__dict__['whatever'] = 3在字典中插入了一个值。

你的解释带有描述符,这正是我所需要的深度。我知道描述符起到了一定作用,但我正在思考为什么不使用常规的staticmethod?顺便说一下:我已经修复了标题中的拼写错误。你可能希望编辑你的第一行以反映这个变化。 - pylang
@pylang 你的意思是为什么不使用常规的staticmethod?用于什么目的? - zstewart
我不认为unknown_mthd这样的函数有什么用处。一个实例或静态方法应该足够。 - pylang
@pylang 我不认为我说过这会有用。只是因为方法定义的方式(方法只是包装函数,将特定值作为第一个参数插入,如果需要,您也可以自己这样做),所以可以创建这样的函数。没有特别的用途,但是防止您在类体中创建零参数函数需要语言具有特殊情况逻辑,而不是让 def 始终只创建一个 function - zstewart

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