高级流量控制
在本章中,我们将讨论Python程序中使用的一些更高级,甚至可能晦涩的流控制技术,您应该在高级水平上熟悉这些技术。
具体来说,我们将介绍:
其他的
循环中的子句其他的
条款试一试
块- 模拟开关语句
- 型式调度
通过理解这些不同寻常的语言特性,您将能够理解您可能遇到的更多代码。您还可以通过消除不必要的条件来降低自己代码的复杂性。
其他的
循环中的子句
你无疑会联想到其他的
关键字用可选子句补充引入的条件子句如果
声明。但是你知道吗其他的
也可以用来关联可选的代码块与循环?这听起来很奇怪,老实说是一种不同寻常的语言特征。这就是为什么它在野外很少见到,而且绝对值得一些解释。
而
..其他的
我们首先说Guido van Rossum, Python的发明者和仁慈的独裁者,已经承认如果他再次开发Python,他将不会包含这个特性。23.回到我们的时间轴,这个特性就在那里,我们需要理解它。
显然,最初使用的动机其他的
关键词用这种方式结合而
loops来自Donald Knuth的早期努力,旨在消除结构化编程语言中的goto。虽然关键字的选择在一开始是令人困惑的,它是有可能合理化的使用其他的
通过这种方式的比较如果其他……
构造:
如果条件:execute_condition_is_true()其他的:execute_condition_is_false()
在如果其他……
结构中的代码如果
子句的值为时执行真正的
当转换为保龄球
换句话说,如果条件是“真实的”。另一方面,如果条件是' falsey ',则其他的
子句被执行。
现在看看而其他. .
构造:
而条件:execute_condition_is_true()其他的:execute_condition_is_false()
我们或许可以窥见选择其他的
关键字。的其他的
子句将在且仅当条件求值为时执行假
.情况可能已经是这样了假
当执行第一次到达而
语句,因此它可以立即分支到其他的
条款。或者可以有任意数量的循环而
在条件变为假
执行转移到其他的
块。
你可能会说,这很公平,但这不等同于把其他的
代码在循环之后,而不是在特殊情况下其他的
像这样?:
而条件:execute_condition_is_true()execute_condition_is_false()
在这个简单的例子中你是对的。但是如果我们放置a打破
语句,则可以在循环体中退出循环,而循环条件永远不会变为false。在这种情况下execute_condition_is_false ()
调用发生在条件下不是假
:
而条件:国旗=execute_condition_is_true()如果国旗:打破execute_condition_is_false()
为了解决这个问题,我们可以用一个如果
语句提供所需的行为:
而条件:国旗=execute_condition_is_true()如果国旗:打破如果不条件:execute_condition_is_false()
这种方法的缺点是测试是重复的,这违反了Don 't Repeat Yourself -或DRY -准则,从而妨碍了可维护性。
的而其他. .
Python中的construct允许我们避免第二次冗余测试:
而条件:国旗=execute_condition_is_true()如果国旗:打破其他的:execute_condition_is_false()
现在,其他的
块只有当主循环条件计算为时执行假
.如果我们以另一种方式跳出循环,比如打破
语句时,执行跳过其他的
条款。毫无疑问,无论你如何合理化,这里的关键字的选择是令人困惑的,如果一个更好的整体nobreak
关键字已用于介绍此块。代替这样的关键字,我们衷心建议,如果你是想要使用这种晦涩且很少使用的语言特性,可以包含这样的nobreak
评论如下:
而条件:国旗=execute_condition_is_true()如果国旗:打破其他的:# nobreakexecute_condition_is_false()
理论就讲到这里;这在实践中有用吗?
我们必须承认,这本书的两位作者都没有使用过而
..其他的
在实践中。我们所看到的几乎每个示例都可以通过另一个更容易理解的构造更好地实现,稍后我们将讨论这个构造。也就是说,让我们看一个例子evaluator.py
:
defis_comment(项):返回isinstance(项,str)而且项.startswith(“#”)def执行(程序):"""执行堆栈程序。参数:program:任何类堆栈的集合,其中堆栈中的每个项可调用操作符或不可调用操作数。最顶层堆栈上的项可能是以'#'开头的字符串文档的目的。Stack-like表示支持:item = stack.pop() #删除并返回顶部的项目stack.append(item) #将一个项目推到顶部如果stack为空,则在布尔上下文中为# False”“”#通过跳过找到'程序'的开始#注释的任何项。而程序:项=程序.流行()如果不is_comment(项):程序.附加(项)打破其他的:# nobreak打印(“空程序!”)返回#评估项目等待=[]而程序:项=程序.流行()如果可调用的(项):试一试:结果=项(*等待)除了异常作为e:打印(错误:“,e)打破程序.附加(结果)等待.清晰的()其他的:等待.附加(项)其他的:# nobreak打印(“项目成功。”)打印(结果:",等待)打印(“完成”)如果__name__= =“__main__ ':进口操作符程序=列表(逆转((#要添加的简短堆栈程序,"#和一些常数相乘",9,13,操作符.mul,2,操作符.添加)))执行(程序)
此代码计算简单的“堆栈程序”。这样的程序被指定为一堆项,其中每个项要么是一个可调用函数(对于这些我们使用任何常规的Python函数),要么是该函数的一个参数。为了求值5 + 2
,我们将像这样建立堆栈:
52+
当加号运算符被求值时,其结果被压入堆栈。这允许我们执行更复杂的操作,例如(5 + 2) * 3
:
52+3.*
由于堆栈中包含了反向波兰表示法的表达式,因此我们在中缀版本中需要的括号是不需要的。实际上,堆栈将是一个Python列表。操作符将可从Python标准库调用运营商
模块,它提供了每个Python中缀操作符的命名函数等价物。最后,当我们使用Python列表作为堆栈时,堆栈的顶部位于列表的末尾,因此为了以正确的顺序获取所有内容,我们需要反转列表:
程序=列表(逆转([5,2,操作符.添加,3.,操作符.mul]))
为了增加兴趣,我们的小堆栈语言还支持注释作为以哈希符号开头的字符串,就像Python一样。然而,这样的注释只允许在程序的开头,也就是堆栈的顶部:
进口操作符程序=列表(逆转((#要添加的简短堆栈程序,"#和一些常数相乘",5,2,操作符.添加,3.,操作符.mul)))
我们希望通过将堆栈程序传递给函数来运行它execute ()
,像这样:
执行(程序)
让我们看看这样的函数是什么样的,以及它如何使用而其他. .
施工效果好。第一件事是execute ()
函数需要做的就是从堆栈顶部弹出所有的注释字符串并丢弃它们。为了帮助实现这一点,我们将定义一个简单的谓词,将堆栈项标识为注释:
defis_comment(项):返回isinstance(项,str)而且项.startswith(“#”)
注意,这个函数依赖于一个重要的Python特性布尔短路.如果
项
不是字符串那么调用startswith ()
提出了一个AttributeError
.然而,当计算布尔运算符时而且
而且或
Python只会在计算结果时计算第二个操作数。当项
是不作为字符串(意味着第一个操作数的计算结果为假
),则布尔值的结果而且
也必须是假
;在这种情况下,不需要计算第二个操作数。
给定这个有用的谓词,我们现在将使用while循环从堆栈顶部清除注释项:
而程序:项=程序.流行()如果不is_comment(项):程序.附加(项)打破其他的:# nobreak打印(“空程序!”)返回
的条件表达式而
语句是程序
堆栈对象本身。记住,在这样的布尔上下文中使用集合的结果为真正的
如果集合非空或假
如果是空的。或者换句话说,空集合是“虚假的”。所以这句话读作“当程序中还有剩余项目时”。
while循环有一个关联的其他的
子句,其中执行将跳转而
条件应计算为假
.当程序中没有剩余的项时,就会发生这种情况。在此子句中,我们打印一个警告,提示程序在逻辑上为空,然后从execute ()
函数。
在而
块,我们pop ()
从堆栈中取出一项——还记得常规的Python列表有这个方法,它从列表中删除并返回最后一项。我们使用our的逻辑否定is_comment ()
谓词来确定刚刚弹出的项是否为不一个评论。如果循环已到达非注释项,则使用调用将其推回堆栈append ()
,这将使堆栈的第一个非注释项位于顶部,然后打破
从循环。记住while循环其他的
子句最好被认为是“no break”子句,因此当我们从循环中中断时,执行将跳过其他的
阻塞并继续执行后面的第一个语句。
此循环执行其他的
在搜索失败的情况下阻塞-在这个例子中,如果我们无法找到第一个非注释项,因为没有一个。搜索失败处理可能是循环最广泛的应用其他的
条款。
现在我们知道堆栈上的所有剩余项组成了实际的程序。我们将使用另一个while循环来求值:
等待=[]而程序:项=程序.流行()如果可调用的(项):试一试:结果=项(*等待)除了异常作为e:打印(错误:“,e)打破程序.附加(结果)等待.清晰的()其他的:等待.附加(项)其他的:# nobreak打印(“项目成功。”)打印(结果:",等待)
在此循环之前,我们设置了一个名为等待
.这将用于在堆栈中积累函数的参数,我们将很快看到。
和前面一样,while循环的条件是程序
堆栈本身,因此当程序堆栈为空时,此循环将完成,控制将转移到while-loop else-子句。这将在程序执行完成时发生。
在while循环中,我们从堆栈中弹出顶部项,并用内置的检查它可调用的()
谓词来决定它是否为函数。为了清楚起见,我们将看一下其他的
条款。这是其他的
与如果
,而不是其他的
与而
!
如果弹出项为不可调用对象,我们将它附加到等待
列表,并再次循环,如果程序
还不是空的。
如果项目是Callable,我们尝试调用它,使用星型参数扩展调用语法将任何挂起的参数传递给函数。如果函数调用失败,我们捕获异常,打印错误消息,然后打破
来自while循环。记住这将绕过循环其他的
条款。如果函数调用成功,我们将返回值赋给结果
,将该值推回程序堆栈,并清除挂起的参数列表。
当程序堆栈为空时其他的
输入与while循环关联的块。对象的任何内容后将打印“程序成功”等待
列表。通过这种方式,程序可以在堆栈底部留下不可调用的值来“返回”结果;这些会被扫进等待
列表,并显示在末尾。
否则循环
现在我们明白了而其他. .
我们可以看看类似的结构其他. .
构造。的其他. .
Construct可能看起来更奇怪而其他. .
的显式条件的缺失为
声明,但是你需要记住其他的
条款实际上是不中断条款。在for循环的情况下,这正是它被调用的时候——当循环不中断地退出时。这包括循环迭代的可迭代序列为空的情况。4
为项在可迭代的:如果匹配(项):结果=项打破其他的:# nobreak#没有匹配到结果=没有一个#经常来这里打印(结果)
典型的使用模式是这样的:我们使用for循环检查可迭代系列中的每一项,并测试每一项。如果项目匹配,我们打破
从循环。的代码中,如果没有找到匹配的代码其他的
Block被执行,它处理“未找到匹配”的情况。
例如,下面是一个代码片段,它确保整数列表中至少包含一个能被指定值整除的整数。如果提供的列表不包含除数的倍数,则除数本身被追加到列表中以建立不变量:
项目=[2,25,9,37,28,14]除数=12为项在项目:如果项%除数= =0:发现=项打破其他的:# nobreak项目.附加(除数)发现=除数打印("{items}包含{found},它是{除数}的倍数".格式(**当地人()))
我们设置了一个数字项列表和一个除数,在本例中为12。for循环遍历这些项,依次测试每一项是否能被除数整除。如果是除数的倍数,则变量发现
设置为当前项,我们从循环中中断-跳过循环-其他的
子句-并打印项目列表。如果for循环结束而没有遇到12的倍数,则将输入loop-else子句,它将除数本身追加到列表中,从而确保列表包含能被除数整除的项。
For-else分句比while-else分句更常见,尽管我们必须强调这两种分句都不常见,而且都被广泛误解。所以尽管我们想你为了理解它们,我们不建议使用它们,除非您确定需要阅读您的代码的每个人都熟悉它们的用法。
在PyCon 2011上进行的一项调查中,大多数受访者无法正确理解使用循环else子句的代码。小心行事!
循环的替代方案其他的
条款
已经指出了这个循环其他的
子句是最好避免的,公平起见,我们为您提供了另一种技术,除了避免晦涩的Python构造之外,我们认为这种技术更好,原因有几个。
几乎每次你看到循环的时候其他的
子句可以通过将循环提取到命名函数中来重构它,而不是打破
-ing,则更倾向于直接从函数返回。代码的搜索失败部分,在其他的
子句,然后可以降低一级并放置在循环体之后。这样做,我们的新ensure_has_divisible ()
函数看起来是这样的:
defensure_has_divisible(项目,除数):为项在项目:如果项%除数= =0:返回项项目.附加(除数)返回除数
这非常简单,任何Python程序员都能理解。我们可以这样使用它:
项目=[2,25,9,37,24,28,14]除数=12股息=ensure_has_divisible(项目,除数)打印("{items}包含{除数}的倍数{被除数}".格式(**当地人()))
这更容易理解,因为它不使用任何晦涩和高级的Python流控制技术。它更容易测试,因为它被提取到一个独立的函数中。它是可重用的,因为它没有与其他代码混合在一起,我们可以给它一个有用且有意义的名称,而不必在代码中添加注释来解释块。
try…除了…其他的构造
我们可以使用else块的第三个有点奇怪的地方是作为尝试。除了
异常处理结构。在这种情况下,其他的
子句执行,如果没有引发异常:
试一试:这段代码可能引发异常do_something()除了ValueError:捕获和处理# ValueErrorhandle_value_error()其他的:没有提出任何例外我们知道do_something()成功了,所以do_something_else()
看到这个,你可能想知道我们为什么不打电话do_something_else ()
在电话之后do_something ()
,像这样:
试一试:这段代码可能引发异常do_something()do_something_else()除了ValueError:捕获和处理# ValueErrorhandle_value_error()
这种方法的缺点是,我们现在没有办法告诉除了
阻止它是否do_something ()
或do_something_else ()
这引发了异常。try块的扩大范围也模糊了我们捕获异常的意图;我们期望异常来自哪里?
虽然在野外很少见到,但它很有用,特别是当你有一系列可能引发相同异常类型的操作,但你只想处理第一个异常时,比如操作,在处理文件时通常会发生:
试一试:f=开放(文件名,“r”)除了OSError:# OSError从Python 3.3开始替换IOError打印("文件无法打开以读取")其他的:#现在我们确定文件是打开的打印(“行数”,总和(1为行在f))f.关闭()
在本例中,打开文件和遍历文件都可能引发OSError
,但我们只对处理调用中的异常感兴趣open ()
.
有可能两者都是其他的
条款及最后
条款。的其他的
块只在没有异常的情况下执行,而最后
条款将总是被执行。
模拟开关
大多数命令式编程语言都包含switch或case语句,用于根据表达式的值实现多路分支。下面是一个C编程语言的例子,根据a的值调用不同的函数menu_option
变量。对于' no such option '的情况也有处理:
开关(menu_option){情况下1:single_player();打破;情况下2:multi_player();打破;情况下3.:load_game();打破;情况下4:save_game();打破;情况下5:reset_high_score();打破;默认的:printf(“没有这样的选择!”);打破;}
虽然switch可以在Python中通过一系列的如果其他…elif…
块,这写起来很乏味,而且很容易出错,因为条件必须重复多次。
Python中的另一种方法是使用可调用对象的映射。根据您想要实现的目标,这些可调用对象可以是lambdas或命名函数。
我们来看看一个你无法获胜的简单冒险游戏kafka.py
,我们将对其进行重构如果其他…elif…
使用可调用对象的字典。在这个过程中,我们还会用到尝试其他. .
:
"""卡夫卡——你赢不了的冒险游戏。"""def玩():位置=(0,0)活着=真正的而位置:如果位置= =(0,0):打印(“你在一个曲折的迷宫里,都是一样的。”)elif位置= =(1,0):打印(“你在黑暗森林里的一条路上。在北面你可以看到一座塔。”)elif位置= =(1,1):打印(“这里有一座高塔,没有明显的门。有条路通向东方。”)其他的:打印(“这里什么都没有。”)命令=输入()我,j=位置如果命令= =“N”:位置=(我,j+1)elif命令= =“E”:位置=(我+1,j)elif命令= =“S”:位置=(我,j-1)elif命令= =“W”:位置=(我-1,j)elif命令= =“L”:通过elif命令= =“Q”:位置=没有一个其他的:打印(“我不明白”)打印(“游戏结束”)如果__name__= =“__main__ ':玩()
游戏循环使用两个如果其他…elif…
链。第一个打印依赖于玩家当前位置的信息。然后,在接受用户的命令后,第二个如果其他…elif…
Chain根据命令执行操作。
让我们重构这段代码以避免那些冗长的代码如果其他…elif…
链,两者都以重复比较相同变量与不同值为特征。
第一条链描述了我们当前的位置。幸运的是,在python3中,尽管python2中没有,print ()
是函数,因此可以在表达式中使用。我们将利用它来构建位置到可调用对象的映射位置
:
位置={(0,0):λ:打印(“你在一个曲折的迷宫里,都是一样的。”),(1,0):λ:打印(“你在一条路上在黑暗的森林.在北面你可以看到一个塔."),(1,1):λ:打印(“有是这里有一座高塔,与没有明显的门.一条路通向……东.")}
我们会用我们在位置
作为键,并在try块中调用结果的可调用对象:
试一试:位置[位置]()除了KeyError:打印(“这里什么都没有。”)
事实上,我们并不是真的想要捕捉KeyError
从可调用的方法来缩小try块的作用域,因此这也给了我们机会使用尝试其他. .
我们之前学过的结构。以下是改进后的代码:
试一试:location_action=位置[位置]除了KeyError:打印(“这里什么都没有。”)其他的:location_action()
我们将查找和调用分离到单独的语句中,并将调用移动到其他的
块。
类似地,我们可以重构如果其他…elif…
链,用于处理用户输入到可调用对象的字典查找。不过,这一次,我们使用了命名函数而不是lambdas,以避免lambdas只能包含表达式而不能包含语句的限制。这是分支结构:
行动={“N”:go_north,“E”:go_east,“年代”:go_south,' W ':go_west,“L”:看,“问”:辞职,}试一试:command_action=行动[命令]除了KeyError:打印(“我不明白”)其他的:位置=command_action(位置)
我们再次将命令操作的查找从调用分离到命令操作。
下面是字典值中引用的5个可调用对象:
defgo_north(位置):我,j=位置new_position=(我,j+1)返回new_positiondefgo_east(位置):我,j=位置new_position=(我+1,j)返回new_positiondefgo_south(位置):我,j=位置new_position=(我,j-1)返回new_positiondefgo_west(位置):我,j=位置new_position=(我-1,j)返回new_positiondef看(位置):返回位置def辞职(位置):返回没有一个
注意,使用这种技术会迫使我们采用更函数式的编程风格。代码不仅被分解成更多的函数,而且这些函数体不能修改的状态位置
变量。相反,我们显式地传入这个值并返回新值。在新版本中,这个变量的突变只发生在一个地方,而不是五个。
尽管新版本总体上更大,但我们认为它更易于维护。例如,如果要添加一个新的游戏状态,如玩家库存,则所有命令操作都需要接受并返回此值。这使得忘记更新状态比在链式中要困难得多如果其他…elif…
块。
让我们添加一个新的“兔子洞”位置,当用户无意中进入这个位置时,就会回到游戏的起始位置。为了做出这样的改变,我们需要改变所有我们的可调用对象在位置映射中接受并返回一个位置和一个活动状态。虽然这看起来很麻烦,但我们认为这是一件好事。任何维护特定位置代码的人现在都可以看到需要维护的状态。以下是定位函数:
def迷宫(位置,活着):打印(“你在一个曲折的迷宫里,都是一样的。”)返回位置,活着defdark_forest_road(位置,活着):打印(“你在黑暗森林里的一条路上。在北面你可以看到一座塔。”)返回位置,活着deftall_tower(位置,活着):打印(“这里有一座高塔,没有明显的门。有条路通向东方。”)返回位置,活着defrabbit_hole(位置,活着):打印(“你掉进了兔子洞,掉进了迷宫。”)返回(0,0),活着
while循环中相应的开关现在看起来像这样:
位置={(0,0):迷宫,(1,0):dark_forest_road,(1,1):tall_tower,(2,1):rabbit_hole,}试一试:location_action=位置[位置]除了KeyError:打印(“这里什么都没有。”)其他的:位置,活着=location_action(位置,活着)
还必须将调用更新为location_action ()
传递当前状态并接收修改后的状态。
现在让我们通过添加一个致命的熔岩坑位置来让游戏更病态一点假
为活着
的地位。下面是熔岩坑位置的函数:
deflava_pit(位置,活着):打印(“你掉进了一个熔岩坑。”)返回位置,假
我们必须记住将这个添加到位置字典中:
位置={(0,0):迷宫,(1,0):dark_forest_road,(1,1):tall_tower,(2,1):rabbit_hole,(1,2):lava_pit,}
我们还将在访问该位置后添加一个额外的条件块来处理致命情况:
如果不活着:打印(“你死了!”)打破
当我们死的时候,我们从而
循环是主要的游戏循环。这给了我们一个使用而其他. .
子句来处理非致命的游戏循环退出,例如选择退出游戏。像这样退出设置位置
变量来没有一个
,即“falsey”:
而位置:#……其他的:# nobreak打印(“你已经选择了退出比赛。”)
现在我们故意退出,设置位置
来没有一个
并导致while循环终止,我们看到来自其他的
与while循环关联的块:
你是在曲折通道的迷宫,所有都.E你在路上在黑暗的森林.在北面你可以看到一座塔.N在那里是这里有一座高塔,与没有明显的门.一条路通向东方.问你已经选择离开比赛了.游戏结束
但当我们掉入熔岩而死时活着
设置为假
.这将导致执行中断循环,但我们没有看到“您已选择离开”消息其他的
Block被跳过:
你是在曲折通道的迷宫,所有都.E你在路上在黑暗的森林.在北面你可以看到一座塔.N在那里是这里有一座高塔,与没有明显的门.一条路通向东方.N你掉进了一个熔岩坑.你死了!游戏结束
型式调度
按类型“分派”意味着要执行的代码在某种程度上取决于一个或多个对象的类型。每当我们调用对象上的方法时,Python都会根据类型分派;类的类型可能会在不同的类中有该方法的多个实现,选择哪个实现取决于类的类型自我
对象。
通常,我们不能对常规函数使用这种多态。一种解决方案是使用开关模拟将调用路由到适当的实现类型
对象作为字典键。这很笨拙,而且要使它既尊重继承关系又尊重确切的类型匹配是很棘手的。
singledispatch
的singledispatch
我们将在本节中介绍的Decorator为这个问题提供了更优雅的解决方案。
考虑下面的代码,它实现了一个简单的形状继承层次结构,特别是一个圆、一个平行四边形和一个三角形,所有这些都继承自一个名为形状
:
类形状:def__init__(自我,固体):自我.固体=固体类圆(形状):def__init__(自我,中心,半径,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.中心=中心自我.半径=半径def画(自我):打印(“\ u25CF”如果自我.固体其他的“\ u25A1”)类平行四边形(形状):def__init__(自我,巴勒斯坦权力机构,pb,个人电脑,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.巴勒斯坦权力机构=巴勒斯坦权力机构自我.pb=pb自我.个人电脑=个人电脑def画(自我):打印(“\ u25B0”如果自我.固体其他的“\ u25B1”)类三角形(形状):def__init__(自我,巴勒斯坦权力机构,pb,个人电脑,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.巴勒斯坦权力机构=巴勒斯坦权力机构自我.pb=pb自我.个人电脑=个人电脑def画(自我):打印(“\ u25B2”如果自我.固体其他的“\ u25B3”)def主要():形状=[圆(中心=(0,0),半径=5,固体=假),平行四边形(巴勒斯坦权力机构=(0,0),pb=(2,0),个人电脑=(1,1),固体=假),三角形(巴勒斯坦权力机构=(0,0),pb=(1,2),个人电脑=(2,0),固体=真正的)]为形状在形状:形状.画()如果__name__= =“__main__ ':主要()
每个类都有一个初始化式和画()
方法。初始化式存储了该形状类型特有的任何几何信息。他们把任何进一步的争论提交给形状
基类,它存储一个标志,指示形状是否为固体
.
当我们说:
形状.画()
在main ()
,特别的画()
是否被调用取决于是否形状
的实例。圆
,平行四边形
,或三角形
.在接收端,所引用的对象形状
由该方法的第一个形式参数引用,我们知道该方法按惯例被称为自我
.因此,我们说调用被“分派”到方法,这取决于第一个参数的类型。
这很好,也是许多面向对象软件的构造方式,但这可能导致糟糕的类设计,因为它违反了单一责任原则。绘图并不是形状固有的行为,更不用说针对特定类型的设备进行绘图。换句话说,形状类应该是关于形状的,而不是关于你可以用形状做的事情,比如绘图、序列化或剪切。
我们想做的是把不是形状固有的责任移出形状类。在我们的例子中,我们的形状不做任何其他事情,所以它们成为没有行为的数据容器,就像这样:
类圆(形状):def__init__(自我,中心,半径,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.中心=中心自我.半径=半径类平行四边形(形状):def__init__(自我,巴勒斯坦权力机构,pb,个人电脑,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.巴勒斯坦权力机构=巴勒斯坦权力机构自我.pb=pb自我.个人电脑=个人电脑类三角形(形状):def__init__(自我,巴勒斯坦权力机构,pb,个人电脑,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.巴勒斯坦权力机构=巴勒斯坦权力机构自我.pb=pb自我.个人电脑=个人电脑
删除了绘图代码后,有几种方法可以在类之外实现绘图职责。我们可以做一串如果其他…elif…
测试使用isinstance ()
:
def画(形状):如果isinstance(形状,圆):draw_circle(形状)elifisinstance(形状,平行四边形):draw_parallelogram(形状)elifisinstance(形状,三角形):draw_triangle(形状)其他的:提高TypeError(“不会画形状”)
在这个版本中画()
我们测试形状
使用最多三次调用isinstance ()
反对圆
,平行四边形
而且三角形
.如果形状
对象不匹配这些类,我们抛出TypeError
.这很难维护,并且被认为是非常糟糕的编程风格。
另一种方法是使用字典查找来模拟开关,其中字典键是类型,字典值是绘图的函数:
def画(形状):抽屉={圆:draw_circle,平行四边形:draw_parallelogram,三角形:draw_triangle,}试一试:抽屉里=抽屉(类型(形状))除了KeyError作为e:提高TypeError(“不会画形状”)从e其他的:抽屉里(形状)
这里我们通过获取类型查找抽屉函数形状
在try块中,转换KeyError
到一个TypeError
如果查找失败。如果没有例外,则调用具有图形的抽屉其他的
条款。
这个看起来更好,但实际上更脆弱,因为我们正在做确切的在进行键查找时进行类型比较。这意味着一个子类,比如说,圆
也不会给他打电话draw_circle ()
.
这些问题的解决方案在Python 3.4中以singledispatch
,一个在Python标准库中定义的装饰器functools
模块,它对类型执行分派。在早期版本的Python(包括Python 2)中,您可以安装singledispatch
包从Python包索引。
支持依赖于参数类型的多种实现的函数称为“泛型函数”,泛型函数的每个版本都被称为函数的“重载”。为不同的参数类型提供另一个版本的泛型函数的行为被调用重载这个函数。这些术语在静态类型语言(如c#、c++或Java)中很常见,但很少在Python上下文中听到。
使用singledispatch
函数定义了一个函数singledispatch
装饰。具体来说,我们定义了一个特定版本的函数,如果没有提供更特定的重载,将调用该函数。稍后我们将讨论特定于类型的重载。
在我们需要导入的文件的顶部singledispatch
:
从functools进口singledispatch
再往下,我们实现泛型画()
功能:
@singledispatchdef画(形状):提高TypeError(“不知道怎么画画{!r}”.格式(形状))
在这种情况下,泛型函数会引发aTypeError
.
回想一下,装饰器包装它们应用到的函数,并将结果包装器绑定到原始函数的名称。因此,在本例中,装饰器返回的包装器绑定到名称画
.的画
包装器有一个名为注册
这是也一个装饰;注册()
可用于提供在不同类型上工作的原始函数的额外版本。这是函数重载。
由于我们的重载都将与原始函数的名称相关联,画
,我们如何称呼重载本身并不重要。按照惯例我们叫他们_
,尽管这不是必需的。这里有一个超载圆
,另一个为平行四边形
三分之一三角形
:
@draw.注册(圆):def_(形状):打印(“\ u25CF”如果形状.固体其他的“\ u25A1”)@draw.注册(平行四边形)def_(形状):打印(“\ u25B0”如果形状.固体其他的“\ u25B1”)@draw.注册(三角形)def_(形状):#画一个三角形打印(“\ u25B2”如果形状.固体其他的“\ u25B3”)
通过这样做,我们已经清晰地分离了关注点。现在绘画依赖于形状,而不是形状依赖于绘画。
我们的主函数,现在看起来像这样,调用全局作用域的泛型绘制函数,和singledispatch
如果存在重载,机器将选择最特定的重载,或者退回到默认实现:
def主要():形状=[圆(中心=(0,0),半径=5,固体=假),平行四边形(巴勒斯坦权力机构=(0,0),pb=(2,0),个人电脑=(1,1),固体=假),三角形(巴勒斯坦权力机构=(0,0),pb=(1,2),个人电脑=(2,0),固体=真正的)]为形状在形状:画(形状)
我们可以添加其他功能形状
以类似的方式,通过定义相对于形状类型具有多态行为的其他泛型函数。
使用带有方法的singledispatch
你要小心不要用singledispatch
带有方法的装饰器。要了解原因,请考虑实现泛型的尝试相交()
谓词方法圆
类,可用于确定一个特定的圆是否与三个已定义形状中的任何一个实例相交:
类圆(形状):def__init__(自我,中心,半径,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.中心=中心自我.半径=半径@singledispatchdef相交(自我,形状):提高TypeError(“不知道如何计算与{的交集!”r}”.格式(形状))@intersects.注册(圆)def_(自我,形状):返回circle_intersects_circle(自我,形状)@intersects.注册(平行四边形)def_(自我,形状):返回circle_intersects_parallelogram(自我,形状)@intersects.注册(三角形)def_(自我,形状):返回circle_intersects_triangle(自我,形状)
乍一看,这似乎是一种合理的方法,但这里存在几个问题。
类所定义的类的类型不能被注册相交
泛型函数,因为我们还没有定义完它。
第二个问题更为根本:回想一下singledispatch
类型的类型进行调度第一个论点:
do_intersect=my_circle.相交(my_parallelogram)
当我们像这样调用新方法时很容易忘记my_parallelogram
实际上是第二个参数Circle.intersects
.my_circle
是第一个参数,它被绑定到自我
参数。因为自我
将总是是一个圆
在这种情况下相交()
不管第二个参数的类型是什么,调用将始终分派给第一个重载。
的使用singledispatch
与方法。然而,并非一切都失去了。解决方案是将泛型函数移出类,并从交换参数的常规方法调用它。让我们一起来看看:
类圆(形状):def__init__(自我,中心,半径,*arg游戏,**kwargs):超级().__init__(*arg游戏,**kwargs)自我.中心=中心自我.半径=半径def相交(自我,形状):#委托给泛型函数,交换参数返回intersects_with_circle(形状,自我)@singledispatchdefintersects_with_circle(形状,圆):提高TypeError(“不知道如何计算{的交集!”R}与{!r}”.格式(圆,形状))@intersects_with_circle.注册(圆)def_(形状,圆):返回circle_intersects_circle(圆,形状)@intersects.注册(平行四边形)def_(形状,圆):返回circle_intersects_parallelogram(圆,形状)@intersects.注册(三角形)def_(形状,圆):返回circle_intersects_triangle(圆,形状)
我们移动泛型函数相交()
到全局作用域,并将其重命名为intersects_with_circle ()
.更换相交()
的方法圆
,它接受正式的论点自我
而且形状
,现在委托给intersect_with_circle ()
将实际实参交换为形状
而且自我
.
为了完成这个例子,我们需要实现另外两个泛型函数,intersects_with_parallelogram ()
而且intersects_with_triangle ()
,不过我把它留作练习。
以这种方式结合基于类的多态性和基于重载的多态性将为我们提供一个完整的实现,而不仅仅是单分派双-dispatch,允许我们做:
形状.相交(other_shape)
这样,函数将基于两者的类型进行选择形状
而且other_shape
,而形状类本身彼此之间没有任何知识,从而保持系统中的耦合是可管理的。
总结
这就是关于Python 3中高级流控制的本章的全部内容。让我们总结一下今天的内容:
- 我们看了
其他的
while-loops中的子句,与更为人所熟知的between进行类比如果
而且其他的
.我们展示了其他的
Block仅在while-loop条件计算为时执行假
.如果通过另一种方式退出循环,例如via打破
或返回
,其他的
子句不执行。因此,其他的
while-循环上的子句只有在循环包含打破
语句。 - loop-else子句是一个有点晦涩且很少使用的构造。我们强烈建议评论
其他的
关键字带有“nobreak”注释,以便清楚地表明在什么条件下执行块。 - 相关的
其他. .
子句以同样的方式工作其他的
子句实际上是“nobreak”子句,只有在循环包含break语句时才有用。它们在for循环搜索中最有用。当迭代时发现一个项时,我们打破
从循环-跳过其他的
条款。如果没有找到任何项,并且循环“自然”完成而没有中断,则其他的
子句执行,并且可以实现处理“未找到”条件的代码。 - 循环的很多用法
其他的
子句——特别是用于搜索的子句——可以通过将循环提取到它自己的函数中来更好地处理。从这里开始,当找到项时直接返回执行,循环之后的代码可以处理“未找到”情况。这比使用循环更清晰、更模块化、更可重用、更可测试其他的
子句中较长的函数。 - 接下来我们看了
尝试其他…只是…
构造。在这种情况下,其他的
子句仅在try块成功完成且未引发任何异常时才执行。这允许的程度试一试
块将被缩小,使它更清楚地从我们期望的异常引发。 - Python缺乏“开关”或“case”结构来实现多分支控制流。我们展示了其他的选择,包括链式的
如果其他…elif…
块和可调用对象的字典。后一种方法还迫使您对每个分支所需要和产生的内容更加明确和一致,因为您必须传递参数和返回值,而不是在每个分支中改变局部状态。 - 方法实现基于类型分派的泛型函数
singledispatch
装饰器可从Python 3.4获得。该装饰器只能应用于模块作用域函数,而不能应用于方法,但通过实现转发方法和参数交换,我们可以将方法委托给泛型函数。这为我们提供了一种实现双重分派调用的方法。 - 顺便说一下,我们看到Python逻辑运算符使用短路求值。这意味着操作符只计算找到结果所需的操作数。这可以用来“保护”表达式在运行时情况下没有意义。