为什么Python使用“魔术方法”?


99

我最近一直在使用Python,而我发现有点奇怪的是,广泛使用了“魔术方法”,例如,使其长度可用,一个对象实现一个方法,def __len__(self)然后在你写len(obj)

我只是想知道为什么对象不能简单地定义一个len(self)方法并直接将其作为对象的成员来调用,例如obj.len()?我敢肯定,Python这么做的确有充分的理由,但是作为一个新手,我还没有弄清楚它们到底是什么。


4
我觉得一般的原因是)的历史和b)类似len()reversed()适用于许多类型的对象,但在方法,如append()仅适用于序列等
格兰特保罗

Answers:


64

AFAIK len在这方面很特别,并且具有历史渊源。

这是FAQ中的报价:

为什么Python使用方法来实现某些功能(例如list.index()),却使用其他方法(例如len(list))呢?

主要原因是历史。函数用于那些对一组类型通用的操作,即使对于根本没有方法的对象(例如,元组),这些功能也可以使用。使用Python的功能部件(map(),apply()等)时,具有可以轻松应用于对象的不定形集合的函数也很方便。

实际上,将len(),max(),min()实现为内置函数实际上比将它们实现为每种类型的方法要少。人们可能会质疑个别情况,但这是Python的一部分,现在进行这样的基本更改为时已晚。必须保留功能以避免大量代码损坏。

其他“魔术方法”(在Python民俗中实际上称为特殊方法)很有道理,其他语言中也存在类似的功能。它们通常用于使用特殊语法时隐式调用的代码。

例如:

  • 重载运算符(存在于C ++和其他语言中)
  • 构造函数/析构函数
  • 用于访问属性的挂钩
  • 元编程工具

等等...


2
对于Python这样的一些优点(尽管我确实承认英语需要工作),Python和最低惊讶原则是一本不错的书。基本要点:它允许标准库实现大量的代码,这些代码变得非常非常可重用,但仍然可以重写。
jpmc26 2014年

20

从Python的Zen中:

面对模棱两可的想法,拒绝猜测的诱惑。
应该有一种-最好只有一种-显而易见的方法。

这是原因之一-自定义方法,开发人员可以自由选择不同的方法的名称,如getLength()length()getlength()或任何责任。Python强制执行严格的命名,以便len()可以使用通用功能。

这是常见的许多类型的对象,所有的操作都投入到神奇的方法,比如__nonzero____len__或者__repr__。不过,它们大多是可选的。

运算符重载也可以通过魔术方法(例如__le__)完成,因此也可以将它们用于其他常见操作。


这是一个令人信服的论点。更令人满意的是“ Guido并不真的相信OO” ...(就像我在其他地方看到的那样)。
安迪·海登

15

Python使用“魔术方法”一词,因为这些方法确实可以为您的程序带来魔术。使用Python的魔术方法的最大优点之一是,它们提供了一种使对象表现为内置类型的简单方法。这意味着您可以避免执行基本运算符的丑陋,违反直觉和非标准的方式。

考虑以下示例:

dict1 = {1 : "ABC"}
dict2 = {2 : "EFG"}

dict1 + dict2
Traceback (most recent call last):
  File "python", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict' and 'dict'

这会产生错误,因为字典类型不支持加法。现在,让我们扩展字典类并添加“ __add__”魔术方法:

class AddableDict(dict):

    def __add__(self, otherObj):
        self.update(otherObj)
        return AddableDict(self)


dict1 = AddableDict({1 : "ABC"})
dict2 = AddableDict({2 : "EFG"})

print (dict1 + dict2)

现在,它给出以下输出。

{1: 'ABC', 2: 'EFG'}

因此,通过添加此方法,突然发生了魔术,而您早些时候遇到的错误也消失了。

我希望,这使您明白了。有关更多信息,请参阅:

Python魔术方法指南(Rafe Kettler,2012)


9

这些功能中的某些功能不仅仅可以实现单个方法(在超类上没有抽象方法)也可以实现。例如,bool()行为类似于:

def bool(obj):
    if hasattr(obj, '__nonzero__'):
        return bool(obj.__nonzero__())
    elif hasattr(obj, '__len__'):
        if obj.__len__():
            return True
        else:
            return False
    return True

您还可以100%确定bool()将始终返回True或False;如果您依靠一种方法,则不能完全确定自己会得到什么。

具有相对复杂的实现一些其他功能(不是底层魔术方法更加复杂,很可能是)是iter()cmp(),和所有的属性的方法(getattrsetattrdelattr)。诸如此类的事物int在执行强制时也可以访问魔术方法(您可以实现__int__),但是作为类型承担双重责任。 len(obj)实际上是一种我认为与没什么不同的情况obj.__len__()


2
而不是hasattr()我会使用try:/ except AttributeError:,而不是if obj.__len__(): return True else: return False我会说,return obj.__len__() > 0但这只是样式问题。
克里斯·卢茨

在python 2.6(btw bool(x)所指x.__nonzero__())中,您的方法无效。bool实例具有一个method __nonzero__(),一旦obj是bool,您的代码就会继续调用自身。也许bool(obj.__bool__())应该像对待您一样对待它__len__?(或者此代码是否实际上适用于Python 3?)
Ponkadoodle 2010年

bool()的循环性质在某种程度上是有意荒谬的,以反映该定义的特殊循环性质。有一个论点认为它应该简单地视为原始。
伊恩·比金

len(x)和之间的唯一区别(当前)x.__len__()是,前者将针对超过的长度引发OverflowError sys.maxsize,而后者通常不会针对以Python实现的类型。但是,那不是功能,而是更多的错误(例如,Python 3.2的range对象可以处理任意较大的范围,但是len与它们一起使用可能会失败。但是,它们也会失败__len__,因为它们是在C中而不是在Python中实现的)
ncoghlan

4

它们并不是真正的“魔术名称”。它只是对象必须实现以提供给定服务的接口。从这个意义上讲,它们没有比您必须重新实现的任何预定义接口定义更神奇。


1

尽管原因主要是历史性的,但Python的特性有其特殊之处 len使得使用函数而不是适当的方法成为可能。

Python中的某些操作被实现为方法,例如list.indexdict.append,而另一些则是可调用和神奇的方法来实现,例如striterreversed。两组之间的差异足够大,因此可以采用不同的方法:

  1. 它们很常见。
  2. strint而朋友是类型。调用构造函数更有意义。
  3. 实现与函数调用不同。例如,iter可能__getitem__在if __iter__不可用时调用,并支持方法调用中不适合的其他参数。出于相同的原因it.next(),已更改为next(it)在最新版本的Python中 -更有意义。
  4. 其中一些是运营商的近亲。有用于调用的语法__iter____next__-称为for循环。为了一致性,功能更好。而且,它对于某些优化来说更好。
  5. 有些功能在某种程度上与其余功能过于相似- repr行为相似str。有str(x)x.repr()将造成混乱。
  6. 其中有些很少使用实际的实现方法,例如 isinstance
  7. 其中一些是实际操作员,getattr(x, 'a')是另一种操作方式,x.a并且getattr具有许多上述质量。

我个人称第一类为方法,第二类为运算符。这不是一个很好的区别,但我希望它能有所帮助。

话虽如此,len这并不完全适合第二组。它与第一个操作更接近,唯一的区别是,它比几乎所有操作都更常见。但是它唯一要做的就是调用__len__,并且非常接近L.index。但是,有一些差异。例如,__len__可能会调用其他功能的实现,例如bool,如果调用了该方法,则len可能会bool(x)与customlen做完全不同的事情的方法。

简而言之,您可以拥有一组类可能实现的非常通用的功能,这些功能可以通过操作员,在对象构造期间通过特殊功能(通常比实现操作员做得更多)执行的特殊功能访问,并且在所有这些操作中具有一些共同的特征。其余所有方法。这len是该规则的一个例外。


0

上面的两个帖子没有太多补充,但是所有的“魔术”功能根本不是魔术。它们是__builtins__模块的一部分,该模块在解释器启动时隐式/自动导入。即:

from __builtins__ import *

程序每次启动之前都会发生。

我一直认为,如果Python仅对交互式外壳执行此操作,并且需要脚本从所需的内置文件中导入各个部分,那会更正确。同样,不同的__ main__处理在shell还是交互式中会更好。无论如何,请检查所有功能,看看没有它们的情况是什么:

dir (__builtins__)
...
del __builtins__
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.