Python(五)-生成器、迭代器、装饰器、程序目录规范
目录:
1、生成器
2、迭代器
3、装饰器
4、软件目录结构规范
第1章 生成器、迭代器、装饰器
1.1 列表生成式
现在有个需求,看列表[0,1,2,3,4,5,6,7,8,9],需求要求把列表里的每个值加1,你怎么实现?
实例1:
#!/usr/bin/env python a=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] b=[] for i in a: b.append(i+1) a=b print(a) 结果: [1, 2, 3, 4, 5, 6, 7, 8, 9,10]
实例2:
#!/usr/bin/env python a=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] a = map(lambda x:x+1,a) for i in a: print(i) 结果: 1 2 3 4 5 6 7 8 9 10
实例3:
#!/usr/bin/env python a=[i+1 for i in range(10)] print(a) 结果: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 注意:实例3就是列表生成式。
1.2 生成器
通过列表生成式,我们可以直接创建一个列表,但是受到内存限制,列表容量肯定式有限的,而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间,在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法,第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:
例如:
#列表生成式 print([ i*2 for i in range(10) ]) #使代码更简介 #生成器 print( i*2 for i in range(10) ) 结果: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] <generator object <genexpr> at 0x101384830> 注意:生成器打印出来是一个内存地址,只能使用__next__方法进行一个一个调用。
1.2.1 生成器小结
1、直邮在调用时才会生成相应数据
2、使用__next__方法进行取下一个值,在2.7里面时next()
3、它只保留一个值,只记住当前一个值,也不可以跳着取值。
1.2.2 使用函数生成生成器(斐波那契算法)
#斐波那契算法:算法为前面的两个数相加得出第三个数,一次类推。 def fib(max): n, a, b = 0, 0, 1 while n < max: yield b #有这个方法就会将打印出来的b,编程生成器的方式,因此,此函数就成为了生成器,想把谁返回到外面就用yield #yield终端函数的状态,然后使用next调用就会回到函数 a,b=b,a+b n=n+1 return 'done' #返回值,是为了后续抓去错误时使用。 # #抓取报错信息 g=fib(6) #将斐波那契的函数返回值付值给g变量 while True: try: #尝试语法 x = next(g) #使用next方法调用此函数,next同等与__next__ print('g:',x) #打印返回值 except StopIteration as e: #使用except 语法抓去报错StopIteration,付值给e,抓去到报错进行返回一个值,这个返回值就是函数的return的值 #为什么要抓去报错呢?因为我们不知道生成器有多长,等死循环到最后没值的时候,那么程序就会报错,所以需要抓去到, #进行指定返回值。 print('生成器返回值为:',e.value) #打印生成器的返回值 break #抓到错误后,推出循环
小结:
1、yield特性:可以停止函数,将函数停止在右yield这一状态,进行跳出函数,执行此函数后面的逻辑,等在使用next方法调用时,就回到此函数,进行执行函数下面的内容。
1.2.3 使用yield特性进行单线程并发
import time def chibaozi(name): #定义个一个函数 print('%s 准备吃包子!' %name) #打印谁来吃包子 while True: #来一个循环 baozi = yield #付值是为来后面使用send方法进行传值 print('包子%s来了,被%s吃了'%(baozi,name)) #打印一下,为了显示返回到这个yield时执行下面的内容 def shengchan(name): #在定义一个函数 c = chibaozi('A') #调用上面的函数,进行付值,在这里是将函数变成生成器的过程 c2 = chibaozi('B') c.__next__() #在这里同时调用两次,在这里调用一次,是为了调用第一次,为了打印yield前面的逻辑内容 c2.__next__()#同上面的next也是一样。 print('老子开始吃包子了!') #打印开始 for i in range(10):#循环十次 time.sleep(1) #睡眠1秒 print('做了2个包子')#打印做的包子 c.send(i) c2.send(i) #调用并传入值 shengchan('chenxin') #进行调用生产函数
注意:这种单线程并发又叫做协程,比线程单位还小的一个单位。
1.3 迭代器
1.3.1 迭代器说明
我们知道,可以直接作用于for循环的数据类型有以下几种
一类是集合数据类型:如list、tuple、dict、set、str等
一类是generator,包括生成器和带yield等generator function
1.3.2 判断是否为可迭代对象
这些可以直接作用于for循环的对象统称为可迭代对象:iterable。
可以使用isinstance()判断一个对象是否iterable对象:
from collections import Iterable print(isinstance([], Iterable)) #判断列表是不是可迭代对象 print(isinstance({}, Iterable))#判断字典是不是可迭代对象 print(isinstance('abc', Iterable))#判断字符串是不是可迭代对象 print(isinstance((x for x in range(10)), Iterable))#判断生成器是不是可迭代对象 print(isinstance(100, Iterable))#判断数字是不是可迭代对象 结果: True True True True False
1.3.3 判断是否为迭代器
而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。
**可以被next函数条用并不断返回下一个值的对象称为迭代器:iterator
可以使用isinstance()判断一个对象是否是iterator对象:
#判断是不是迭代器 from collections import Iterator print(isinstance((x for x in range(10)), Iterator)) print(isinstance([], Iterator)) print(isinstance({}, Iterator)) print(isinstance('abc', Iterator)) 结果: True False False False 生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
1.3.4 使用iter方法生成迭代器
把list、dict、str等Iterable变成Iterator可以使用iter()函数:
print(isinstance(iter([]), Iterator)) print(isinstance(iter('abc'), Iterator)) #结果 True True
你可能会问,为什么list、dict、str等数据类型不是Iterator?
这是因为Python等Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误,可以把这个数据流看做时一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算时惰性的,只有在需要返回下一个数据时才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数,而实用list是永远不可能存储全体自然数的。
1.3.5 小结:
1、凡是可作用于for循环的对象都是Iterable类型。
2、凡是可作用next函数的对象都是Iterator类型,他们表示一个惰性计算的序列。
3、集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。
4、python的for循环本质上就是通过不断调用next函数实现的。例如:
for x in [1,2,3,4,5]: pass 实际上完全等价于: # 首先获得Iterator对象: it = iter([1, 2, 3, 4, 5]) # 循环: while True: try: # 获得下一个值: x = next(it) except StopIteration: # 遇到StopIteration就退出循环 break
1.4 装饰器
1.4.1 定义
装饰器是由两种不同的函数类型组成的,分别是高阶函数与嵌套函数组成。
1、高阶函数:
a) 第一规则,修改了被装饰的函数的调用方法
import time def bar(): time.sleep(2) print('我是bar函数') def test1(func): start_time = time.time() func() stop_time = time.time() print('我是func运行的时间%s'%(stop_time-start_time)) test1(bar) 结果: 我是bar函数 我是func运行的时间2.0051779747009277
b) 第二规则:给装饰器添加一个返回值,
#高阶函数(第二个规则) import time def bar(): time.sleep(2) print('我是bar函数') def test2(func): print(func) return func bar=test2(bar) bar() 结果: <function bar at 0x10137ad08> 我是bar函数 2、嵌套函数 def foo(): print('in the foo') def bar(): print('in the bar') bar() foo() 结果: in the foo in the bar
3、装饰器,高阶函数+嵌套函数
import time def timer(func): #timer(test1) func=test1 def deco(*args,**kwargs): #可以支持传入任意多个参数 start_time = time.time() func(*args,**kwargs) #此func事timer的参数传进来的,就是被装饰的函数的内存地址,func=test1 stop_time = time.time() print('func的运行时间%s'%(stop_time-start_time)) return deco #这个return返回deco的执行结果,也就是test1的执行结果 @timer #test1=timer(test1) #装饰下面的test1 def test1(): time.sleep(2) print('in the test1') test1() 结果: in the test1 func的运行时间2.0028600692749023
1.4.2 统一认证接口(高潮版)
目前只有被装饰的函数可以传参数
import time user,passwd='chenxin','123' def auth(func):#func=被装饰的函数的内存地址 def wrapper(*args, **kwargs): username = input('username:').strip() password = input('password:').strip() if user == username and passwd == password: print('authentication') res = func(*args, **kwargs) # 调用被装饰函数 else: exit('cuowu') return wrapper def index(): print('welcome to index page') @auth def home(): print('welcome to home page') @auth def bbs(): print('welcome to bbs page') home() bbs() 结果: username:chenxin password:123 authentication welcome to home page username:chenxin password:123 authentication welcome to bbs page
1.4.3 统一认证接口(超级赛亚人高潮版)
目前被装饰器的也可以传参数,装饰器本身也可以传参数,需求是auth有不同的认证地方和方式,例如:home只认证本地,而bbs认证数据库的用户名密码等。
import time user,passwd = 'cx','abc123' def auth(auth_type): print('auth func:',auth_type) def outer_wrapper(func): def wrapper(*args, **kwargs): print('wrapper func args :', *args,**kwargs) if auth_type == 'local': username = input('Username:').strip() password = input('Password:').strip() if user == username and passwd == password: print('认证通过!!!') return func(*args, **kwargs) else: exit('认证失败!!') elif auth_type == 'ldap': print('不会ldap!!') return wrapper return outer_wrapper def index(): print('welcome to index page') @auth(auth_type="local") def home(): print('welcome to home page') return 'from home' @auth(auth_type="ldap") def bbs(): print('welcome to bbs page') index() home() bbs() 结果: auth func: local auth func: ldap welcome to index page wrapper func args : Username:cx Password:abc123 认证通过!!! welcome to home page wrapper func args : 不会ldap!!
第2章 软件目录结构规范
为什么要设计好目录结构?
“设计项目目录结构”,就和“代码编码风格”一样,属于个人风格问题。对于这种风格的规范,一直都存在两种态度,
1、一类认为,这种个人风格问题“无关紧要”。理由是能让城乡work就好,风格问题根本不是问题。
2、另一类认为,规范化能更好的控制程序结构,让程序具有更高的可读性。
我是比较偏向后者,
"项目目录结构"其实也是属于"可读性和可维护性"的范畴,我们设计一个层次清晰的目录结构,就是为了达到以下两点:
1、可读性高:不熟悉这个项目的代码的人,一眼就能看懂目录结构,知道程序启动脚本是哪个,测试目录在哪,配置文件在哪等等,从而非常快速的了解这个项目。
2、可维护性高:定义好组织规则后,维护者就能很明确的知道,新增的哪个文件和代码应该放在什么目录下,这个好处是,随着时间的推移,代码/配置的规模增加,项目结构不会混乱,仍然能够组织良好。
所以,我认为保持一个层次清晰的目录结构是有必要的,更何况组织一个良好的工程目录,其实是一件很容易的事。
2.1 目录组织方式
关于如何组织一个较好的Python工程目录结构,已经有一些得到了共识的目录结构,在Stackoverflow的这个问题上,能看到大家对Python目录结构的讨论。
这里面说的已经很好了,我也不打算重新造轮子举例各种不同的方式,这里我说一下我的理解和体会。
假如你的项目名为foo,我比较建议的最方便快捷目录结构这样就足够了:
Foo/ |-- bin/ | |-- foo | |-- foo/ | |-- tests/ | | |-- __init__.py | | |-- test_main.py | | | |-- __init__.py | |-- main.py | |-- docs/ | |-- conf.py | |-- abc.rst | |-- setup.py |-- requirements.txt |-- README
简要解释一下:
1、bin:存放项目的一些可执行文件,当然你可以起名scripts之类的也可行。
2、foo:存放项目的所有源代码。
a)源代码中的所有模块、包都应该放在此目录,不要置于顶层目录
b)其子目录tests存放单元测试代码
c)程序的入口最好命名为main.py
3、docs:存放一下文档
4、setup.py:安装、部署、打包脚本。
5、requirements.txt:存放软件依赖的外部Python包列表。
6、README:项目说明文件
除此之外,有一些方案给出了更加多的内容,比如LICENSE.txt,Changelog.txt等文件,我没有列在这里,因为这些东西主要是项目开源的时候需要用到,如果你想写一个开源软件,目录该如何组织,可参考
2.1.1 关于README内容
这个我觉得是每个项目都应该有的一个文件,目的是能简要描述该项目的信息,让读者快速了解这个项目。
它需要说明一下几个事项:
1、软件定位,软件基本功能。
2、运行代码的方法:安装环境、启动命令等。
3、简要的使用说明
4、代码目录结构说明,更详细点可以说明软件的基本原理。
5、常见问题说明
以上几点是比较好的一个README,在软件开发初期,由于开发过程中以上内容可能不明确或者发生变化,并不是一个要在一开始就有信息都补全,但在项目完结的时候,是需要撰写这样的一个文档的,
可以参考Redis源码中Readme的写法,这里面简洁但是清晰的描述了Redis功能和源码结构。
2.1.2 关于requirements.txt和setup.py
setup.py
一般来说,用setup.py来管理代码的打包、安装、部署问题,业界标准的写法是用Python流行打包工具setuptools来管理这些事情,这种方式普遍应用于开源项目中,不过这里的核心思想不是用来标准化的工具来解决这些问题,而是说,一个项目一定要有一个安装部署工具,能快速便捷的在一台新机器上将环境装好、代码部署好和将程序运行起来。
我们开始接触Python写项目的时候,安装环境、部署代码、运行程序这个过程全是手动完成,,遇过一下问题。
1、安装环境时经常忘了最近又添加了一个新的Python包,结果一到线上运行,程序就出错了。
2、Python包的版本依赖问题,有时候我们程序中使用一个版本的Python包,但是官方的已经是最新的包了,通过手动安装就可能安装错了。
3、如果依赖的包很多的话,一个一个安装这些依赖是很费时的事情。
4、新同学开始写项目的时候,将程序跑起来非常麻烦,因为可能经常忘了要怎么安装各种依赖。
Setup.py可以将这些事情自动化起来,提高效率、减少出错的概率,“复杂的东西自动化,能自动化的东西一定要自动化”,是一个非常好的习惯。
Setuptools的文档比较庞大,刚接触的话,可能不太好找到切入点,学习技术的方式就是看他人是怎么用的,可以参考一下Python的一个web框架,flash是如何写的:setup.py,当然,简单点自己写个安装脚本(depliy.sh)替代setup.py也未尝不可。
requirements.txt
这个文件存在的目的是:
1、方便开发者维护软件的包依赖,将开发过程中新增的包添加进这个目录中,避免在setup.py安装依赖时漏掉软件包。
2、方便读者明确项目使用了哪些Python包。
这个文件的格式是每一行包含一个包依赖的说明,通常是flask>=0.10这种格式,要求是这个格式能被pip识别,这样就可以简单的通过pip install –r requirements.txt来把所有Python包依赖都装好了。
2.1.3 关于配置文件的使用方法
注意,在上面的目录结构中,没有将conf.py放在源代码目录下,而是放在docs目录下。
很多项目对配置问的使用方法是:
1、配置文件写在一个或多个Python文件中,比如此处的conf.py
2、项目中哪个模块用到这个配置文件就直接通过import conf这种形式来在代码中使用配置。
这种做法不是很好:
1、这让单元测试变的困难(因为模块内部依赖了外部的配置)
2、另一方面配置文件作为用户控制程序的接口,应当可以由用户自由指定该文件的路径。
3、程序组件可复用性太差,因为这种贯穿所有模块的代码硬编程方式,使得大部分模块都依赖conf.py这个文件,
所以为认为配置的使用,更好的方式为:
1、模块的配置都是可以灵活配置的,不受外部配置文件的影响。
2、程序的配置也是可以灵活控制的。
能够佐证这个思想的是,用过nginx和MySQL的都知道,nginx、MySQL这些程序都可以自由指定用户配置。
所以,不应该在代码中直接import conf来使用配置文件,上面目录结构中的conf.py,是给出的一个配置样例,不是在写死在程序中直接引用的配置文件,可以通过main.py启动参数指定配置路径的方式来让程序读取配置内容,当然,这里的conf.py你可以换个类似的名字,比如,settings.py,或者你可以使用其他格式的内容来编写配置文件,比如settings.yaml之类的。