企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] Python在许多方面似乎比面向对象编程更让人想起结构化或函数式编程。尽管面向对象编程是过去二十年中最明显的编程范式,函数式编程最近有所复苏。就像Python的数据结构一样,其中大多数工具是底层面向对象实现的语法糖;我们可以将它们看作是建立在(已经抽象的)面向对象范式之上的另一个抽象层。在本章中,我们将介绍一些Python不是严格面向对象的特性: * 在一次调用中处理多个常见任务的内置函数 * 文件输入/输出和上下文管理器 * 方法重载的替代方法 * 函数也是对象 ## Python内置函数 Python中有许多函数可以在一些特定类型的对象上执行任务或计算,而不需要使用对象类的方法。它们通常是适用于多种类型类的抽象通用计算。这是鸭子类型最好的应用;这些函数接受具有某些属性或者方法的对象,并且能够使用这些方法执行泛型操作。其中一些都是具有特殊的双下划线的方法。我们已经使用了许多内置函数,但是让我们快速浏览一下重要的函数,然后挑选一些学习。 ### `len()`函数 最简单的例子是`len()`函数,它计算某种容器对象内项目的数量,例如,字典或列表。你以前已经见过这个函数: ``` >>> len([1,2,3,4]) 4 ``` 为什么这些对象没有`length`属性,而必须调用一个函数?技术上来说,它们有。可以调用`len()`的大多数对象都有一个方法`__len __()`,可以返回相同的值。所以`len(myobj)`和`myobj.__len__()`是一样的。 </b> 那为什么我们使用`len()`函数而不是`__len__`方法呢?很明显`__len__`是一种特殊的双下划线方法,我们最好不要直接调用这个方法。这仅仅是一个解释。Python开发人员不会轻而易举作出这种设计决策。 </b> 主要原因是效率。当我们在一个对象上调用`__len__`时,该对象在其命名空间中查找该方法,如果在这个对象上定义了特殊的`__getattribute__`方法(每次访问对象的属性或方法时都会被调用),那么`__getattribute__`也必须被调用。此外,`__getattribute__`这种特殊的方法可能被设计成做一些讨厌的事情,比如拒绝让我们可以使用特殊的方法,例如`__len__`!`len()`函数不会有这样的问题。它实际上调用对象类上的`__len__`函数,所以`len(myobj)`被映射到`MyObj.__len__(myobj)`。 </b> 另一个原因是可维护性。将来,Python开发人员可能希望更改`len()`,以便它可以计算没有`__len__`方法的对象的长度,例如,通过计算迭代器中返回的项数。他们只要改变一个函数即可,而不是修改程序中所有的无数的`__len__`方法。 </b> `len()`作为外部函数存在,还有另一个极其重要且经常被忽视的原因:向后兼容性。这在很多文章中经常被引用为“历史原因”,这是一个温和的轻蔑短语,作者会用它来说明,事情是这样的,因为很久以前犯了一个错误,但我们一直坚持不修改它。严格地说,`len()`不是一个错误,这是一个设计决定,这个决定,是在一个不太面向对象的时代决定的。它经受住了时间的考验,有一些好处,所以一定要习惯。 ### `Reversed`反转函数 `reversed()`函数将任何序列作为输入,并返回顺序相反的副本。当我们想要循环时,它通常用于形成循环从后到前的项目。 </b> 类似于`len`,`reversed`调用类上的`__reversed__()`函数。如果该方法不存在,`reversed`通过调用`__len__`和`__getitem__`构建反向序列,这两个方法用于定义序列。如果我们想以某种方式定制或优化反向过程,只需要重写`__reversed__`即可: ``` normal_list=[1,2,3,4,5] class CustomSequence(): def __len__(self): return 5 def __getitem__(self, index): return "x{0}".format(index) class FunkyBackwards(): def __reversed__(self): return "BACKWARDS!" for seq in normal_list, CustomSequence(), FunkyBackwards(): print("\n{}: ".format(seq.__class__.__name__), end="") for item in reversed(seq): print(item, end=", ") ``` 结尾的`for`循环打印了一个正常列表和两个自定义序列实例的反转版本(译注:`FunkyBackwards()`代码在`python3.5`运行时并不成功)。输出显示`reversed`对所有三个都起作用,但是当我们自定义`__reversed__`时,结果却大不相同: ``` list: 5, 4, 3, 2, 1, CustomSequence: x4, x3, x2, x1, x0, FunkyBackwards: B, A, C, K, W, A, R, D, S, !, ``` 当我们反转`CustomSequence`时,会为每个项目调用`__getitem__`方法,它只是在索引前插入一个`x`。对于`FunkyBackwards`,`__reversed__`方法返回一个字符串,其每个字符都在`for`循环中被单独输出。 > 这两个类不是很好的序列,因为它们没有定义一个正确版本的`__iter__`,这样作用在它们的向前`for`循环永远不会结束。 ### `Enumerate`枚举函数 有时,当我们在`for`循环中遍历容器时,我们希望返回正在处理的当前项目的索引(列表中的当前位置)。`for`循环没有给我们提供索引,但是`Enumerate`函数可以提供:它创建元组序列,其中每个元组中的第一个对象是索引,第二个是原始项目。 </b> 如果我们需要直接使用索引号,这很有用。考虑一些输出文件中每一行的行号简单的代码: ``` import sys filename = sys.argv[0] #原书写的是sys.argv[1],正确的应该是sys.argv[0] with open(filename) as file: for index, line in enumerate(file): print("{0}: {1}".format(index+1, line), end='') ``` 使用自己的`filename`作为输入文件运行此代码,显示了它是如何工作的: ``` 1: import sys 2: filename = sys.argv[1] 3: 4: with open(filename) as file: 5: for index, line in enumerate(file): 6: print("{0}: {1}".format(index+1, line), end='') ``` `Enumerate`函数返回元组序列,我们的`for`循环拆分每个元组转换成两个值,`print`语句将它们格式化。它给每个行号的索引加一,因为像所有序列一样,`enumerate`是从零开始的。 </b> 我们只涉及了几个更重要的Python内置函数。如同你所看到的,这些内置函数中,有些调用的是面向对象的概念,而有些则是纯粹的函数或程序范式。标准库中还有很多其他的例子;一些更有趣的包括: * `all`和`any`,它们接受可迭代对象,并判断对象中所有项或任意项的值(如非空字符串或列表,非零值数字、不是`None`的对象或字面`True`),如果为真,则返回`True` * `eval`、`exec`和`compile`,它们将字符串作为代码在解释器中执行。注意,它们并不安全,所以不要执行未知用户向你提供的代码(通常,假设所有未知用户是恶意的、愚蠢的或两者兼有) * `hasattr`、`getattr`、`setattr`和`delattr`,它们允许通过字符串名称操作对象属性 * `zip`,它接受两个或多个序列并返回一个新的元组序列,其中每个元组包含来自每个序列的单个值 * 还有更多!请参阅每个的解释器帮助文档,使用` dir(__builtins__)`中列出的函数。 ### `File I/O` 到目前为止,我们接触文件系统的例子完全是在文本文件上操作的,并不需要我们去想这背后发生了什么。然而,操作系统实际上将文件表示为字节序列,而不是文本。我们会做在第8章“字符串和序列化中”深入探讨字节和文本之间的关系。现在,我们只要知道从文件中读取文本数据是一个相当复杂的过程。Python,尤其是Python 3,将替我们完成这些复杂的过程。我们不幸运吗? </b> 文件的概念早在任何人创造这个面向对象编程术语之前就已经存在了。然而,Python已经包装了一个接口,操作系统在这个接口中提供了一种甜蜜的抽象,允许我们对文件(或者类似文件,相对于鸭子类型)对象进行操作。 </b> `open()`内置函数用于打开一个文件并返回一个文件对象。为从文件中读取文本,我们只需要将文件的名称传递给函数。文件将被打开并被读取,字节将被平台默认编码器转换为文本。 </b> 当然,我们并不总是想读文件;我们经常想给他们写数据!要打开文件进行写入,我们需要传递一个`mode`参数作为第二个位置参数,值为“w”: ``` contents = "Some file contents" file = open("filename", "w") file.write(contents) file.close() ``` 我们也可以提供值“a”作为模式参数,这会写入的内容附加到文件的末尾,而不是完全覆盖现有文件内容。 </b> 这些带有内置包装器将字节转换成文本是很棒的,但是如果我们想要打开的文件是图像、可执行文件或其他二进制文件时,该怎么办呢? </b> 为了打开一个二进制文件,我们修改模式字符串添加一个“b”。所以,“wb”会打开一个文件来写字节,而“rb”允许我们读取它们。这么做很像文本文件操作,但是没有自动将文本编码成字节。当我们阅读时这样一个文件,它将返回字节`bytes`对象而不是字符串`str`,当我们写入它时,如果我们试图传递文本对象,将会失败。 > 这些用于控制文件打开方式的模式字符串相当神秘,既不pythonic也不是面向对象所特有的。然而,处理方式几乎与所有其他编程语言都一致。文件输入输出是操作系统的基本工作之一,所有编程语言都必须与操作系统对话,使用相同的系统调用。很高兴Python返回了一个带有很多方法的文件对象,而不是主流操作系统用于识别文件句柄的整数! 一旦打开一个文件进行读取,我们可以使用`read`、`readline`或`readlines`方法获取文件的内容。`read`方法返回全部内容的`str`或`bytes`对象,这取决于模式中是否有“b”。小心不要在大型文件上没有参数的情况下使用此方法。如果你试图将那么多数据加载到内存中,后果自负! </b> 也可以从文件中读取固定数量的字节;我们传递一个描述我们要读取多少字节的整数,作为`read`方法的参数。下一次读取调用将加载下一个字节序列,依此类推。我们可以在`while`循环中以可管理的块读取整个文件。 </b> `readline`方法从文件中返回一行(每行的结尾以换行符、回车符或两者都有,具体取决于创建文件的操作系统)。我们可以反复调用它来获得额外的行。多个`readlines`方法一起使用,可以返回一个文件中所有行的列表。像读取方法一样,在非常大的文件上使用`readlines`是不安全的。这两种方法都以字节模式打开文件,但是只有当我们解析类文本的数据,在合理的位置有新的一行时,才是有意义的。例如,图像或音频文件没有换行符(除非换行符恰好代表某些像素或声音),所以`readlines`是没有意义的。 </b> 为了可读性,并避免一次将一个大文件读入内存,通常最好直接在文件对象上使用`for`循环。对于文本文件,它将每次读取一行,我们可以在循环体内处理它。对于二进制文件,使用`read()`方法读取固定大小的数据块,通过传递参数获取要读取的最大字节数。 </b> 写一个文件也一样容易;文件对象上的`write`方法写入字符串(或字节,如果是二进制数据)。可以重复调用它来写入多个字符串。`writelines`方法接受字符串序列,并将每个序列值按顺序写入文件。但`writelines`方法不在序列中的每个项目后追加一行。它基本上算是一个命名的函数,用来将字符串序列的内容写入文件中,而无需使用`for`循环显式迭代它。 </b> 最后,我们看一下`close`方法。这种方法应该当我们完成读写文件时调用,以确保任何写入的缓冲被写入磁盘,文件已被正确清理,以及与该文件相关联的所有资源都被释放回操作系统。从技术上讲,这将在脚本退出时自动发生,但是最好显式地自我清理,尤其是在长期运行的过程中。 ### 把它放在上下文中 当我们完成文件时,需要关闭它们,这可能我们的代码变得相当丑陋。因为在文件输入/输出期间任何时候都可能发生异常,所以我们应该尝试将所有对一个文件的调用包装到`try...finally`。文件应该在`finally`中被关闭,不管输入/输出是否成功。这不是很Python。当然,还有更优雅的方法。 </b> 如果我们在一个类似文件的对象上运行`dir`,我们会看到它有两个名为`__enter__`和`__exit__`的方法。这些方法将文件对象转换成所谓的**上下文管理器**。基本上,如果我们使用一种叫做`with`语句的特殊语法,这些方法将在嵌套代码执行前后调用。在文件对象上,`__exit__`方法确保文件被关闭,即使出现异常。我们不再需要显式管理文件的关闭。`with`语句实际上看起来像这样: ``` with open('filename') as file: for line in file: print(line, end='') ``` `open`调用返回一个文件对象,该对象具有`__enter__`和`__exit__`方法。返回的对象由`as`子句分配给名为`file`的变量。我们知道当代码返回到外部缩进级别时文件将被关闭,即使出现异常,文件也会被关闭。 </b> `with`语句在标准库中的几个地方,用于启动或者清理代码。例如,`urlopen`调用返回一个对象,该对象可以在`with`语句中使用,以便通信结束时清理socket。线程模块中的锁可以在`with`语句被执行后释放锁。 </b> 最有趣的是,因为`with`语句可以应用于任何具有适当的特殊方法的对象上,我们可以在自己的框架中使用它。例如,请记住,字符串是不可变的,但是有时你需要从多个片段建立一个字符串。为了提高效率,通常通过存储片段字符串,然后把它们合并来实现。让我们创建一个简单的上下文管理器,它允许我们构建一个字符序列,并在退出时自动将其转换为字符串: ``` class StringJoiner(list): def __enter__(self): return self def __exit__(self, type, value, tb): self.result = "".join(self) ``` 这段代码将所需的两种特殊方法添加到继承自列表的上下文管理器类中。`__enter__`方法执行任何必需的启动代码(此处没有启动代码),然后在with语句中将返回一个分配给`as`之后变量的对象。通常,正如我们在这里所做的,对象只是上下文管理器本身。`__exit__`方法接受三个参数。正常情况下情况下,这些都被赋予`None`。但是,如果`with块`内部发生异常,它们将被设置为与的类型、值和回溯相关的异常。这允许`__exit__`方法即使出现异常时也能执行任何必需的代码清理工作。在我们的例子中,我们采取了一个粗略的方法,通过连接字符串中的字符来创建结果字符串,而不管是否发生了异常。 </b> 虽然这是我们可以编写的最简单的上下文管理器之一,它的用处可能不大,但它确实可以与`with`语句一起工作。看看它的运行情况: ``` import random, string with StringJoiner() as joiner: for i in range(15): joiner.append(random.choice(string.ascii_letters)) print(joiner.result) ``` 这段代码构造了一个由15个随机字符组成的字符串。它将使用从列表继承的`append`方法这些字符附加到`StringJoiner`对象。当`with`语句超出范围(回到外部缩进级别),则调用`__exit__`方法,`result`属性在`StringJoiner`对象上将变得可用。我们打印该值以查看随机字符串。 (译注:更简单的例子如下:) ``` a = StringJoiner(['a','b']) with a as f: pass print(f.result) ``` ## 方法重载的替代方法 许多面向对象编程语言的一个突出特征是**方法重载**。方法重载指拥有多个接受不同参数集的同名方法。静态类型化语言,如果我们想要一个方法接受整数或字符串,方法重载会很有用处。在非面向对象语言中,我们可能需要定义两个称为`add_s`和`add_i`的函数,以适应这种情况。静态类型面向对象语言,我们需要两种方法,都叫做`add`,一种接受字符串,另一种接受整数。 </b> 在Python中,我们只需要一种方法,它接受任何类型的对象。可能有要对对象类型进行一些测试(例如,如果它是字符串,则将其转换为整数),但只需要一种方法。 </b> 但是,当我们想要一个具有相同名称的方法可以接受不同数量或不同组的参数,那还是得使用方法重载。例如,电子邮件的`message`方法可能有两个版本,一个版本接受“发件人”电子邮件地址。另一个版本查找默认的“发件人”地址。Python不允许存在多个同名的方法,但它提供了一个不同的、同样灵活的接口。 </b> 我们已经在前面的例子里,看到了将参数发送到方法和函数的一些可能的方法,现在我们将涵盖所有细节。最简单的函数没有参数。我们可能不需要下面这个例子,但它仍然一个完整的例子: ``` def no_args(): pass ``` 然后调用这个函数: ``` no_args() ``` 接受参数的函数,会提供一个逗号分隔的、具有参数名字的列表。只需要提供参数名称即可。当调用函数时,这些参数的值必须按照参数列表中的位置被提供,也可以忽略其中的参数。下面是之前例子中,我们最常用的定义有参函数的方法: ``` def mandatory_args(x, y, z): pass ``` 然后调用这个函数: ``` mandatory_args("a string", a_variable, 5) ``` 任何类型的对象都可以作为参数传递:对象、容器、主数据类型、甚至函数和类。上面这个例子被传递的参数分别是字符串、未知变量,以及一个整数。 ### 默认参数 如果我们想让参数是可选的,不需要再创建一个有不同参数组的方法,我们可以在一个方法中,用等号给参数一个默认值。如果被调用的方法没有这个参数值,参数将被分配一个默认值。当然,我们也可以选择一个不同的值来覆盖这个默认值。通常,使用`None`、空字符串、或列表作为默认值是合适的。 </b> 下面是一个带有默认值参数的函数定义: ``` def default_arguments(x, y, z, a="Some String", b=False): pass ``` 前三个参数仍然是强制性的,必须被传入被调用的函数。剩下两个参数提供了默认参数值。 </b> 有许多调用这个函数的方法。我们可以按顺序提供所有的参数值,就好像它们是位置参数(都得被传入): ``` default_arguments("a string", variable, 8, "", True) ``` 或者,我们只按顺序提供强制性参数,而把默认值分配给剩下的相应的参数: ``` default_arguments("a longer string", some_variable, 14) ``` 我们也可以用等号语法按不同的传参顺序调用函数,或者是忽略我们不感兴趣的参数。例如,我们可以忽略第一个键参数,而仅提供第二个键参数: ``` default_arguments("a string", variable, 14, b=True) ``` 我们甚至可以使用等号语法打乱必填参数的顺序,只要提供它们的值即可。 ``` >>> default_arguments(y=1,z=2,x=3,a="hi") 3 1 2 hi False ``` 这么多选择,似乎很难选,但是如果你把位置参数看作是顺序列表,把键参数看作字典,你会发现其实很容易选择的。如果你需要一个有特定参数的函数,把这个参数定义为强制性的;如果你有一个有意义的默认值,把它定义为键参数。选择什么样的传参方法,主要依赖于哪些参数值是需要被提供的,以及哪些参数值可以用默认值。 </b> 键参数有一个要注意的地方,一旦给函数提供了一个默认值给键参数,这个默认值在函数被解释后就固定了,我们不能在调用函数时修改这个默认值。或者说我们不能动态地生成默认值。例如,下面这段代码并不能像我们希望的那样执行: ``` number = 5 def funky_function(number=number): print(number) number=6 funky_function(8) funky_function() print(number) ``` 如果我们运行这段代码,它会先输出8,然后调用没有参数的函数,输出5。我们已经给变量在`number`重新赋值为6,这点最后代码可以证明,但当这个函数被调用的时候,仍然打印5;这说明参数默认值时在函数被定义的时候就被计算了,而不是在调用函数的时候再确认默认值。 </b> 这对空容器,例如列表、集合和字典,将会有点小问题。例如,我们调用一个函数,函数将提供一个我们将要操作的列表,但是这个列表是可选的。我们可能选择一个空列表作为参数默认值。我们不要这么做;这段代码将在第一次执行的时候,生成一个唯一的列表: ``` >>> def hello(b=[]): ... b.append('a') ... print(b) ... >>> hello() ['a'] >>> hello() ['a', 'a'] ``` 看到没,这并不是我们所希望的。解决这个问题的办法是,将参数默认值设置为`None`,然后在方法内使用`iargument = argument if argument else [ ]`(译注:不知道这是什么解决办法,我改成下面这样)。 ``` >>> def hello(b=None): ... f = list(b) if b else ["a"] ... print(f) ... >>> hello() ['a'] >>> hello("b") ['b'] ``` ### 可变参数列表 缺省值不允许我们拥有方法重载的所有好处。但Python有一个平滑的技巧,就是在定义方法时,可以定义任意数量的位置参数或键参数,但不需要显示命名这些参数。我们也可以将任意列表和字典传递到这些函数中。 </b> 例如,接受链接或链接列表并下载网页的函数,使用这样的可变参数或`varargs`。而不是接受单一的参数值。这是一个链接列表,我们可以接受任意数量的参数,其中每个参数都是不同的链接。为此,我们在函数定义中指定`*`运算符: ``` def get_pages(*links): for link in links: #download the link with urllib print(link) ``` `*links`参数表示“我会接受任意数量的参数并把它们放进一个名为`links`的列表中”。如果我们只提供一个参数,它将是一个包含一个参数的列表元素;如果我们不提供参数,它将是一个空列表。因此,下面这些函数调用都是有效的: ``` get_pages() get_pages('http://www.archlinux.org') get_pages('http://www.archlinux.org', 'http://ccphillips.net/') ``` 我们也可以接受任意数量的键参数。这些以字典方式传入函数。它们在函数声明中用两个星号指定(如`**kwargs`)。工具通常用于配置设置。下列类允许我们用默认值指定一组选项: ``` class Options: default_options = { 'port': 21, 'host': 'localhost', 'username': None, 'password': None, 'debug': False, } def __init__(self, **kwargs): self.options = dict(Options.default_options) self.options.update(kwargs) def __getitem__(self, key): return self.options[key] ``` 这个类中所有有趣的东西都发生在`__init__`方法中。我们有一个类层级的默认选项和相应值的字典。`__init__`方法中的第一件事是复制这个字典。我们这样做而不去修改字典的原因是,以防我们实例化两个独立的选项集(记住,类级变量在类的实例之间是共享的)。然后,`__init__`使用新字典上的更新方法,将任何非默认值的键参数更改为传入的参数值。`__getitem__`方法允许我们在新类上使用更简单得索引语法。下面是一个例子: ``` >>> options = Options(username="dusty", password="drowssap", debug=True) >>> options['debug'] True >>> options['port'] 21 >>> options['username'] 'dusty' ``` 我们能够使用字典索引语法访问我们的选项实例,字典包括默认值和我们使用键参数设置的值。 </b> 关键字参数语法可能很危险,因为它可能会破坏“显式永远比隐式好”的规则。在前面的例子中,可以传递任意键参数到`options`的初始化方法中,甚至是默认词典不存在的选项。这可能不是一件坏事,这取决于类的目的,但是使用该类的人很难发现哪些有效选项是可用的。这很容易输入令人困惑的打字错误(例如,“Dedug”而不是“debug”),它增加了两个选项,然而应该只有一个应该存在。 </b> 有时候我们需要将任意参数传递给第二个函数,这时候键参数是非常有用的,但是我们不知道这些参数将会是什么。在第三章“当对象是相似的”,我们建立了对多重继承的支持。当然,我们可以组合可变参数和可变键参数都放到一个函数中,我们也可以使用普通的位置参数和默认参数。下面的例子有点做作,但展示了四种类型的参数: ``` import shutil import os.path def augmented_move(target_folder, *filenames, verbose=False, **specific): '''将所有的文件移动到指定的文件夹中, 允许对特定的文件做特殊处理''' def print_verbose(message, filename): '''如果打开verbose,则打印消息''' if verbose: print(message.format(filename)) for filename in filenames: target_path = os.path.join(target_folder, filename) if filename in specific: if specific[filename] == 'ignore': print_verbose("Ignoring {0}", filename) elif specific[filename] == 'copy': print_verbose("Copying {0}", filename) shutil.copyfile(filename, target_path) else: print_verbose("Moving {0}", filename) shutil.move(filename, target_path) ``` 本示例将处理一个含有任意数量的文件列表。第一个参数是目标文件夹,默认行为是将所有剩余的非键参数文件移动到那个文件夹。然后有一个包含默认值的键参数`verbose`,它告诉我们是否打印每个已处理文件的信息。最后,我们可以提供一本字典,包含对特定文件名执行的操作;默认行为是移动文件,但是如果在键参数中指定了有效的字符串操作,它将被忽略或复制。请注意函数中参数的顺序;首先指定位置参数,然后是`*filenames`列表,然后是任何指定的键参数,最后是一个`**specific`字典,用来保存剩余的键参数。 </b> 我们创建了一个内部帮助函数`print_verbose`,只有`verbose`被设置了,它才会打印消息。这个函数通过封装这个功能来保持代码可读。 </b> 在一般情况下,假设文件存在,这个函数可以这样被调用: ``` >>> augmented_move("move_here", "one", "two") ``` 该命令将文件`one`和`two`移动到`move_here`目录中, 假设文件存在(函数中没有错误检查或异常处理,因此,如果文件或目标目录不存在,它会非常失败)。移动后将在没有任何输出,因为`verbose`默认为`False`。 </b> 如果我们想看到输出,我们可以用: ``` >>> augmented_move("move_here", "three", verbose=True) Moving three ``` 这会移动一个名为`three`的文件,并告诉我们它在做什么。请注意,在此示例中,无法将`verbose`指定为位置参数;我们必须传递键参数。否则,Python会认为这是在`*filenames`列表中的另一个文件名。 </b> 如果我们想复制或忽略列表中的一些文件,而不是移动它们, 我们可以传递额外的键参数: ``` >>> augmented_move("move_here", "four", "five", "six", four="copy", five="ignore") ``` 这将移动第六个文件并复制第四个文件,但不会显示任何输出,因为我们没有指定`verbose`。当然,我们也可以这样做,键参数可以以任何顺序传递: ``` >>> augmented_move("move_here", "seven", "eight", "nine", seven="copy", verbose=True, eight="ignore") Copying seven Ignoring eight Moving nine ``` ### 解包参数 还有一个巧妙的技巧,涉及可变参数和键参数。我们已经在前面的一些例子中使用了它,但是还缺一个解释。给定一个值列表或字典,我们可以将这些值传递到函数,就像它们是正常的位置参数或键参数一样。看一看下面的代码: ``` def show_args(arg1, arg2, arg3="THREE"): print(arg1, arg2, arg3) some_args = range(3) more_args = { "arg1": "ONE", "arg2": "TWO"} print("Unpacking a sequence:", end=" ") show_args(*some_args) print("Unpacking a dict:", end=" ") show_args(**more_args) ``` 运行这段代码,将显示: ``` Unpacking a sequence: 0 1 2 Unpacking a dict: ONE TWO THREE ``` 该函数接受三个参数,其中一个具有默认值。但当我们提供一个包含三个参数的列表,我们可以在函数调用中使用`*`运算符把它分解成三个参数。如果我们有一个参数词典,我们可以使用`**`语法将其分解为键参数的集合。 </b> 这很有用,当把用户输入或外部来源(例如,互联网页面或文本文件)收集到的信息传递到函数或被调用的方法时。 </b> 还记得我们前面的例子吗,它使用文本文件中的标题和行来创建包含联系信息的字典列表?除了在列表中增加字典,我们还可以使用键拆包将参数传递给`__init__ `方法,该方法接受相同的参数集。看看你能不能实现这件事。(译注:待补充) ## 函数也是对象 过分强调面向对象原则的编程语并不喜欢不是方法的函数。在这种语言中,你需要创建一个对象来包装所涉及的方法。有许多情况,我们想要传递一个被调用的对象去执行一个动作。这在事件驱动编程中最常见,例如图形编程工具包或异步服务器;我们会在第10章“Python设计模式I”和第11章“Python设计模式II”看到一些这样做的设计模式。 </b> 在Python中,我们不需要将这样的方法包装在对象中,因为函数已经是对象!我们可以设置函数的属性(尽管这不是一个常见的活动),我们可以把它们传过去,以后再调用他们。他们甚至有一些特别的可以直接访问的属性。下面是另一个人为的例子: ``` def my_function(): print("The Function Was Called") my_function.description = "A silly function" def second_function(): print("The second was called") second_function.description = "A sillier function." def another_function(function): print("The description:", end=" ") print(function.description) print("The name:", end=" ") print(function.__name__) print("The class:", end=" ") print(function.__class__) print("Now I'll call the function passed in") function() another_function(my_function) another_function(second_function) ``` 如果我们运行这段代码,我们可以看到我们能够传递两个不同的函数进入我们的第三个函数,并为每个函数获得不同的输出: ``` The description: A silly function The name: my_function The class: <class 'function'> Now I'll call the function passed in The Function Was Called The description: A sillier function. The name: second_function The class: <class 'function'> Now I'll call the function passed in The second was called ``` 我们在函数上设置了一个属性,命名为`description`(不是很好的描述,诚然)。我们还能够看到函数的`__name__`属性,并访问它的类,表明该函数实际上是一个具有属性的对象。那我们通过使用可调用语法(括号)去调用函数。 </b> 函数作为顶级对象这一事实最常用于将它们在稍后的日期执行,例如,当满足某个条件时。让我们构建一个事件驱动定时器,它是这样做的: ``` import datetime import time class TimedEvent: def __init__(self, endtime, callback): self.endtime = endtime self.callback = callback def ready(self): return self.endtime <= datetime.datetime.now() class Timer: def __init__(self): self.events = [] def call_after(self, delay, callback): end_time = datetime.datetime.now() + \ datetime.timedelta(seconds=delay) self.events.append(TimedEvent(end_time, callback)) def run(self): while True: ready_events = (e for e in self.events if e.ready()) for event in ready_events: event.callback(self) self.events.remove(event) time.sleep(0.5) ``` 在生产环境中,这段代码肯定需要使用docstrings做额外的文档说明!`call_after`方法至少应该提到`delay`参数以秒为单位,`callback`应该接受一个参数:用来调用的计时器。 </b> 我们这里有两个类。`TimedEvent`类实际上并不打算被其他类访问;它所做的只是存储`endtime`和`callback`。我们甚至可以在这里使用元组或命名元组,但给对象一个行为,用来通知我们事件是否已经准备好运行,可能更加方便,所以我们使用一个类来代替。 </b> 计时器`Timer`类只存储即将发生的事件的列表。它有一个`call_after`方法用于添加新事件。此方法接受一个延迟`delay`参数,作为执行回调之前等待的秒数,`callback`方法本身是一个要在正确时间执行的函数。`callback`方法应该接受一个参数。 </b> `run`方法非常简单;它使用生成器表达式滤掉任何已经发生的事件,并按顺序执行。定时器循环会一直运行,所以必须用键盘中断(*Ctrl + C*或*Ctrl +break*)。每次迭代后,我们都要暂停半秒钟,给系统休息一下。 </b> 需要注意的是使用回调函数的行。这个函数像任何其他对象一样传递,计时器永远不会知道或关心函数的原始名称是什么或者在哪里定义的。当调用这个函数时,计时器只是将括号语法应用于这个存储的变量。 </b> 这里有一组测试定时器的回调函数: ``` from timer import Timer import datetime def format_time(message, *args): now = datetime.datetime.now().strftime("%I:%M:%S") print(message.format(*args, now=now)) def one(timer): format_time("{now}: Called One") def two(timer): format_time("{now}: Called Two") def three(timer): format_time("{now}: Called Three") class Repeater: def __init__(self): self.count = 0 def repeater(self, timer): format_time("{now}: repeat {0}", self.count) self.count += 1 timer.call_after(5, self.repeater) timer = Timer() timer.call_after(1, one) timer.call_after(2, one) timer.call_after(2, two) timer.call_after(4, two) timer.call_after(3, three) timer.call_after(6, three) repeater = Repeater() timer.call_after(5, repeater.repeater) format_time("{now}: Starting") timer.run() ``` 这个例子让我们看到多个回调函数如何与定时器交互。第一个回调函数是`format_time`函数。它使用字符串`format`方法添加消息的当前时间,并说明了活动中的可变参数。`format_time`方法将使用可变参数语法接受任意数量的位置参数,然后作为位置参数转发给字符串的`format`方法。之后,我们创建了三个简单的回调方法,输出当前时间和一条短消息,告诉我们哪个回调方法已经调用过了。 </b> `Repeater`类演示了方法也可以用作回调,因为它们实际上是函数。它还显示了回调函数的`timer`参数很有用:我们可以从当前正在运行回调,给计数器添加一个时间事件。然后我们创建一个计时器,并向其中添加几个事件,这些事件经过不同的时间之后被调用。最后,我们启动计时器运行;输出显示事件按预期顺序运行: ``` 02:53:35: Starting 02:53:36: Called One 02:53:37: Called One 02:53:37: Called Two 02:53:38: Called Three 02:53:39: Called Two 02:53:40: repeat 0 02:53:41: Called Three 02:53:45: repeat 1 02:53:50: repeat 2 02:53:55: repeat 3 02:54:00: repeat 4 ``` Python 3.4引入了与此类似的通用事件循环架构。我们会稍后在第13章“并发”中讨论它。 ### 使用函数作为属性 函数作为对象的一个有趣效果是它们可以被设置为其他对象上的可调用属性。可以为实例化的对象添加或更改函数: ``` class A: def print(self): print("my class is A") def fake_print(): print("my class is not A") a = A() a.print() a.print = fake_print a.print() ``` 这段代码创建了一个非常简单的带有`print`方法的类,`print`并没有告诉我们任何我们不知道的事情。然后我们创建一个新的函数来告诉我们一些我们不相信的事情。 </b> 当我们调用`A`类实例上的`print`方法时,它的行为与预期的一样。但如果我们将`print`方法设置为一个新方法,它告诉我们一些不同的东西: ``` my class is A my class is not A ``` 也可以替换类方法而不是对象方法,尽管这种情况下,我们必须将`self`参数添加到参数列表中。这将改变所有实例对象的方法,即使是对已经实例化的实例。显然,对于代码维护,这样做是危险和令人困惑的。阅读代码的人会看到一个方法被调用,并在原始类中查找该方法。但是原始类上的方法并不是被调用的那个。弄清楚到底发生了什么会变得棘手和令人沮丧。 </b> 但是它确实有它的用途。通常,在运行时替换或添加方法(称为猴子修补)用于自动化测试。如果测试客户端-服务器应用程序,在测试客户端时,我们可能不想实际连接到服务器;这可能导致资金意外转移或尴尬的测试电子邮件被发送到真实的人们那里。相反,我们可以用测试代码来替换对象上的一些关键方法,向服务器发送请求,因此它只记录方法被调用了。 </b> 猴子补丁也可以用来修复bug或者在第三方代码中添加一些互动的特征,但行为却不尽如人意。我们应该谨慎使用它;它几乎总是一个“混乱的黑客”。但有时候,它是调整现有库以满足我们需求的唯一方法。 ### 可调用的对象 正如函数可以作为对象一样,也可以创建一个可以像函数一样调用的对象。 </b> 任何对象都可以通过简单地给它一个可接受必需参数的`__call__`方法来调用。让我们将计时器示例的`Repeater`类修改成可调用的对象: ``` class Repeater: def __init__(self): self.count = 0 def __call__(self, timer): format_time("{now}: repeat {0}", self.count) self.count += 1 timer.call_after(5, self) timer = Timer() timer.call_after(5, Repeater()) format_time("{now}: Starting") timer.run() ``` 这个例子与前面的类没有太大不同;我们所做的就是将`repeater`方法的名字改为`__call__`,然后将对象本身作为一个可调用的函数。请注意,当我们调用`call_after`时,我们传递的参数是`Repeater()`。这两个括号正在创建类的新实例;它们并没有显式地调用类。这将在计时器内稍后发生。如果我们想在新实例化的对象上执行`__call__`方法,我们会使用相当奇怪的语法:`Repeater()()`。第一组括号构成了对象;第二组执行`__call__`方法。如果我们发现自己在这样做,我们可能没有使用正确的抽象。只有当对象应该像函数一样对待时再使用`__call__`方法。 ## 个案研究 为了结合本章中介绍的一些原则,让我们构建一个邮件列表经理。经理将跟踪分到各个命名组中的电子邮件地址。当该发信息的时候,我们可以选择一个小组,将邮件发送给该组的所有电子邮件地址。 </b> 现在,在我们开始运行这个项目之前,我们应该有一个安全的测试方法,它不需要给一群真实的人发电子邮件。幸运的是,Python支持我们这样做;像测试HTTP服务器一样,它有一个内置的简单邮件传输协议(SMTP)服务器,我们可以指示它捕获我们想发送的任何消息,而不是实际发送它们。我们可以使用以下命令运行服务器: ``` python -m smtpd -n -c DebuggingServer localhost:1025 ``` 在命令提示符下运行此命令将启动运行在本地机器上的端口1025。但是我们已经指示它使用`DebuggingServer`类(它来自内置的SMTP模块),它不发送邮件给可能的接收方,而只是在接收时在终端屏幕上打印出来它们。不错吧。 </b> 现在,在编写邮件列表之前,让我们编写一些实际发送邮件的代码。当然,Python在标准库中也支持这一点,它提供一个有点奇怪的接口,所以我们将编写一个新函数来包装它: ``` import smtplib from email.mime.text import MIMEText def send_email(subject, message, from_addr, *to_addrs, host="localhost", port=1025, **headers): email = MIMEText(message) email['Subject'] = subject email['From'] = from_addr for header, value in headers.items(): email[header] = value sender = smtplib.SMTP(host, port) for addr in to_addrs: del email['To'] email['To'] = addr sender.sendmail(from_addr, addr, email.as_string()) sender.quit() ``` 我们不会太过详细地讲解这个方法中的代码;标准库文档可以为你提供更有效使用`smtplib`和`email`模块所需的所有信息。 </b> 我们在函数调用中使用了可变参数和键参数语法。可变参数列表允许我们在默认情况下提供单个收件人地址,同时也允许必要时提供多个收件人地址。任何额外的键参数都映射到电子邮件头。这是一个可变参数和键参数的令人兴奋的用法,但对于调用函数的人来说,这真不是一个很好的界面。事实上,它做了了许多程序员想做的不可能的事情。 </b> 传递到函数中的头代表可以附加到方法上的辅助头。头可能包括 `Reply-To`,`Return-Path`或者几乎任何东西。但是为了在Python中成为一个有效的标识符,一个名字不能包含-字符。一般来说,这个字符代表减法。因此,不可能调用`Reply-To = my@email.com`的函数。看来我们太急于使用键参数,因为它们是我们刚刚在本章中了解到的新工具。 </b> 我们将不得不把这个参数改成普通的字典;这将会奏效,因为任何字符串都可以用作字典中的键。默认情况下,我们想要这本字典为空,但是我们不能将默认参数设置为空字典。所以,我们必须将默认参数设为`None`,然后在方法的开始设置字典: ``` def send_email(subject, message, from_addr, *to_addrs, host="localhost", port=1025, headers=None): headers = {} if headers is None else headers ``` 如果我们在一个终端上运行调试SMTP服务器,我们可以在Python解释器中测试代码: ``` >>> send_email("A model subject", "The message contents", "from@example.com", "to1@example.com", "to2@example.com") ``` 然后,如果我们检查调试SMTP服务器的输出,我们会得到以下结果: ``` ---------- MESSAGE FOLLOWS ---------- Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: A model subject From: from@example.com To: to1@example.com X-Peer: 127.0.0.1 The message contents ------------ END MESSAGE ------------ ---------- MESSAGE FOLLOWS ---------- Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: A model subject From: from@example.com To: to2@example.com X-Peer: 127.0.0.1 The message contents ------------ END MESSAGE ------------ ``` 太好了,它已经把我们的包含主题和消息内容的电子邮件“发送”到了两个预期的地址。现在我们可以发送消息了,让我们开始处理电子邮件 分组管理系统。我们需要一个对象,能够和电子邮件地址组相匹配。因为这是一种多对多的关系(任何一个电子邮件地址可以在多个组中;任何一个组都可以关联有多个电子邮件地址),我们研究过的数据结构似乎没有一个是非常理想的。我们可以尝试用一个组名字典,与相关的电子邮件地址列表相匹配,但这将重复电子邮件地址。我们也可以试试一个电子邮件地址字典与组匹配,但导致组重复。两者似乎都不是最佳选择。让我们试试后一个版本,尽管直觉告诉我,组到电子邮件地址的解决方案会更简单。 </b> 因为我们字典中的值总是唯一电子邮件地址的集合,我们可能应该将它们存储在一个`set`容器中。我们可以使用`defaultdict`确保每个键始终有一个可用的`set`容器: ``` from collections import defaultdict class MailingList: '''管理发送电子邮件地址的分组''' def __init__(self): self.email_map = defaultdict(set) def add_to_group(self, email, group): self.email_map[email].add(group) ``` 现在,让我们添加一个方法,允许我们从一个或更多组中收集所有电子邮件地址。这可以通过将组列表转换为集合来实现: ``` def emails_in_groups(self, *groups): groups = set(groups) emails = set() for e, g in self.email_map.items(): if g & groups: emails.add(e) return emails ``` 首先,看看我们迭代的内容:`self.email_map.items()`。这种方法 ,当然会为字典中的每个项目返回键值对元组。值是代表组的字符串集合。我们把它们分成两个变量,命名为`e`和`g`,代表电子邮件地址和组的缩写。仅当传递的组与电子邮件地址组相交时,我们才将电子邮件地址添加到返回集合中。`g & groups`语法是`g.intersection(groups)`的快捷方式;`set`类通过使用特殊的`__add__`方法调用`intersection`来实现这一点。 > 使用集合解析器可以使这段代码更加简洁,我们将在第9章“迭代器模式”中讨论。 现在,有了这些构造块,我们可以简单地将一个方法添加到我们的`MailingList`类中,向特定组发送消息: ``` def send_mailing(self, subject, message, from_addr, *groups, headers=None): emails = self.emails_in_groups(*groups) send_email(subject, message, from_addr, *emails, headers=headers) ``` 该函数依赖于可变参数列表。作为输入,它需要一个组列表作为可变参数。它获取指定组的电子邮件列表,并将它们可变参数传递给`send_email`,其他参数也被传递到这个方法中。 </b> 可以在一个命令行窗口测试该程序,确保SMTP调试服务器运行正常,在第二个命令行窗口中,使用以下命令加载代码: ``` python -i mailing_list.py ``` 创建一个`MailingList`对象: ``` >>> m = MailingList() ``` 然后创建几个假的电子邮件地址和组,大致如下: ``` >>> m.add_to_group("friend1@example.com", "friends") >>> m.add_to_group("friend2@example.com", "friends") >>> m.add_to_group("family1@example.com", "family") >>> m.add_to_group("pro1@example.com", "professional") ``` 最后,使用这样的命令向特定的组发送电子邮件: ``` >>> m.send_mailing("A Party", "Friends and family only: a party", "me@example.com", "friends", "family", headers={"Reply-To": "me2@example.com"}) ``` 给定组中每个电子邮件地址应该显示在SMTP服务器上的控制台上。 </b> 邮件列表可以正常工作,但是有点没用;一旦我们离开程序,我们的信息数据库丢失了。让我们修改它,添加一组方法,将电子邮件组列表从文件加载并保存到文件中。 </b> 通常,在磁盘上存储结构化数据时,有必要思考它是如何储存的。海量数据库系统存在的原因之一是如果其他人已经把这种想法应用到数据存储中,你就不必这么做了。我们会在下一章中,研究一些数据序列化机制,但对于这个例子,让我们保持简单,并采用第一个可行的解决方案。 </b> 我想到的数据格式是存储每个电子邮件地址,后面跟一个空格,再跟逗号分隔的组列表。这种格式似乎合理,我们会在后面继续讨论格式,因为数据格式不是本章的主题。然而,为了说明为什么你需要努力思考如何在磁盘上以什么样的格式存储数据,让我们强调格式的几个问题。 </b> 首先,电子邮件地址中的空格字符在技术上是合法的。大多数电子邮件提供商禁止空格(他们有充分的理由),但是定义电子邮件地址的规范说,如果电子邮件地址有引号,它可以包含空格。如果我们要用一个空格作为我们的数据格式中的哨兵,技术上我们应该能够区分这个空格和作为电子邮件一部分的空格。为简单起见,我们假装这不是真的,但现实生活中的数据编码充满了像这样的愚蠢问题。第二,考虑逗号分隔的组列表。如果有人决定在一个组名里放置一个逗号,会发生什么呢?如果我们决定在组名中使用逗号是非法的,我们应该在`add_to_group`添加验证来确保这一点。为了教学清楚,我们也将忽略这个问题。最后,还有许多安全隐患需要考虑:有人会通过在他们电子邮件地址中放一个假的逗号,让他们分到错误的组里吗?如果解析器遇到无效文件,它会做什么? </b> 这次讨论的要点是尝试使用已经在真实环境测试过的一种数据存储方法,而不是设计自己的数据序列化协议。那里有一大堆你可能忽略的奇怪的异常案例,最好使用已经遇到并修复了这些异常案例的代码。 </b> 但是忘记这一点,让我们写一些基本的代码,但不要指望这么点代码可以确保这种简单的数据格式是安全: ``` email1@mydomain.com group1,group2 email2@mydomain.com group2,group3 ``` 这段代码如下: ``` def save(self): with open(self.data_file, 'w') as file: for email, groups in self.email_map.items(): file.write('{} {}\n'.format(email, ','.join(groups)) ) def load(self): self.email_map = defaultdict(set) try: with open(self.data_file) as file: for line in file: email, groups = line.strip().split(' ') groups = set(groups.split(',')) self.email_map[email] = groups except IOError: pass ``` 在`save`方法中,我们在上下文管理器中打开该文件,并将格式化字符串写入该文件。记住换行符;Python没有为我们添加这些。`load`方法首先重置字典(以防它包含以前调用加载的数据),使用`for...in`语法,循环遍历文件中的每一行。同样,换行符也应包含在行变量中,所以我们必须调用`.strip()`。我们将在下一章中学习更多关于这种字符串操作的知识。 </b> 在使用这些方法之前,我们需要确保对象有一个`self.data_file`属性,这可以通过修改`__init__`来完成: ``` def __init__(self, data_file): self.data_file = data_file self.email_map = defaultdict(set) ``` 我们可以在解释器中测试这两个方法: ``` >>> m = MailingList('addresses.db') >>> m.add_to_group('friend1@example.com', 'friends') >>> m.add_to_group('family1@example.com', 'friends') >>> m.add_to_group('family1@example.com', 'family') >>> m.save() ``` `addresses.db`文件将包含下面两行: ``` friend1@example.com friends family1@example.com friends,family ``` We can also load this data back into a MailingList object successfully: 我们也可以将这个文件加载回`MailingList`对象中: ``` >>> m = MailingList('addresses.db') >>> m.email_map defaultdict(<class 'set'>, {}) >>> m.load() >>> m.email_map defaultdict(<class 'set'>, {'friend2@example.com': {'friends\n'}, 'family1@example.com': {'family\n'}, 'friend1@example.com': {'friends\n'}}) ``` 如你所见,我忘记了`load`命令,也可能很容易忘记`save`命令。让任何想在他们代码中使用我们的`MailingList`API人都容易一点,让我们提供支持上下文管理器的方法: ``` def __enter__(self): self.load() return self def __exit__(self, type, value, tb): self.save() ``` 这些简单的方法只是将它们的工作委托给加载和保存,但是我们现在可以在交互式解释器中编写这样的代码,并且知道所有以前存储的地址会被加载,当我们完成时,整个列表将被保存: ``` >>> with MailingList('addresses.db') as ml: ... ml.add_to_group('friend2@example.com', 'friends') ... ml.send_mailing("What's up", "hey friends, how's it going", 'me@ example.com', 'friends') ```