5.6. 特殊类方法

除了普通的类方法之外,Python 类还可以定义许多特殊方法。特殊方法不是由您的代码直接调用(像普通方法那样),而是在特定情况下或使用特定语法时由 Python 为您调用。

正如您在上一节中所见,普通方法在将字典包装到类中起到了很大的作用。但是,仅凭普通方法是不够的,因为除了调用方法之外,您还可以对字典执行很多操作。首先,您可以使用不包含显式调用方法的语法来获取设置项。这就是特殊类方法的用武之地:它们提供了一种将非方法调用语法映射到方法调用的方法。

5.6.1. 获取和设置项

示例 5.12. __getitem__ 特殊方法

    def __getitem__(self, key): return self.data[key]
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__getitem__("name") 1
'/music/_singles/kairo.mp3'
>>> f["name"]             2
'/music/_singles/kairo.mp3'
1 __getitem__ 特殊方法看起来很简单。与普通方法 clearkeysvalues 一样,它只是重定向到字典以返回其值。但是它是如何被调用的呢?好吧,您可以直接调用 __getitem__,但在实践中您实际上不会这样做;我只是在这里这样做是为了向您展示它是如何工作的。使用 __getitem__ 的正确方法是让 Python 为您调用它。
2 这看起来就像您用来获取字典值的语法,实际上它也返回了您期望的值。但这里有一个缺失的环节:在幕后,Python 已将此语法转换为方法调用 f.__getitem__("name")。这就是为什么 __getitem__ 是一个特殊类方法的原因;您不仅可以自己调用它,还可以通过使用正确的语法让 Python 为您调用它。

当然,Python 有一个 __setitem__ 特殊方法与 __getitem__ 配合使用,如下一个示例所示。

示例 5.13. __setitem__ 特殊方法

    def __setitem__(self, key, item): self.data[key] = item
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__setitem__("genre", 31) 1
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':31}
>>> f["genre"] = 32            2
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':32}
1 __getitem__ 方法一样,__setitem__ 只是重定向到真实的字典 self.data 来完成其工作。与 __getitem__ 一样,您通常不会像这样直接调用它;当您使用正确的语法时,Python 会为您调用 __setitem__
2 这看起来像普通的字典语法,当然除了 f 实际上是一个试图伪装成字典的类,而 __setitem__ 是这种伪装的重要组成部分。这行代码实际上在幕后调用了 f.__setitem__("genre", 32)

__setitem__ 是一个特殊的类方法,因为它会被自动调用,但它仍然是一个类方法。就像在 UserDict 中定义 __setitem__ 方法一样容易,您可以在后代类中重新定义它以覆盖祖先方法。这允许您定义在某些方面像字典一样工作的类,但定义了超出内置字典的自身行为。

这个概念是您在本章中学习的整个框架的基础。每种文件类型都可以有一个处理程序类,该类知道如何从特定类型的文件中获取元数据。一旦知道了一些属性(如文件的名称和位置),处理程序类就知道如何自动派生其他属性。这是通过覆盖 __setitem__ 方法、检查特定键并在找到它们时添加额外的处理来完成的。

例如,MP3FileInfoFileInfo 的后代。当设置 MP3FileInfoname 时,它不会像祖先 FileInfo 那样只设置 name 键;它还会在文件中查找 MP3 标签并填充一整套键。下一个示例展示了这是如何工作的。

示例 5.14. 在 MP3FileInfo 中覆盖 __setitem__

    def __setitem__(self, key, item):         1
        if key == "name" and item:            2
            self.__parse(item)                3
        FileInfo.__setitem__(self, key, item) 4
1 请注意,此 __setitem__ 方法的定义与祖先方法完全相同。这很重要,因为 Python 将为您调用该方法,并且它希望使用一定数量的参数来定义它。(从技术上讲,参数的名称无关紧要;重要的是参数的数量。)
2 这是整个 MP3FileInfo 类的关键:如果您要为 name 键赋值,您希望做一些额外的事情。
3 您为 name 所做的额外处理封装在 __parse 方法中。这是在 MP3FileInfo 中定义的另一个类方法,当您调用它时,您使用 self 来限定它。仅仅调用 __parse 将会查找在类外部定义的普通函数,这不是您想要的。调用 self.__parse 将会查找在类中定义的类方法。这不是什么新鲜事;您以相同的方式引用数据属性
4 在完成此额外处理之后,您要调用祖先方法。请记住,这在 Python 中永远不会自动完成;您必须手动完成。请注意,您正在调用直接祖先 FileInfo,即使它没有 __setitem__ 方法。没关系,因为 Python 会沿着祖先树向上查找,直到找到具有您正在调用的方法的类,因此这行代码最终会找到并调用在 UserDict 中定义的 __setitem__
Note
在类中访问数据属性时,您需要限定属性名称:self.attribute。在类中调用其他方法时,您需要限定方法名称:self.method

示例 5.15. 设置 MP3FileInfoname

>>> import fileinfo
>>> mp3file = fileinfo.MP3FileInfo()                   1
>>> mp3file
{'name':None}
>>> mp3file["name"] = "/music/_singles/kairo.mp3"      2
>>> mp3file
{'album': 'Rave Mix', 'artist': '***DJ MARY-JANE***', 'genre': 31,
'title': 'KAIRO****THE BEST GOA', 'name': '/music/_singles/kairo.mp3',
'year': '2000', 'comment': 'http://mp3.com/DJMARYJANE'}
>>> mp3file["name"] = "/music/_singles/sidewinder.mp3" 3
>>> mp3file
{'album': '', 'artist': 'The Cynic Project', 'genre': 18, 'title': 'Sidewinder', 
'name': '/music/_singles/sidewinder.mp3', 'year': '2000', 
'comment': 'http://mp3.com/cynicproject'}
1 首先,您创建一个 MP3FileInfo 的实例,而不传递文件名。(您可以这样做,因为 __init__ 方法的 filename 参数是可选的。)由于 MP3FileInfo 没有自己的 __init__ 方法,Python 会沿着祖先树向上查找并找到 FileInfo__init__ 方法。此 __init__ 方法手动调用 UserDict__init__ 方法,然后将 name 键设置为 filename,它是 None,因为您没有传递文件名。因此,mp3file 最初看起来像一个只有一个键 name 的字典,其值为 None
2 现在真正的乐趣开始了。设置 mp3filename 键会触发 MP3FileInfo(而不是 UserDict)上的 __setitem__ 方法,该方法注意到您正在使用真实值设置 name 键,并调用 self.__parse。虽然您还没有跟踪 __parse 方法,但您可以从输出中看到它设置了其他几个键:albumartistgenretitleyearcomment
3 修改 name 键将再次经历相同的过程:Python 调用 __setitem__,后者调用 self.__parse,后者设置所有其他键。