python与多数其他语言一样默认是单线程同步的顺序执行语句.这也是所有语言的基础.
python中语句会一行一行地被解释执行,而流控制语句则是用于控制当前解释执行哪条语句的工具
python的流程分支现在有两种:
if
语句match
语句
在python 3.10之前python只有if
语句,虽然这种方式看似减少了关键字数量,但这也带来了一定程度上语义上的歧义,我们不得不用if
语句写出非常不优雅的代码,个人认为是python设计的一大败笔.好在现在有了match
语句.
在没有match
语句时if是流程分支的唯一选择,它有两种语义可以满足全部对分支语句的需求,只是写起来可能会不优雅:
-
if/else
代表判断逻辑,一般用于根据一条语句的结果正误来确定下一步的执行内容 -
if/elif/else
代表多分支,一般用于根据一个变量的取值不同来确定下一步执行的内容.
if/elif
来实现多分支并不优雅,需要为一个变量写多次判断语句,因此如果分支逻辑简单,完全可以使用字典+lambda函数
def which_type(x):
return {
int:lambda x:print(x,"is int"),
float:lambda x:print(x,"is float"),
list:lambda x:print(x,"is list")
}.get(type(x),lambda x:print("i dont know"))(x)
which_type(2)
2 is int
match语句正式名为模式匹配
.它是用来优雅实现多分支的工具.其通用语法如下:
match subject:
case <pattern_1>:
<action_1>
case <pattern_2>:
<action_2>
case <pattern_3>:
<action_3>
case _:
<action_wildcard>
其中subject
可以为一个表达式(值),这个值会与以一个或多个case
语句块形式给出的一系列模式进行比较从而执行对应的操作. 具体来说模式匹配的操作如下:
-
针对
subject
在match语句中求值 -
从上到下对
subject
与case
语句中的每个模式进行比较直到确认匹配到一个模式. -
执行与被确认匹配的模式相关联的动作.
-
如果没有确认到一个完全的匹配,则如果提供了使用通配符
_
的最后一个case
语句,则它将被用作已匹配模式. 如果没有确认到一个完全的匹配并且不存在使用通配符_
的case
语句,则整个match代码块不执行任何操作.
match语句之所以在3.10版本才放出来和它的命名有关,它之所以叫模式匹配
而不是分支
是因为它除了可以判断值后进行分支外,更重要的是可以匹配模式
,远比java
,c++
,js
中的switch
语句强大.
最基础的匹配,也就是其他语言中的swtich
语句的用法. 更进一步的case
支持使用|
分隔条件进行或匹配,也支持使用as
来捕获对应匹配的值.
def http_error(status:int|str)->str:
match int(status):
case 400:
return "Bad request"
case 401 | 402 | 403 as x:
return f"Not allowed x"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the internet"
http_error(403)
'Not allowed x'
match可以解包序列类型的subject
,其原理类似x,y,z = X
,而解包后的结果会作为一个tuple与case语句中的pattern
进行比对.
pattern
中允许有变量,解析到的对应为止的值会被赋值到变量中.我们可以在case
的处理语句中使用这些变量.
pattern
中类似解包,可以使用*
代表多个值,也可以使用_
抛弃匹配对象不进行匹配,也可以使用*_
抛弃多个对象不进行匹配
我们同样可以使用as
捕获符合特定匹配的值,同时还可以使用if
增加匹配条件.
def check_point(point:tuple[int,int,int])->str:
match point:
case (0, 0, 0):
return "Origin"
case (x, 0, 0):
return f"X={x}"
case (x, 0|1|2 as y, 0):
return f"X={x} Y={y}"
case (0, y, 0):
return f"Y={y}"
case (0, 0, z):
return f"Z={z}"
case (_,_, 0):
return f"XY"
case (_,0, _):
return f"XZ"
case (0,_, _):
return f"YZ"
case (x, y, z) if x==y:
return f"X,Y={x}, Z={z}"
case (x, y, z):
return f"X={x}, Y={y}, Z={z}"
case (0,*_):
return f"X=0"
case _:
raise ValueError("Not a 3D point")
check_point((0,2))
'X=0'
check_point((0,2,0))
'X=0 Y=2'
check_point((0,2,3))
'YZ'
check_point((1,2,3))
'X=1, Y=2, Z=3'
check_point((2,2,3))
'X,Y=2, Z=3'
match可以用于匹配字典,pattern
部分也是一个字典,其中的字段会用于在subject
搜索捕获.需要注意与匹配序列不同,pattern
部分的字典中没有声明的键会被忽略.我们也可以使用**rest
来匹配那些没有明确声明的键.
def check_map(x:dict[str,int])->str:
match x:
case {"x": 0, "y": l,**rest}:
return f"has fix x 0 y {l} rest {rest}"
case {"x": b, "y": l}:
return f"has x {b} y {l}"
case {"x": b}:
return f"has x {b}"
case _:
raise ValueError("nothing match")
check_map({"x":0})
'has x 0'
check_map({"x":0,"y":1,"z":2})
"has fix x 0 y 1 rest {'z': 2}"
check_map({"x":1,"y":2,"z":3})
'has x 1 y 2'
match可以用于匹配类,pattern部分是希望匹配的类的实例化表达式,其中的参数则是类中的字段,实例化参数会用于在subject对应的对象中搜索捕获.参数的值可以是变量,这样case语句部分就可以使用这个变量了.
class Point:
x: int
y: int
def __repr__(self)->str:
return f"({self.x},{self.y})"
def __init__(self,x:int,y:int)->None:
self.x = x
self.y = y
def location(point:Point)->str:
match point:
case Point(x=0, y=0):
return "Origin is the point's location."
case Point(x=0, y=y):
return f"Y={y} and the point is on the y-axis."
case Point(x=x, y=0):
return f"X={x} and the point is on the x-axis."
case Point():
return "The point is located somewhere else on the plane."
case _:
return "Not a point"
location(Point(x=0,y=1))
'Y=1 and the point is on the y-axis.'
我们也可以组合以上各种来匹配复杂的模式.比如下面是用于匹配多个位置坐标的例子:
def locations(point:list[Point])->str:
match point:
case []:
return "No points in the list."
case [Point(x=0, y=0)]:
return "The origin is the only point in the list."
case [Point(x=x, y=y)]:
return f"A single point {x}, {y} is in the list."
case [Point(x=0, y=y1), Point(x=0, y=y2) as p2]:
return f"Two points on the Y axis at {y1}, {y2} are in the list. p2 is {p2}"
case [Point(x=x1, y=y1), Point(x=x2, y=y2),*points] if all([isinstance(point,Point) for point in points]):
return f"points ({x1},{y1}), ({x2},{y2}) and points {points}."
case _:
return "Something else is found in the list."
locations([])
'No points in the list.'
locations([Point(x=0,y=0)])
'The origin is the only point in the list.'
locations([Point(x=0,y=0),Point(x=0,y=4)])
'Two points on the Y axis at 0, 4 are in the list. p2 is (0,4)'
locations([Point(x=1,y=1),Point(x=2,y=1),Point(x=3,y=1)])
'points (1,1), (2,1) and points [(3,1)].'
locations([Point(x=1,y=1),Point(x=2,y=1),(3,1)])
'Something else is found in the list.'
循环语句主要目的就是让一段代码反复顺序执行,python有两种循环语句:
-
for循环
主要用于根据先验循环次数执行循环内代码的情况.他会遍历一个迭代器,先验的次数就是这个迭代器的长度,每取出一个对象都会执行for块中的语句
-
while循环
主要用于没有先验循环次数,而是根据判断条件执行循环内代码的情况
for i in range(10):
print(i)
0
1
2
3
4
5
6
7
8
9
i = 0
while i<10:
print(i)
i+=1
0
1
2
3
4
5
6
7
8
9
循环中可以使用break跳出循环或者使用continue跳出当次循环
i = 0
while True:
i += 1
if i > 10 :
break
if i % 2 == 0 :
continue
print(i)
1
3
5
7
9
在Python中,try/except
不仅用于处理错误,还常用于控制流程.为此,Python 官方词汇表还定义了一个缩略词(口号):
- EAFP
取得原谅比获得许可容易(easier to ask for forgiveness than permission).这是一种常见的Python编程风格,先假定存在有效的键或属性,如果假定不成立,那么捕获异常.这种风格简单明快,特点是代码中有很多try和except语句.与其他很多语言一样(如C 语言),这种风格的对立面是LBYL风格.
- LBYL
三思而后行(look before you leap).这种编程风格在调用函数或查找属性或键之前显式测试前提条件.与EAFP
风格相反,这种风格的特点是代码中有很多if
语句.在多线程环境中,LBYL
风格可能会在"检查"和"行事"的空当引入条件竞争.例如对if key in mapping: return mapping[key]
这段代码来说,如果在测试之后,但在查找之前,另一个线程从映射中删除了那个键,那么这段代码就会失败.这个问题可以使用锁或者EAFP
风格解决.
如果选择使用EAFP
风格,那就要更深入地了解else
子句,并在try/except
语句中合理使用
else
子句不仅能在if
语句中使用,还能在for
、while
和try
语句中使用.for/else
、while/else
和try/else
的语义关系紧密,不过与if/else
差别很大.
此处的else语句理解为then
可能更加合适,它代表的是"没有遇到特殊情况就执行以下代码"这样的语义.具体到不同的语句,语义如下:
-
for
仅当
for
循环运行完毕时)即for
循环没有被break
语句终止)才运行else
块 -
while
仅当
while
循环因为条件为假值而退出时(即while
循环没有被break
语句中止)才运行else
块
循环语句中使用else语句可以省去大量的状态变量,最典型的就是为避免网络异常而多次访问某个地址的场景
import requests
conn = requests.get("http://www.baidu.com")
def get_html(url):
for i in range(3):
try:
conn = requests.get(url)
except:
print(f"第{i+1}次连不上服务器")
else:
return conn.text
else:
raise ConnectionError("连不上服务器")
get_html("http://www.baidu.com")
'<!DOCTYPE html>\r\n<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>ç\x99¾åº¦ä¸\x80ä¸\x8bï¼\x8cä½\xa0å°±ç\x9f¥é\x81\x93</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=ç\x99¾åº¦ä¸\x80ä¸\x8b class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>æ\x96°é\x97»</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>å\x9c°å\x9b¾</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>è§\x86é¢\x91</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>è´´å\x90§</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>ç\x99»å½\x95</a> </noscript> <script>document.write(\'<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=\'+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ \'" name="tj_login" class="lb">ç\x99»å½\x95</a>\');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">æ\x9b´å¤\x9a产å\x93\x81</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>å\x85³äº\x8eç\x99¾åº¦</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使ç\x94¨ç\x99¾åº¦å\x89\x8då¿\x85读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>æ\x84\x8fè§\x81å\x8f\x8dé¦\x88</a> 京ICPè¯\x81030173å\x8f· <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>\r\n'
get_html("http://www.bidss.ecom")
第1次连不上服务器
第2次连不上服务器
第3次连不上服务器
---------------------------------------------------------------------------
ConnectionError Traceback (most recent call last)
Cell In[29], line 1
----> 1 get_html("http://www.bidss.ecom")
Cell In[27], line 10, in get_html(url)
8 return conn.text
9 else:
---> 10 raise ConnectionError("连不上服务器")
ConnectionError: 连不上服务器
- try
仅当try
块中没有异常抛出时才运行else
块。官方文档还指出--else
子句抛出的异常不会由前面的except
子句处理.
在所有情况下,如果异常或者return
,break
或continue
语句导致控制权跳到了复合语句的主块之外,else
子句也会被跳过
上面说道EAFP风格,需要注意的是这种风格下try/else
语句应当被大量有针对性地使用,它应当细粒度的防守单条语句而不是像多数人那样防守一段代码.这会让代码看起来很啰嗦,但相对来说更加安全
上下文管理器对象存在的目的是管理with
语句,就像迭代器的存在是为了管理for语句一样.
with
语句的目的是简化try/finally
模式.这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return
语句或sys.exit()
调用而中止,也会执行指定的操作.finally
子句中的代码通常用于释放重要的资源,或者还原临时变更的状态.
上下文管理器协议包含__enter__
和__exit__
两个方法.with
语句开始运行时,会在上下文管理器对象上调用__enter__
方法.with
语句运行结束后,会在上下文管理器对象上调用__exit__
方法,以此扮演finally
子句的角色.
最常见的例子是确保关闭文件对象.这在前文已经有所描述.注意,与函数和模块不同,with
块没有定义新的作用域.
-
__enter__()
方法__enter__()
方法要求最好返回一个对象(如果不返回一个对象,as语句会捕获一个None),一般是self
,但不一定.除了返回上下文管理器之外,还可能返回其他对象. -
__exit__(exc_type, exc_value, traceback)
方法- exc_type 异常类(例如ZeroDivisionError)
- exc_value
异常实例.有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用
exc_value.args
获取 - traceback traceback对象
不管控制流程以哪种方式退出with块,都会在上下文管理器对象上调用
__exit__
方法,而不是在__enter__
方法返回的对象上调用.with
语句的as
子句是可选的.对open
函数来说,必须加上as
子句,以便获取文件的引用.不过,有些上下文管理器会返回None
,因为没什么有用的对象能提供给用户.
下面看一个上下文管理器修改上下文环境中print
函数行为的例子:
import sys
class LookingGlass:
def __enter__(self):
self.original_write = sys.stdout.write
sys.stdout.write = self.reverse_write
return 'JABBERWOCKY'
def reverse_write(self, text):
self.original_write(text[::-1])
def __exit__(self, exc_type, exc_value, traceback):
sys.stdout.write = self.original_write
if exc_type is ZeroDivisionError:
print('Please DO NOT divide by zero!')
return True
with LookingGlass() as what:
print('Alice, Kitty and Snowdrop')
print(what)
pordwonS dna yttiK ,ecilA
YKCOWREBBAJ
what
'JABBERWOCKY'
print('Back to normal.')
Back to normal.
抛开了with语句,上下文管理器也可以这样使用
manager = LookingGlass()
monster = manager.__enter__()
print(monster == 'JABBERWOCKY')
print(monster)
manager.__exit__(None, None, None)
print(monster)
eurT
YKCOWREBBAJ
JABBERWOCKY
使用try/finally
可以这样写
manager = LookingGlass()
try:
monster = manager.__enter__()
print(monster == 'JABBERWOCKY')
print(monster)
except:
pass
finally:
manager.__exit__(None, None, None)
print(monster)
eurT
YKCOWREBBAJ
JABBERWOCKY
我们可以在同一个with
段中启动多个上下文管理器,这样他们会在退出时一起回收资源.设想这样一个场景,我们需要将文件a.txt
中的数据转换提取后存入b.csv
,这种情况下我们就需要一个读文件的上下文和一个写文件的上下文,我们当然可以先读后写写两个with段实现,但这样会显得很啰嗦.不妨这样写:
with (open("a.txt") as fr,
open("b.csv","w") as fw):
content = fr.read()
fw.write(transform(content))
contextlib
模块中提供了一些类和其他函数,用于快速的构建上下文管理器
- closing
如果对象提供了close()
方法,但没有实现__enter__/__exit__
协议,那么可以使用这个函数构建上下文管理器
- suppress
构建临时忽略指定异常的上下文管理器
- @contextmanager
这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了
- ContextDecorator
这是个基类,用于定义基于类的上下文管理器.这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数.
- ExitStack
这个上下文管理器能进入多个上下文管理器.with
块结束时,ExitStack
按照后进先出的顺序调用栈中各个上下文管理器的__exit__
方法.如果事先不知道with
块要进入多少个上下文管理器,可以使用这个类.例如同时打开任意一个文件列表中的所有文件.
@contextmanager
装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义__enter__
和__exit__
方法,而只需实现有一个yield
语句的生成器,生成想让__enter__
方法返回的值.
在使用@contextmanager
装饰的生成器中,yield
语句的作用是把函数的定义体分成三部分:
- yield 语句前面的所有代码在with块开始时(即解释器调用
__enter__
方法时)执行 - yield 语句,用于抛出
__enter__
要返回的对象,并可以接收异常 - yield 语句后面的代码在with块结束时(即调用
__exit__
方法时)执行
import contextlib
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
yield 'JABBERWOCKY'
sys.stdout.write = original_write
with looking_glass() as what:
print(what)
print("12345")
print(what)
print("12345")
YKCOWREBBAJ
54321
JABBERWOCKY
12345
其实,contextlib.contextmanager
装饰器会把函数包装成实现__enter__
和__exit__
方法的类.
这个类的__enter__
方法有如下作用:
- 调用生成器函数,保存生成器对象(这里把它称为gen)
- 调用
next(gen)
,执行到yield
关键字所在的位置. - 返回
next(gen)
产出的值,以便把产出的值绑定到with/as
语句中的目标变量上
with
块终止时,__exit__
方法会做以下几件事
- 检查有没有把异常传给
exc_type
;如果有,调用gen.throw(exception)
,在生成器函数定义体中包含yield
关键字的那一行抛出异常. - 否则调用
next(gen)
,继续执行生成器函数定义体中yield语句之后的代码
如果在with
块中抛出了异常,Python解释器会将其捕获,然后在looking_glass
函数的yield
表达式里再次抛出.但是那里没有处理错误的代码,因此looking_glass
函数会中止,永远无法恢复成原来的sys.stdout.write
方法,导致系统处于无效状态.因此上面的例子并不完整,下面给出完整的例子
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
try:
yield 'JABBERWOCKY'
except Exception as e:
msg = 'a error!'
finally:
sys.stdout.write = original_write
if msg:
print(msg)
with looking_glass() as what:
print(what)
print("12345")
raise AssertionError("123")
print(what)
print("12345")
YKCOWREBBAJ
54321
a error!
JABBERWOCKY
12345