1. Python - with语句
在实际的编码过程中,有时有一些任务,需要事先做一些设置,事后做一些清理,这时就需要Python
的with
出场了,with
能够对这样的需求进行一个比较优雅的处理,最常用的例子就是对访问文件的处理。
with
语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要"清理"操作,释放资源。比如文件使用后的自动关闭,线程中的自动获取和释放等。
-
上下文管理协议(Context Management Protocol):包含方法
__enter__()
和__exit__()
,支持该协议的对象要实现这两个方法。 -
上下文管理器(Context Manager):支持上下文管理协议的对象,这种对象实现了
__enter__()
和__exit__()
方法。上下文管理器定义执行with
语句时要建立的运行时上下文,负责执行with
语句块上下文中的进入与退出操作。通常使用with
语句调用上下文管理器,也可以通过直接调用其方法来使用。
一段基本的with
表达式,其结构是这样的:
with EXPR as VAR:
BLOCK
其中EXPR
可以是任意表达式;as VAR
是可选的。其一般的执行过程是这样的:
(1). 执行EXPR
,生成上下文管理器context_manager
;
(2). 获取上下文管理器的__exit()__
方法,并保存起来用于之后的调用;
(3). 调用上下文管理器的__enter__()
方法;如果使用了as
子句,则将__enter__()
方法的返回值赋值给as
子句中的VAR
;
(4). 执行BLOCK中
的表达式;
(5). 不管是否执行过程中是否发生了异常,执行上下文管理器的__exit__()
方法,__exit__()
方法负责执行“清理”工作,如释放资源等。如果执行过程中没有出现异常,或者语句体中执行了语句break/continue/return
,则以None
作为参数调用__exit__(None, None, None)
;如果执行过程中出现异常,则使用sys.exc_info
得到的异常信息为参数调用__exit__(exc_type, exc_value, exc_traceback)
;
(6). 出现异常时,如果__exit__(type, value, traceback)
返回False
,则会重新抛出异常,让with
之外的语句逻辑来处理异常,这也是通用做法;如果返回True
,则忽略异常,不再对异常进行处理。
自定义上下文管理器
自定义的上下文管理器要实现上下文管理协议所需要的__enter__()
和__exit__()
两个方法:
#!/usr/bin/env python
class DBManager(object):
def __init__(self):
pass
def __enter__(self):
print('__enter__')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('__exit__')
return True
def getInstance():
return DBManager()
with getInstance() as dbManagerIns:
print('with demo')
代码运行结果如下:
__enter__
with demo
__exit__
2. Python - 迭代器
迭代器指的是迭代取值的工具,迭代是指一个重复的过程,每一次重复都是基于上一次结果而来.
迭代提供了一种通用的不依赖索引的迭代取值方式.
一. 可迭代对象
但凡内置有__iter__
方法的对象,都称为可迭代对象,可迭代的对象:str,list,tuple,dict,set
,文件对象
二. 迭代器对象
1. 既内置有__next__
方法的对象,执行该方法可以不依赖索引取值
2. 又内置有__iter__
方法的对象,执行迭代器的__iter__
方法得到的依然是迭代器本身
迭代器一定是可迭代对象,可迭代对象不一定是迭代器对象,文件对象本身就是一个迭代器对象.
dic = {'x':1,'y':2,'z':3}
iter_dic = dic.__iter__()
print(iter_dic.__next__())
print(iter_dic.__next__())
print(iter_dic.__next__())
print(iter_dic.__next__())
结果:
x
y
z
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
for循环本质为迭代器循环
工作原理:
- 先调用in后对象的
__iter__
方法,将其变成一个迭代器对象 - 调用
next
(迭代器),将得到的返回值赋值给变量名 - 循环往复直到
next
(迭代器)抛出异常,for
会自动捕捉异常然后结束循环
PS:可以从for的角度,分辨但凡可以被for循环取值的对象就是可迭代对象
迭代器优点:
- 提供了一种通用不依赖索引的迭代取值方式
- 同一时刻在内存中只存在一个值,更节省内存
迭代器缺点:
- 取值不如按照索引的方式灵活,不能取指定的某一个值,只能往后取,不能往前去
- 无法预测迭代器的长度
3. Python - 生成器
生成器就是一种自定义的迭代器,本质为迭代器
但凡函数内包含yield
关键字,调用函数不会执行函数体代码,会得到一个返回值,该返回值就是生成器对象
例如:
>>> def func():
... print('first')
... yield 1
... print('second')
... yield 2
... print('third')
...
>>> g=func() #调用函数不会执行函数体代码,会得到一个生成器对象
>>> res1=next(g)
first
>>> print(res1)
1
>>> res2=next(g)
second
>>> print(res2)
2
>>>
总结yield
:只能在函数内使用
1. yield
提供了一种自定义迭代器的解决方案
2. yield
可以保存函数的暂停的状态
3. yield
对比return
:
相同点:都可以返回值,值得类型与个数没有限制
不同点:yield
可以返回多次值,而return
只能返回一次值函数就会结束
** 生成器表达式
>>> g = (i**2 for i in range(1,6) if i > 3)
>>> print(g)
<generator object <genexpr> at 0x0000022C27C911B0>
>>> print(next(g))
16
>>> print(next(g))
25
>>>
两种创建生成器的方式(生成器表达式和yield
关键字)
迭代器与生成器的区别:
(1). 生成器
生成器本质上就是一个函数,它记住了上一次返回时在函数体中的位置。
对生成器函数的第二次(或第n次)调用,跳转到函数上一次挂起的位置。
而且记录了程序执行的上下文。
生成器不仅“记住”了它的数据状态,生成还记住了程序执行的位置。
(2). 迭代器
迭代器是一种支持next()
操作的对象。它包含了一组元素,当执行next()
操作时,返回其中一个元素。
当所有元素都被返回后,再执行next()报异常—StopIteration
生成器一定是可迭代的,也一定是迭代器对象
(2). 区别:
①生成器是生成元素的,迭代器是访问集合元素的一中方式
②迭代输出生成器的内容
③迭代器是一种支持next()操作的对象
④迭代器(iterator)
:其中iterator
对象表示的是一个数据流,可以把它看做一个有序序列,但我们不能提前知道序列的长度,只有通过nex()
函数实现需要计算的下一个数据。可以看做生成器的一个子集。
一个生成器对象一定是迭代器对象,是可迭代的。但是一个迭代器对象,不一定是生成器对象。生成器与可迭代器是两个不同的对象
参考:
迭代器与生成器的区别
Python迭代器
4. Python - lambda函数
在Python
中,lambda
的语法是唯一的。其形式如下:
lambda argument_list: expression
-
这里的
argument_list
是参数列表,它的结构与Python
中函数(function)
的参数列表是一样的。 -
这里的
expression
是一个关于参数的表达式。表达式中出现的参数需要在argument_list
中有定义,并且表达式只能是单行的。
这里的lambda argument_list: expression
表示的是一个函数。这个函数叫做lambda
函数。
三个特性
lambda函数有如下特性:
-
lambda函数是匿名的:所谓匿名函数,通俗地说就是没有名字的函数。lambda函数没有名字。
-
lambda函数有输入和输出:输入是传入到参数列表argument_list的值,输出是根据表达式expression计算得到的值。
-
lambda函数一般功能简单:单行expression决定了lambda函数不可能完成复杂的逻辑,只能完成非常简单的功能。由于其实现的功能一目了然,甚至不需要专门的名字来说明。
三、用法
- 将
lambda
函数赋值给一个变量,通过这个变量间接调用该lambda
函数。
例如,add=lambda x, y: x+y
add(1,2)
,输出为3
。
- 将
lambda
函数赋值给其他函数,从而将其他函数用该lambda函数替换。
例如,为了把标准库time
中的函数sleep
的功能屏蔽(Mock)
,我们可以在程序初始化时调用:time.sleep=lambda x:None
。
这样,在后续代码中调用time库的sleep函数将不会执行原有的功能。
例如,执行time.sleep(3)
时,程序不会休眠3秒钟,而是什么都不做。
- 将
lambda
函数作为其他函数的返回值,返回给调用者。
函数的返回值也可以是函数。
例如,return lambda x, y: x+y
返回:加法函数。
这时,lambda函数实际上是定义在某个函数内部的函数,称之为嵌套函数,或者内部函数。
对应的,将包含嵌套函数的函数称之为外部函数。
内部函数能够访问外部函数的局部变量。
- 将
lambda
函数作为参数传递给其他函数。
部分Python内置函数接收函数作为参数。典型的此类内置函数有这些。
filter
函数
此时lambda函数用于指定过滤列表元素的条件。
例如filter(lambda x: x % 3 == 0, [1, 2, 3])指定将列表[1,2,3]中能够被3整除的元素过滤出来,其结果是[3]。
sorted
函数
此时lambda函数用于指定对列表中所有元素进行排序的准则。
例如sorted([1, 2, 3, 4, 5, 6, 7, 8, 9], key=lambda x: abs(5-x))
将列表[1, 2, 3, 4, 5, 6, 7, 8, 9]按照元素与5距离从小到大进行排序,其结果是[5, 4, 6, 3, 7, 2, 8, 1, 9]。
map
函数
此时lambda函数用于指定对列表中每一个元素的共同操作。
例如map(lambda x: x+1, [1, 2,3])将列表[1, 2, 3]中的元素分别加1,其结果[2, 3, 4]。
-
reduce
函数
此时lambda函数用于指定列表中两两相邻元素的结合条件。
例如reduce(lambda a, b: '{}, {}'.format(a, b), [1, 2, 3, 4, 5, 6, 7, 8, 9])将列表 [1, 2, 3, 4, 5, 6, 7, 8, 9]中的元素从左往右两两以逗号分隔的字符的形式依次结合起来,其结果是'1, 2, 3, 4, 5, 6, 7, 8, 9'。
另外,部分Python
库函数也接收函数作为参数,例如gevent
的spawn
函数。此时,lambda
函数也能够作为参数传入。
参考:
python lambda表达式
python lambda表达式简单用法
5. Python - 递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n
,用函数fact(n)
表示,可以看出:
fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n
所以,fact(n)
可以表示为n x fact(n-1)
,只有n=1
时需要特殊处理。
于是,fact(n)
用递归的方式写出来就是:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
上面就是一个递归函数。可以试试:
>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
如果我们计算fact(5),可以根据函数定义看到计算过程如下:
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)
这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000)
:
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
上面的fact(n)
函数由于return n * fact(n - 1)
引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
可以看到,return fact_iter(num - 1, num * product)
仅返回递归函数本身,num - 1
和num * product
在函数调用前就会被计算,不影响函数调用。
fact(5)
对应的fact_iter(5, 1)
的调用如下:
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python
解释器也没有做优化,所以,即使把上面的fact(n)
函数改成尾递归方式,也会导致栈溢出。
小结
-
使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
-
针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
-
Python
标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。
参考:
python递归函数
网友评论