foo.bar()和bar(foo)的区别是什么?

9
考虑:
class Parent():
    def __init__(self, last_name, eye_color):
        self.last_name = last_name
        self.eye_color = eye_color

    def show_info(self):
        print("Last Name - "+self.last_name)
        print("Eye Color - "+self.eye_color)

billy_cyrus = Parent("Cyrus", "blue")

上述内容来自Udacity的Python课程。我发现我可以使用以下任意一种方式来调用billy_cyrusshow_info方法:
billy_cyrus.show_info()
Parent.show_info(billy_cyrus)

我很好奇为什么会有两种方法?它们之间有什么区别?如果有的话,什么情况下会使用其中一种?我正在使用Python 3.6,如果有关系的话。


5
完全没有区别。第一个方法只是对后面的方法进行了语法糖处理。这就是self参数的来源。然而,使用Class.methodinstance.method(后者为绑定方法)传递方法作为参数(例如用于map或回调函数)时可能会很有用。 - tobias_k
@tobias_k。那不是严格、追求完美的100%正确。 - Mad Physicist
@MadPhysicist 这就是为什么这是一条评论而不是答案。请随意详细说明。 - tobias_k
2
能够处理对象而不知道它们的确切类别是非常有用的。如果有人想要传递一个“祖父母”或“监护人”代替“父母”,那么Parent.show_info(billy_cyrus)就会出现问题。 - Charles Duffy
@ChrisDecker。您应该考虑选择一个答案。这会让您获得积分,问题会被标记为已回答,回答者会获得积分,并且这只是一件礼貌的事情。 - Mad Physicist
显示剩余3条评论
2个回答

11

就调用方法而言,大多数情况下没有区别。但在底层机制的工作方式上,有一点不同。

由于show_info是一个method,它是类中的descriptor。这意味着当你通过一个instance访问它时,在该实例中没有被另一个attribute遮蔽,.运算符会调用描述符上的__get__方法为该实例创建一个绑定方法。绑定方法基本上是一个闭包,在你提供任何其他参数之前,它会自动传递self参数。你可以像这样看到绑定发生:

>>> billy_cyrus.show_info
<bound method Parent.show_info of <__main__.Parent object at 0x7f7598b14be0>>

每次在类方法上使用.操作符时,都会创建一个不同的闭包。
另一方面,如果通过类对象访问方法,则不会绑定。该方法是一个描述符,只是类的常规属性:
>>> Parent.show_info
<function __main__.Parent.show_info>

您可以通过调用方法的__get__来模拟在调用方法之前绑定方法的确切行为:

>>> bound_meth = Parent.show_info.__get__(billy_cyrus, type(billy_cyrus))
>>> bound_meth
<bound method Parent.show_info of <__main__.Parent object at 0x7f7598b14be0>>

再说一遍,在99.99%的情况下,这对你没有任何影响,因为功能上bound_meth()Parent.bound_meth(billy_cyrus)最终都会使用相同的参数调用相同的基础函数对象。

它有影响的地方

有几个地方需要注意如何调用类方法。一个常见的用例是当您覆盖一个方法,但想要使用父类提供的定义。例如,假设我有一个通过覆盖__setattr__使其"不可变"的类。我仍然可以在实例上设置属性,就像下面展示的__init__方法中一样:

class Test:
    def __init__(self, a):
        object.__setattr__(self, 'a', a)
    def __setattr__(self, name, value):
        raise ValueError('I am immutable!')

如果我在__init__中尝试正常调用__setattr__,比如使用self.a = a,每次都会引发ValueError。但是通过使用object.__setattr__,我可以绕过这个限制。或者,我可以使用super().__setattr__('a', a)达到相同的效果,或者使用self.__dict__['a'] = a实现非常相似的效果。
Silvio Mayolo的答案有另一个很好的例子,您可以故意将类方法用作可应用于多个对象的函数。
另一个地方也很重要(虽然不涉及调用方法),那就是当您使用其他常见描述符property时。与方法不同,属性是data-descriptors。这意味着它们除了__get__之外还定义了__set__方法(和可选的__delete__)。属性创建了一个虚拟属性,其getter和setter是任意复杂的函数,而不仅仅是简单的赋值。要正确使用属性,必须通过实例来进行。例如:
class PropDemo:
    def __init__(self, x=0):
        self.x = x
    @property
    def x(self):
        return self.__dict__['x']
    @x.setter
    def x(self, value):
        if value < 0:
            raise ValueError('Not negatives, please!')
        self.__dict__['x'] = value

现在你可以做这样的事情。
>>> inst = PropDemo()
>>> inst.x
0
>>> inst.x = 3
>>> inst.x
3

如果您尝试通过类访问属性,则可以获取底层描述符对象,因为它将是未绑定的属性:

>>> PropDemo.x
<property at 0x7f7598af00e8>

顺便提一下,在Python中隐藏与__dict__属性相同名称的属性是一个巧妙的技巧,因为类中的数据描述符在__dict__中优先于实例__dict__条目,即使实例__dict__条目优先于类中的非数据描述符。

可能会出现的问题

在Python中,您可以使用实例方法覆盖类方法。这意味着type(foo).bar(foo)foo.bar()根本不调用同一个基础函数。对于magic methods来说这无关紧要,因为它们总是使用前者的调用方式,但对于普通方法调用来说可能会产生很大的影响。

有几种方法可以在实例上重写方法。我认为最直观的方法是将实例属性设置为绑定方法。以下是一个修改后的billy_cyrus示例,假设原始问题中定义了Parent

def alt_show_info(self):
    print('Another version of', self)

billy_cyrus.show_info = alt_show_info.__get__(billy_cyrus, Parent)

在这种情况下,对实例调用方法与对类调用方法将产生完全不同的结果。顺便说一下,这仅适用于方法是非数据描述符的情况。如果它们是数据描述符(具有__set__方法),那么赋值billy_cyrus.show_info = alt_show_info.__get__(billy_cyrus, Parent)将不会覆盖任何内容,而只会重定向到__set__,并且在billy_cyrus的__dict__中手动设置它将被忽略,就像属性一样。
其他资源
以下是一些关于描述符的资源:
Python参考-描述符协议:http://python-reference.readthedocs.io/en/latest/docs/dunderdsc/ (官方?)描述符指南:https://docs.python.org/3/howto/descriptor.html

6

这两种方法没有语义上的区别,完全是风格问题。通常情况下,你会使用billy_cyrus.show_info(),但第二种方法也是被允许的,它使你可以使用Parent.show_info将方法本身作为一等对象来获取。如果不允许这样做,那么像这样的操作就不可能(或者至少会相当困难)。

function = Parent.show_info
so_many_billy_cyrus = [billy_cyrus, billy_cyrus, billy_cyrus]
map(function, so_many_billy_cyrus)

1
如果你说“相当困难”是指“使用lambda b: b.show_info()”……但是,是的,在那种情况下,这种风格可能更受欢迎。 - tobias_k
我很好奇 - 如果您打算像那样使用 show_info,难道不应该将其定义为独立函数而不是实例属性吗? - wwii
一般来说,map 相当于列表推导式,因此 [x.show_info() for x in so_many_billy_cyrus] 可以正常工作。但这只是强调了您关于等价性的原始观点,所以加1。 - Mad Physicist
2
@MadPhysicist 我相信你的意思是“在Python 2中,map等同于列表推导式”。你知道,严格地讲,这样是100%正确的...;-) - tobias_k
1
在Python3中,将[]替换为()即可获得等效的生成器表达式。我的错误。 - Mad Physicist
显示剩余3条评论

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