💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 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) ``` | | | | --- | --- | | \[1\] | 注意 `UserDict` 是一个基类,不是从任何其他类继承而来。 | | \[2\] | 这就是我们[在 `FileInfo` 类中进行了覆盖](defining_classes.html#fileinfo.class.example "例 5.4. 定义 FileInfo 类")的 `__init__` 方法。注意这个父类的参数列表与子类不同。很好,每个子类可以拥有自已的参数集,只要使用正确的参数调用父类就可以了。这里父类有一个定义初始值的方法 (通过在 `dict` 参数中传入一个字典),这一方法我们的 `FileInfo` 没有用上。 | | \[3\] | Python 支持数据属性 (在 Java 和 Powerbuilder 中叫做 “实例变量”,在 C++ 中叫 “数据成员”),它是由某个特定的类实例所拥有的数据。在本例中,每个 `UserDict` 实例将拥有一个 `data` 数据属性。要从类外的代码引用这个属性,需要用实例的名字限定它,`_instance_.data`,限定的方法与你用模块的名字来限定函数一样。要在类的内部引用一个数据属性,我们使用 `self` 作为限定符。习惯上,所有的数据属性都在 `__init__` 方法中初始化为有意义的值。然而,这并不是必须的,因为数据属性,像局部变量一样,当你首次赋给它值的时候[突然产生](../native_data_types/declaring_variables.html "3.4. 变量声明")。 | | \[4\] | `update` 方法是一个字典复制器:它把一个字典中的键和值全部拷贝到另一个字典。这个操作_并不_ 事先清空目标字典,如果一些键在目标字典中已经存在,则它们将被覆盖,那些键名在目标字典中不存在的则不改变。应该把 `update` 看作是合并函数,而不是复制函数。 | | \[5\] | 这个语法你可能以前没看过 (我还没有在这本书中的例子中用过它)。这是一条 `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() ``` | | | | --- | --- | | \[1\] | `clear` 是一个普通的类方法,可以在任何时候被任何人公开调用。注意,`clear` 像所有的类方法一样 (常规的或专用的),使用 `self` 作为它的第一个参数。(记住,当你调用方法时,不用包括 `self`;这件事是 Python 替你做的。) 还应注意这个封装类的基本技术:将一个真正的字典 (`data`) 作为数据属性保存起来,定义所有真正字典所拥有的方法,并且将每个类方法重定向到真正字典上的相应方法。(你可能已经忘了,字典的 `clear` 方法[删除它的所有关键字](../native_data_types/index.html#odbchelper.dict.del "例 3.5. 从 dictionary 中删除元素")和关键字相应的值。) | | \[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` 的内置函数上。 | > 注意 > 在 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 ``` | | | | --- | --- | | \[1\] | 第一个区别是你不需要导入 `UserDict` 模块,因为 `dict` 是已经可以使用的内建数据类型。第二个区别是你不是继承自 `UserDict.UserDict` ,而是直接继承自 `dict`。 | | \[2\] | 第三个区别有些晦涩,但却很重要。`UserDict` 内部的工作方式要求你手工地调用它的 `__init__` 方法去正确初始化它的内部数据结构。`dict` 并不这样工作,它不是一个封装所以不需要明确的初始化。 | ## 进一步阅读 * _Python Library Reference_ 提供了 [`UserDict` 模块](http://www.python.org/doc/current/lib/module-UserDict.html) 和 [`copy` 模块](http://www.python.org/doc/current/lib/module-copy.html) 的文档。 ## Footnotes \[3\] 实际上,这一点并不是那么难以理解。考虑两个类,`base` 和 `child`,`base` 中的方法 `a` 需要调用 `self.b`;而我们又在 `child` 中覆盖了方法 `b`。然后我们创建一个 `child` 的实例,`ch`。调用 `ch.a`,那么此时的方法 `a` 调用的 `b` 函数将不是 `base.b`,而是 `child.b`。――译注