昨天来源:CSDN
引言:Python 不只是一门支持面向对象范式的语言。在多范式的外表下,Python 用对象来构建它的大框架。因此,我们可以及早切入面向对象编程,从而了解Python的深层魅力。 本文选自《从Python开始学编程》,我们来看看Python那些意想不到的对象。Python 中的许多语法结构都是由对象实现的,循环就可以通过对象实现。循环对象并不是在Python 诞生之初就存在的,但它的发展极为迅速,特别是在Python 3 时代,循环对象正在成为循环的标准形式。
那么,什么是循环对象呢?所谓的循环对象包含有一个next方法1。这个方法的目的是生成循环的下一个结果。在生成过循环的所有结果之后,该方法将抛出StopIteration 异常。 当一个像for 这样的循环语法调用循环对象时,它会在每次循环的时候调用next方法,直到StopIteration出现。循环接收到这个异常,就会知道循环已经结束,将停止调用next。 我们用内置函数iter把一个列表转变为循环对象。这个循环对象将拥有next方法。我们多次调用next方法,将不断返回列表的值,直到出现异常:>>>example_iter = iter([1, 2]) >>>example_iter.__next__ # 显示1 >>>example_iter.__next__ # 显示2# 出现StopIteration 异常。我们上面重复调用next的过程,就相当于手动进行了循环。我们可以把循环对象包裹在for 中自动进行循环:
foritemin iter([1, 2]): print(item)在这里,for 结构自动调用 next方法,将该方法的返回值赋予给item。循环知道出现StopIteration 的时候结束。当然,我们可以省去内置函数iter 的转换。这是因为,for 结构会自动执行这一转换。
相对于序列,循环对象的好处在于:不用在循环还没开始的时候,就生成要使用的元素。所有要使用的元素可以在循环过程中逐渐生成。这样,不仅节省了空间,提高了效率,还会使编程更加灵活。 我们可以借助生成器(generator)来自定义循环对象。生成器的编写方法和函数定义类似,只是在return 的地方改为yield。生成器中可以有多个yield。当生成器遇到一个yield 时,会暂停运行生成器,返回yield后面的值。当再次调用生成器的时候,会从刚才暂停的地方继续运行,直到下一个yield。生成器自身又构成一个循环对象,每次循环使用一个yield 返回的值。 下面是一个生成器:defgen: a = 100yield a a = a*8yield a yield1000该生成器共有三个yield, 如果用作循环对象时,会进行三次循环。
for i in gen: print(i)再考虑下面一个生成器:
defgen: i = 0while i < 10000000: i = i + 1yield i这个生成器能产生10 000 000 个元素。如果先创建序列保存这10 000 000 个元素,再循环遍历,那么这个序列将占用大量的空间。出于同样的原因,Python 中的内置函数range返回的是一个循环对象,而不是一个序列。
前面说过,在Python 中,函数也是一种对象。实际上,任何一个有call特殊方法的对象都被当作是函数。比如下面的例子:
callclassSampleMore(object):def__call__(self, a):return a + 5 add_five = SampleMore # 生成函数对象 print(add_five(2)) # 像一个函数一样调用函数对象,结果为7。add_five 为SampleMore 类的一个对象,当被调用时,add_five 执行加5 的操作。
前面说过,Python 中的模块对应一个.py文件。模块也是对象。比如,我们直接引入标准库中的模块time:
import timeprint(dir(time))可以看到,time 有很多属性可以调用,例如sleep方法。我们之前用import 语句引入其他文件中定义的函数,实际上就是引入模块对象的属性,比如:
from time import sleepsleep(10) print("wake up")模块time 的sleep会中止程序。调用时的参数说明给了中止的时间。我们还可以用简单暴力的方法,一次性引入模块的所有属性:
from time import * sleep(10)既然知道了sleep是time的一个方法,那么我们当然可以利用对象.属性的方式来调用它。
import timetime.sleep(10)我们在调用方法时附带上了对象名。这样做的好处是可以拓展程序的命名空间,避免同名冲突。例如,如果两个模块中都有sleep方法,那么我们可以通过不一样的模块名来区分开来。在my_time.py 中写入函数:
defsleep(self): print("I am sleeping.")在main.py 中引入内置模块time 和自定义模块my_time:
import time import my_time time.sleep my_time.sleep上面的两次对sleep方法的调用中,我们通过对象名区分出了不同的sleep。 在引入模块时,我们还可以给模块换个名字:
import time as tt.sleep(10)在引入名字较长的模块时,这个换名字的办法能有效地挽救程序员的手指。 可以将功能相似的模块放在同一个文件夹中,构成一个模块包。比如放在this_dir 中:
import this_dir.module引入this_dir 文件夹中的module 模块。 该文件夹中必须包含一个init.py 的文件,提醒Python,该文件夹为一个模块包。init.py 可以是一个空文件。 每个模块对象都有一个name属性,用来记录模块的名字,例如:
import timeprint(time.__name__)当一个.py 文件作为主程序运行时,比如python foo.py,这个文件也会有一个对应的模块对象。但这个模块对象的name属性会是”main”。因此,我们在很多.py 文件中可以看到下面的语句:
if __name__ == "__main__": ...它的意思是说,如果这个文件作为一个主程序运行,那么将执行下面的操作。有的时候,一个.py 文件中同时有类和对象的定义,以及对它们的调用。当这些.py 文件作为库引入时,我们可能并不希望执行这些调用。通过把调用语句放到上面的if 中,就可以在调用时不执行这些调用语句了。
前面我们提到过,可以在程序中加入异常处理的try 结构,捕捉程序中出现的异常。实际上,我们捕捉到的也是一个对象,比如:
try: m = 1/0except ZeroDivisionError as e: print("Catch NameError in the sub-function") print(type(e)) # 类型为"exceptions.ZeroDivisionError" print(dir(e)) # 异常对象的属性 print(e.message) # 异常信息integer division or modulo by zero利用except… as… 的语法,我们在except 结果中用e 来代表捕获到 的类型对象。关键字except 直接跟随的ZeroDivisionError 实际上是异常 对象的类。正因为如此,我们在举出异常时会创建一个异常对象:
raise ZeroDivisionError在Python 中,循环、函数、模块、异常都是某种对象。当然,我们可以完全按照面向过程中的方式来调用这些语法,而不必关注它们底层的对象模型。但出于学习的目的,这些语法结构的对象模型能加深我们对Python 的理解。