5.5. 探索 UserDict:一个包装类

正如您所见,FileInfo 是一个表现得像字典的类。为了进一步探讨这一点,让我们来看看 UserDict 模块中的 UserDict 类,它是 FileInfo 类的父类。这没什么特别的;这个类是用 Python 编写的,并像任何其他 Python 代码一样存储在一个 .py 文件中。具体来说,它存储在您的 Python 安装目录下的 lib 目录中。

Tip
在 Windows 上的 ActivePython IDE 中,您可以通过选择 文件->Locate... (Ctrl-L) 快速打开库路径中的任何模块。

示例 5.9. 定义 UserDict


class UserDict:                                1
    def __init__(self, dict=None):             2
        self.data = {}                         3
        if dict is not None: self.update(dict) 4 5
1 请注意,UserDict 是一个基类,它没有继承自任何其他类。
2 这是您在 FileInfo 类中重写__init__ 方法。请注意,此父类中的参数列表与子类中的参数列表不同。没关系;每个子类都可以有自己的一组参数,只要它使用正确的参数调用父类即可。在这里,父类有一种定义初始值的方法(通过在 dict 参数中传递字典),而 FileInfo 没有使用这种方法。
3 Python 支持数据属性(在 JavaPowerbuilder 中称为“实例变量”,在 C++ 中称为“成员变量”)。数据属性是由类的特定实例保存的数据片段。在这种情况下,UserDict 的每个实例都将有一个数据属性 data。要从类外部的代码中引用此属性,可以使用实例名称对其进行限定,例如 instance.data,这与使用模块名称限定函数的方式相同。要从类内部引用数据属性,请使用 self 作为限定符。按照惯例,所有数据属性都在 __init__ 方法中初始化为合理的值。但是,这不是必需的,因为数据属性(如局部变量)在 首次赋值时 就会存在。
4 update 方法是一个字典复制器:它将一个字典中的所有键和值复制到另一个字典中。这不会 首先清除目标字典;如果目标字典中已经有一些键,则来自源字典中的键将被覆盖,但其他键将保持不变。将 update 视为合并函数,而不是复制函数。
5 这是一种您以前可能没有见过的语法(我在本书的示例中没有使用过)。它是一个 if 语句,但它没有在下一行开始缩进的代码块,而是在冒号后面的同一行只有一个语句。这是完全合法的语法,当代码块中只有一个语句时,可以使用这种快捷方式。(这就像在 C++ 中指定一个没有花括号的语句一样。)您可以使用这种语法,也可以在后续行中使用缩进代码,但不能对同一个代码块同时使用这两种语法。
Note
JavaPowerbuilder 支持按参数列表进行函数重载, 一个类可以有多个同名但参数数量不同或参数类型不同的方法。其他语言(最著名的是 PL/SQL)甚至支持按参数名称进行函数重载; 一个类可以有多个同名、参数数量相同、参数类型相同但参数名称不同的方法。Python 不支持这两种方法中的任何一种;它没有任何形式的函数重载。方法仅由其名称定义,并且每个类中只能有一个具有给定名称的方法。因此,如果子类有一个 __init__ 方法,那么它总是 会覆盖父类的 __init__ 方法,即使子类使用不同的参数列表定义它也是如此。同样的规则也适用于任何其他方法。
Note
Python 的原作者 Guido 对方法重写的解释如下:“派生类可以覆盖其基类的方法。因为方法在调用同一个对象的其它方法时没有特殊权限,所以一个基类的方法调用同一个基类中定义的另一个方法时,实际上可能会调用派生类中重写该方法的方法。(对于 C++ 程序员来说:Python 中的所有方法实际上都是虚方法。)” 如果您不明白这句话(它把我搞糊涂了),请随意忽略它。我只是想把它传递下去。
Caution
始终在 __init__ 方法中为实例的所有数据属性分配初始值。这将为您节省以后数小时的调试时间,避免因为引用未初始化(因此不存在)的属性而跟踪 AttributeError 异常。

示例 5.10. UserDict 普通方法

    def clear(self): self.data.clear()          1
    def copy(self):                             2
        if self.__class__ is UserDict:          3
            return UserDict(self.data)         
        import copy                             4
        return copy.copy(self)                 
    def keys(self): return self.data.keys()     5
    def items(self): return self.data.items()  
    def values(self): return self.data.values()
1 clear 是一个普通的类方法;它可以被任何人随时公开调用。请注意,clear 像所有类方法一样,将 self 作为其第一个参数。(请记住,在调用方法时不需要包含 self;这是 Python 为您添加的。)还要注意这个包装类的基本技术:将一个真实的字典(data)存储为数据属性,定义真实字典拥有的所有方法,并让每个类方法重定向到真实字典上的相应方法。(如果您忘记了,字典的 clear 方法 会删除其所有键 及其关联的值。)
2 真实字典的 copy 方法返回一个新的字典,该字典是原始字典的精确副本(所有键值对都相同)。但是 UserDict 不能简单地重定向到 self.data.copy,因为该方法返回一个真实的字典,而您想要返回的是一个与 self 类相同的新实例。
3 您可以使用 __class__ 属性来查看 self 是否是 UserDict;如果是,那就太好了,因为您知道如何复制 UserDict:只需创建一个新的 UserDict 并将您存储在 self.data 中的真实字典传递给它即可。然后,您立即返回新的 UserDict,您甚至不需要进入下一行的 import copy
4 如果 self.__class__ 不是 UserDict,那么 self 必须是 UserDict 的某个子类(例如 FileInfo),在这种情况下,事情就变得棘手了。UserDict 不知道如何精确复制其后代;例如,子类中可能定义了其他数据属性,因此您需要遍历它们并确保复制所有属性。幸运的是,Python 附带了一个模块来完成这项工作,它叫做 copy。我在这里不会详细介绍(不过如果你有兴趣,可以自己深入研究一下,它是一个非常酷的模块)。只需说 copy 可以复制任意的 Python 对象,这就是你在这里使用它的方式。
5 其余的方法都很简单,将调用重定向到 self.data 上的内置方法。
Note
在 2.2 之前的 Python 版本中,不能直接子类化字符串、列表和字典等内置数据类型。为了弥补这一点,Python 附带了模拟这些内置数据类型行为的包装类:UserStringUserListUserDict。通过组合使用普通方法和特殊方法,UserDict 类可以很好地模拟字典。在 Python 2.2 及更高版本中,可以直接从 dict 等内置数据类型继承类。本书附带的示例中给出了一个例子,位于 fileinfo_fromdict.py 中。

Python 中,可以直接从 dict 内置数据类型继承,如本例所示。与 UserDict 版本相比,这里有三个区别。

示例 5.11. 直接从内置数据类型 dict 继承


class FileInfo(dict):                  1
    "store file metadata"
    def __init__(self, filename=None): 2
        self["name"] = filename
1 第一个区别是不需要导入 UserDict 模块,因为 dict 是内置数据类型,始终可用。第二个区别是直接从 dict 继承,而不是从 UserDict.UserDict 继承。
2 第三个区别很微妙但很重要。由于 UserDict 的内部工作方式,它要求手动调用其 __init__ 方法来正确初始化其内部数据结构。dict 不是这样工作的;它不是包装器,不需要显式初始化。

关于 UserDict 的进一步阅读