Skip to content

Latest commit

 

History

History
807 lines (481 loc) · 22.7 KB

单线程同步流程控制.md

File metadata and controls

807 lines (481 loc) · 22.7 KB

单线程同步流程控制

python与多数其他语言一样默认是单线程同步的顺序执行语句.这也是所有语言的基础.

python中语句会一行一行地被解释执行,而流控制语句则是用于控制当前解释执行哪条语句的工具

流程分支

python的流程分支现在有两种:

  • if语句
  • match语句

在python 3.10之前python只有if语句,虽然这种方式看似减少了关键字数量,但这也带来了一定程度上语义上的歧义,我们不得不用if语句写出非常不优雅的代码,个人认为是python设计的一大败笔.好在现在有了match语句.

if语句

在没有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语句正式名为模式匹配.它是用来优雅实现多分支的工具.其通用语法如下:

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

其中subject可以为一个表达式(值),这个值会与以一个或多个case语句块形式给出的一系列模式进行比较从而执行对应的操作. 具体来说模式匹配的操作如下:

  • 针对subject在match语句中求值

  • 从上到下对subjectcase语句中的每个模式进行比较直到确认匹配到一个模式.

  • 执行与被确认匹配的模式相关联的动作.

  • 如果没有确认到一个完全的匹配,则如果提供了使用通配符_的最后一个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

循环中可以使用break跳出循环或者使用continue跳出当次循环

i = 0
while True:
    i += 1
    if i > 10 :
        break
    if i % 2 == 0 :
        continue
    print(i)
1
3
5
7
9

try/except 不仅用于处理错误,还常用于控制流程

在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语句

else子句不仅能在if语句中使用,还能在forwhiletry语句中使用.for/elsewhile/elsetry/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&amp;tpl=mn&amp;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>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使ç\x94¨ç\x99¾åº¦å\x89\x8då¿\x85读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>æ\x84\x8fè§\x81å\x8f\x8dé¦\x88</a>&nbsp;京ICPè¯\x81030173å\x8f·&nbsp; <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,breakcontinue语句导致控制权跳到了复合语句的主块之外,else子句也会被跳过

EAFP风格的python代码

上面说道EAFP风格,需要注意的是这种风格下try/else语句应当被大量有针对性地使用,它应当细粒度的防守单条语句而不是像多数人那样防守一段代码.这会让代码看起来很啰嗦,但相对来说更加安全

上下文管理器和with块

上下文管理器对象存在的目的是管理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模块

contextlib模块中提供了一些类和其他函数,用于快速的构建上下文管理器

  • closing

如果对象提供了close()方法,但没有实现__enter__/__exit__协议,那么可以使用这个函数构建上下文管理器

  • suppress

构建临时忽略指定异常的上下文管理器

  • @contextmanager

这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了

  • ContextDecorator

这是个基类,用于定义基于类的上下文管理器.这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数.

  • ExitStack

这个上下文管理器能进入多个上下文管理器.with块结束时,ExitStack按照后进先出的顺序调用栈中各个上下文管理器的__exit__方法.如果事先不知道with块要进入多少个上下文管理器,可以使用这个类.例如同时打开任意一个文件列表中的所有文件.

使用@contextmanager

@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__方法有如下作用:

  1. 调用生成器函数,保存生成器对象(这里把它称为gen)
  2. 调用next(gen),执行到yield关键字所在的位置.
  3. 返回next(gen)产出的值,以便把产出的值绑定到with/as语句中的目标变量上

with块终止时,__exit__方法会做以下几件事

  1. 检查有没有把异常传给exc_type;如果有,调用gen.throw(exception),在生成器函数定义体中包含yield关键字的那一行抛出异常.
  2. 否则调用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