5.5. 探索 UserDict：一个封装类
Description: Python from novice to pro
Keywords: Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free

# 深入 Python :Dive Into Python 中文版 Python 从新手到专家 [Dip_5.4b_CPyUG_Release] | Find:

## 5.5. 探索 `UserDict`：一个封装类

如你所见，`FileInfo` 是一个有着像字典一样的行为方式的类。为了进一步揭示这一点，让我们看一看在 `UserDict` 模块中的 `UserDict` 类，它是我们的 `FileInfo` 类的父类。它没有什么特别的，也是用 Python 写的，并且保存在一个 `.py` 文件里，就像我们其他的代码。特别之处在于，它保存在你的 Python 安装目录的 `lib` 目录下。

在 Windows 下的 ActivePython IDE 中，你可以快速打开在你的库路径中的任何模块，使用 File->Locate... (Ctrl-L)。

### 例 5.9. 定义 `UserDict` 类

    class UserDict:
        def __init__(self, dict=None):
            self.data = {}
            if dict is not None: self.update(dict)

| 注意 `UserDict` 是一个基类，不是从任何其他类继承而来。
| 这就是我们在 `FileInfo` 类中进行了覆盖的 `__init__` 方法。注意这个父类的参数列表与子类不同。很好，每个子类可以拥有自已的参数集，只要使用正确的参数调用父类就可以了。这里父类有一个定义初始值的方法 (通过在 `dict` 参数中传入一个字典)，这一方法我们的 `FileInfo` 没有用上。
| Python 支持数据属性 (在 Java 和 Powerbuilder 中叫做 “实例变量”，在 C++ 中叫 “数据成员”)，它是由某个特定的类实例所拥有的数据。在本例中，每个 `UserDict` 实例将拥有一个 `data` 数据属性。要从类外的代码引用这个属性，需要用实例的名字限定它，`instance.data`，限定的方法与你用模块的名字来限定函数一样。要在类的内部引用一个数据属性，我们使用 `self` 作为限定符。习惯上，所有的数据属性都在 `__init__` 方法中初始化为有意义的值。然而，这并不是必须的，因为数据属性，像局部变量一样，当你首次赋给它值的时候突然产生。
| `update` 方法是一个字典复制器：它把一个字典中的键和值全部拷贝到另一个字典。这个操作并不 事先清空目标字典，如果一些键在目标字典中已经存在，则它们将被覆盖，那些键名在目标字典中不存在的则不改变。应该把 `update` 看作是合并函数，而不是复制函数。
| 这个语法你可能以前没看过 (我还没有在这本书中的例子中用过它)。这是一条 `if` 语句，但是没有在下一行有一个缩近块，而只是在冒号后面，在同一行上有单条语句。这完全是合法的，它只是当你在一个块中仅有一条语句时的一个简写。(它就像在 C++ 中没有用大括号包括的单行语句。) 你可以用这种语法，或者可以在后面的行写下缩近代码，但是不能对同一个块同时用两种方式。

Java 和 Powerbuilder 支持通过参数列表的重载，也就是 一个类可以有同名的多个方法，但这些方法或者是参数个数不同，或者是参数的类型不同。其它语言 (最明显如 PL/SQL) 甚至支持通过参数名的重载，也就是 一个类可以有同名的多个方法，这些方法有相同类型，相同个数的参数，但参数名不同。Python 两种都不支持，总之是没有任何形式的函数重载。一个 `__init__` 方法就是一个 `__init__` 方法，不管它有什么样的参数。每个类只能有一个 `__init__` 方法，并且如果一个子类拥有一个 `__init__` 方法，它总是 覆盖父类的 `__init__` 方法，甚至子类可以用不同的参数列表来定义它。

Python 的原作者 Guido 是这样解释方法覆盖的：“子类可以覆盖父类中的方法。因为方法没有特殊的优先级设置，父类中的一个方法在调用同类中的另一方法时，可能其实调用到的却是一个子类中覆盖父类同名方法的方法。 (C++ 程序员可能会这样想：所有的 Python 方法都是虚函数。)”如果你不明白 (它令我颇感困惑)，不必在意。我想我要跳过它。[3]

应该总是在 `__init__` 方法中给一个实例的所有数据属性赋予一个初始值。这样做将会节省你在后面调试的时间，不必为捕捉因使用未初始化 (也就是不存在) 的属性而导致的 `AttributeError` 异常费时费力。

### 例 5.10. `UserDict` 常规方法

        def clear(self): self.data.clear()
        def copy(self):
            if self.__class__ is UserDict:
                return UserDict(self.data)
            import copy
            return copy.copy(self)
        def keys(self): return self.data.keys()
        def items(self): return self.data.items()
        def values(self): return self.data.values()

| `clear` 是一个普通的类方法，可以在任何时候被任何人公开调用。注意，`clear` 像所有的类方法一样 (常规的或专用的)，使用 `self` 作为它的第一个参数。(记住，当你调用方法时，不用包括 `self`；这件事是 Python 替你做的。) 还应注意这个封装类的基本技术：将一个真正的字典 (`data`) 作为数据属性保存起来，定义所有真正字典所拥有的方法，并且将每个类方法重定向到真正字典上的相应方法。(你可能已经忘了，字典的 `clear` 方法删除它的所有关键字和关键字相应的值。)
| 真正字典的 `copy` 方法会返回一个新的字典，它是原始字典的原样的复制 (所有的键-值对都相同)。但是 `UserDict` 不能简单地重定向到 `self.data.copy`，因为那个方法返回一个真正的字典，而我们想要的是返回同一个类的一个新的实例，就像是 `self`。
| 我们使用 `__class__` 属性来查看 `self` 是否是一个 `UserDict`，如果是，太好了，因为我们知道如何拷贝一个 `UserDict`：只要创建一个新的 `UserDict` ，并传给它真正的字典，这个字典已经存放在 `self.data` 中了。然后你立即返回这个新的 `UserDict`，你甚至于不需要在下面一行中使用 `import copy`。
| 如果 `self.__class__` 不是 `UserDict`，那么 `self` 一定是 `UserDict` 的某个子类 (如可能为 `FileInfo`)，生活总是存在意外。`UserDict` 不知道如何生成它的子类的一个原样的拷贝，例如，有可能在子类中定义了其它的数据属性，所以我们只能完全复制它们，确定拷贝了它们的全部内容。幸运的是，Python 带了一个模块可以正确地完成这件事，它叫做 `copy`。在这里我不想深入细节 (然而它是一个绝对酷的模块，你是否已经想到要自已研究它了呢？)。说 `copy` 能够拷贝任何 Python 对象就够了，这就是我们在这里用它的原因。
| 其余的方法是直截了当的重定向到 `self.data` 的内置函数上。

在 Python 2.2 之前的版本中，你不可以直接子类化字符串、列表以及字典之类的内建数据类型。作为补偿，Python 提供封装类来模拟内建数据类型的行为，比如：`UserString`、`UserList` 和 `UserDict`。通过混合使用普通和特殊方法，`UserDict` 类能十分出色地模仿字典。在 Python 2.2 和其后的版本中，你可以直接从 `dict` 内建数据类型继承。本书 `fileinfo_fromdict.py` 中有这方面的一个例子。

如例子中所示，在 Python 中，你可以直接继承自内建数据类型 `dict`，这样做有三点与 `UserDict` 不同。

### 例 5.11. 直接继承自内建数据类型 `dict`

    class FileInfo(dict):
        "store file metadata"
        def __init__(self, filename=None):
            self["name"] = filename

| 第一个区别是你不需要导入 `UserDict` 模块，因为 `dict` 是已经可以使用的内建数据类型。第二个区别是你不是继承自 `UserDict.UserDict` ，而是直接继承自 `dict`。
| 第三个区别有些晦涩，但却很重要。`UserDict` 内部的工作方式要求你手工地调用它的 `__init__` 方法去正确初始化它的内部数据结构。`dict` 并不这样工作，它不是一个封装所以不需要明确的初始化。

### 进一步阅读

- Python Library Reference 提供了 `UserDict` 模块 和 `copy` 模块 的文档。

### Footnotes

[3] 实际上，这一点并不是那么难以理解。考虑两个类，`base` 和 `child`，`base` 中的方法 `a` 需要调用 `self.b`；而我们又在 `child` 中覆盖了方法 `b`。然后我们创建一个 `child` 的实例，`ch`。调用 `ch.a`，那么此时的方法 `a` 调用的 `b` 函数将不是 `base.b`，而是 `child.b`。――译注