4.4意想不到的对象
1.循环对象
Python中的许多语法结构都是由对象实现的,循环就可以通过对象实现。循环对象并不是在Python诞生之初就存在的,但它的发展极为迅速,特别是在Python3时代,循环对象正在成为循环的标准形式。
所谓的循环对象包含有一个__next__()方法。这个方法的目的是生成循环的下一个结果。在生成过循环的所有结果之后,该方法将抛出Stoplteration异常。
当一个像for这样的循环语法调用循环对象时,它会在每次循环的时候调用__next__()方法,直到Stoplteration出现。循环接收到这个异常,就会知道循环已经结束,将停止调用__next__()。
现用内置函数iter()iterant把一个列表转变为循环对象。这个循环对象将拥有__next__()方法法。多次调用__next__()方法,将不断返回列表的值, 直到出现异常:
>>> example_iter = iter([1,2]) >>> example_iter.__next__() 1 >>> example_iter.__next__() 2 >>> example_iter.__next__() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>>
上面重复调用__next__()的过程,就相当于手动进行了循环。
把循环对象包裹在for中循环:
for item in iter([1,2]): print(item) —————————————————————— 1 2
在这里,for结构自动调用__next__(),将该方法的返回值赋予给item。可以省去内置函数iter的转换是因为for结构会自动执行这一转换。
相对于序列,循环对象的好处在于:不用在循环还没开始的时候,就生成要使用的元素。所有要使用的元素可以在循环过程中逐渐生成。这样,不仅节省了空间,提高了效率,还会使编程更加灵活。
我们可以借助生成器(generator)来自定义循环对象。生成器的编写方法和函数定义类似,只是在return的地方改为yield。生成器中可以有多个yield。当生成器遇到一个yield时,会暂停运行生成器,返回yield 后面的值。当再次调用生成器的时候,会从刚才暂停的地方继续运行,直到下一个yield。生成器自身又构成一个循环对象,每次循环使用一个yield返回的值。
下面是一个生成器:
def gen(): a = 100 yield a a = a*8 yield a yield 1000 for i in gen(): print(i) _______________ 100 800 1000
再考虑下面一个生成器:
def gen(): i = 0 while i < 10000: i = i + 1 yield i
这个生成器能产生10000个元素。如果先创建序列保存这10000个元素,再循环遍历,那么这个序列将占用大量的空间。出于同样的原因,Python中的内置函数range()返回的是一个循环对象,而不是一个序列。
2.函数对象
在Python中函数也是一种对象。实际上,任何一个有 __call__()特殊方法的对象都被当作是函数。比如下面的例子:
class SampleMore(object): def __call__(self,a): return a + 5 add_five = SampleMore() print(add_five(2)) _________________________ 7
add_five为SampleMore类的一个对象,当被调用时,add_five执行加5的操作。
3.模块对象
前面说过,Python中的模块对应一个.py文件。模块也是对象。比如标准库中的模块time:
import time print(dir(time)) ________________ ['CLOCK_MONOTONIC', 'CLOCK_MONOTONIC_RAW', 'CLOCK_PROCESS_CPUTIME_ID', 'CLOCK_REALTIME', 'CLOCK_THREAD_CPUTIME_ID', 'CLOCK_UPTIME_RAW', '_STRUCT_TM_ITEMS', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'altzone', 'asctime', 'clock_getres', 'clock_gettime', 'clock_gettime_ns', 'clock_settime', 'clock_settime_ns', 'ctime', 'daylight', 'get_clock_info', 'gmtime', 'localtime', 'mktime', 'monotonic', 'monotonic_ns', 'perf_counter', 'perf_counter_ns', 'process_time', 'process_time_ns', 'sleep', 'strftime', 'strptime', 'struct_time', 'thread_time', 'thread_time_ns', 'time', 'time_ns', 'timezone', 'tzname', 'tzset']
可以看到,time有很多属性可以调用,例如sleep方法。我们之前用import语句引入其他文件中定义的函数,实际上就是引入模块对象的属性,比如:
from time import sleep sleep(10) print("wake up") ________________ 10秒后出现wake up
还可以用简单暴力的方法,一次性引入模块的所有属性:
from time import * sleep(10)
既然知道了 sleep()是time的一个方法,也可以利用对象属性的方式来调用它。
import time time.sleep(10)
调用方法时附带上对象名的好处是可以拓展程序的命名空间,避免同名冲突。例如,如果两个模块中都有sleep()方法可以通过不一样的模块名来区分开来:
在my_time.py中写入函数:
def sleep(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 t t.sleep(10)
在引入名字较长的模块时,这个换名字的办法能有效地挽救程序员的手指。
可以将功能相似的模块放在同一个文件夹中,构成一个模块包。比如放在this_dir中:
import this_dir.module
引入this dir文件夹中的module模块。
该文件夹中必须包含一个__init__.py的文件,提醒Python,该文件为一个模块包。__init__.py可以是一个空文件。
每个模块对象都有一个__name__属性,用来记录模块的名字,例如:
import time print(time.__name__)
当一个.py文件作为主程序运行时,比如python foo.py这个文件也会有一个对应的模块对象。但这个模块对象的 _name__属性会是 "__main__"。因此在很多.py文件中可以看到下面的语句:
if__name__ == "__main__":
它的意思是说,如果这个文件作为一个主程序运行,那么将执行下面的操作。有的时候,一个.py文件中同时有类和对象的定义以及对它们的调用。通过把调用语句放到上面的if中,就可以在调用时不执行这些调用语句了。
4.异常对象
程序中加入异常处理的try结构可以捕捉程序中出现的异常。实际上,我们捕捉到的也是一个对象,比如:
try: m = 1/0 except ZeroDivisionError as e: print("Catch NameError in the sub-function") print(type(e)) print(dir(e)) #异常对象的属性 print(e.args) __________________________________ Catch NameError in the sub-function <class 'ZeroDivisionError'> ['__cause__', '__class__', '__context__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__suppress_context__', '__traceback__', 'args', 'with_traceback'] ('division by zero',)
注意⚠️python3要把书里的print(e.message)改成print(e.args)
利用except... as...的语法,在except结果中用e来代表捕获到的类型对象。关键字except直接跟随的ZeroDivisionError实际上是异常对象的类。正因为如此在举出异常时会创建一个异常对象:
raise ZeroDivisionError()
在Python中,循环、函数、模块、异常都是某种对象。当然,我们 可以完全按照面向过程中的方式来调用这些语法,而不必关注它们底层的对象模型。
附录A代码规范
类的命名采用首字母大写的英文单词。如果由多个单词连接而成,则每个单词的首字母都大写。单词之间不出现下画线。
对象名、属性名和方法名,全部用小写字母。单词之间用下画线连接。