当前位置: 首页 > 后端技术 > Python

在Python中编写条件分支代码的技巧

时间:2023-03-26 13:35:33 Python

序言编写条件分支代码是编码过程中不可或缺的一部分。如果用马路来比喻,现实世界中的代码从来都不是笔直的高速公路,而更像是一张由无数个岔路口组成的市区地图。我们程序员就像司机一样,我们需要告诉我们的程序在下一个路口是左转还是右转。编写良好的条件分支代码非常重要,因为糟糕、复杂的分支处理很容易混淆并降低代码质量。因此,本文将重点介绍使用Python编写分支代码时应该注意的事项。Python中的分支代码Python支持最常见的if/else条件分支语句,但它缺少其他编程语言中常见的switch/case语句。此外,Python还为for/while循环和try/except语句提供了else分支,可以在一些特殊场景下使用。接下来,我将从最佳实践、常用技巧、常见陷阱三个方面来谈谈如何写出优秀的条件分支代码。最佳实践1.避免多级分支嵌套。如果这篇文章只能缩减为一句话,那么那句话一定是“千方百计避免分支嵌套”。过多的分支嵌套是许多新程序员最常犯的错误之一。如果新手JavaScript程序员写了很多层分支嵌套,那么你可能会看到一层又一层的花括号:if{if{if{...}}}。俗称“嵌套If语句地狱”。但是由于Python使用缩进而不是{},深度嵌套的分支可能会比其他语言产生更严重的后果。例如,过多的缩进级别很容易使代码超过PEP8规定的每行字数限制。我们看这段代码:defbuy_fruit(nerd,store):"""去水果店买苹果——首先检查商店是否开门——如果有苹果,就买1个——如果钱不够,那么回家再拿钱"""ifstore.is_open():ifstore.has_stocks("apple"):ifnerd.can_afford(store.price("apple",amount=1)):nerd.buy(商店,“苹果”,金额=1)返回其他:nerd.go_home_and_get_money()returnbuy_fruit(nerd,store)else:rivemadatnofruit(“noappleinstore!”)else:revionmadatnofruit(“商店关闭!”)上面这段代码最大的问题是直接翻译了原来的条件分支需求,导致短短十几行代码就出现了三层嵌套分支。这样的代码可读性和可维护性都很差。但是我们可以使用一个非常简单的技巧:“早期结束”来优化这段代码:defbuy_fruit(nerd,store):ifnotstore.is_open():raiseMadAtNoFruit("storeisclosed!")ifnotstore.has_stocks("apple"):raiseMadAtNoFruit("noappleinthestore!")ifnerd.can_afford(store.price("apple",amount=1)):nerd.buy(store,"apple",amount=1)returnelse:nerd.go_home_and_get_money()returnbuy_fruit(nerd,store)“提前结束”的意思是:在函数中使用return或raise等语句提前结束分支中的函数。比如在新的buy_fruit函数中,当不满足分支条件时,我们直接抛出异常结束本次代码分支。这样的代码没有嵌套分支,更直接易读。2.封装那些过于复杂的逻辑判断。如果条件分支中的表达式过于复杂,not/and/or太多,这段代码的可读性会大大降低,比如下面的代码:活跃度大于10,如果activity.is_activeandactivity.remaining>10anduser.is_activeand(user.sex=='female'oruser.level>3):user.add_coins(10000)return对于这样的代码,我们可以考虑将具体的分支逻辑封装成一个函数或者方法来简化代码:ifactivity.allow_new_user()和user.match_activity_condition():user.add_coins(10000)return其实重写代码后,之前的评论文字其实是可以去掉的。因为下面的代码已经达到了自解释的目的。至于具体什么样的用户符合活动条件?这种问题应该通过具体的match_activity_condition()方法来回答。提示:适当的封装不仅直接提高了代码的可读性,事实上,如果上述活动判断逻辑在代码中出现不止一次,封装就更有必要了。否则,重复的代码会极大的破坏这个逻辑的可维护性。3.注意不同分支下的重复代码。重复代码是代码质量的天敌,而条件分支语句极易成为重复代码的重灾区。因此,我们在编写条件分支语句时,需要特别注意不要产生不必要的重复代码。让我们看这个例子:#对于一个新用户,创建一个新的配置文件,否则更新旧的配置文件ifuser.no_profile_exists:create_user_profile(username=user.username,email=user.email,age=user.age,address=user.address,#新用户,设置用户积分为0points=0,created=now(),)else:update_user_profile(username=user.username,email=user.email,age=user.age,addresuser.address,updated=now(),)在上面的代码中,我们一眼就可以看出,在不同的分支下,程序调用了不同的函数,做了不同的事情。但是,因为那些重码的存在,我们很难简单的区分两者的区别在哪里。事实上,由于Python的动态特性,我们可以简单地重写上面的代码以显着提高可读性:profile_func=update_user_profileextra_args={'updated':now()}profile_func(username=user.username,email=user.email,age=user.age,address=user.address,**extra_args)当你写分支代码时,请特别注意分支产生的重复代码块。如果您可以轻松消除它们,请不要犹豫。4.谨慎使用三元表达式。三元表达式是Python2.5以后才支持的语法。在此之前,Python社区一度认为三元表达式是不必要的,需要用x和a或b来模拟。事实是,在许多情况下,纯if/else语句的代码可读性确实更好。一味追求三元表达式很容易诱使你写出复杂且可读性差的代码。所以,请记住只使用三元表达式来处理简单的逻辑分支常用技巧1、在使用“德摩根定律”进行分支判断时,我们有时会这样写代码:#如果用户没有登录或者用户没有使用chrome,如果不是user.has_logged_inornot则拒绝提供服务user.is_from_chrome:return"ourserviceisonlyavailableforchromeloggedinuser"当你第一次看到这段代码的时候,是不是需要想一会才能明白它想做什么?这是因为上面的逻辑表达式中有2个nots和1个or。而我们人类就是不擅长处理太多的“否定”“或”等逻辑关系。这个时候,德摩根定律就该发挥作用了。通俗地说,德摩根定律就是notAornotB等同于not(AandB)。通过这样的转换,上面的代码可以改写如下:ifnot(user.has_logged_inanduser.is_from_chrome):return"ourserviceisonlyopenforchromeloggedinuser",代码如何更易读?记住德摩根定律,很多时候在条件分支中简化代码逻辑是非常有用的。2、自定义对象的“布尔真假”我们常说在Python中“万物皆对象”。其实不光是“万物皆对象”,我们还可以使用很多神奇的方法(文档中称之为:用户自定义方法)。我们可以使用很多其他语言做不到的魔术方法来影响代码的执行。比如Python中的所有对象都有自己的“布尔真假”:布尔值为false的对象:None,0,False,[],(),{},set(),frozenset(),......为真的布尔对象:非零值、True、非空序列、元组、普通用户类实例、......通过内置函数bool(),可以轻松查看布尔为真或一个对象的错误。而Python在判断条件分支的时候也是使用这个值:>>>bool(object())True关键点就在这里,虽然所有用户类实例的布尔值都是true。但是Python提供了一种改变这种行为的方法:自定义类的__bool__魔术方法(Python2.X版本中的__nonzero__)。当一个类定义了一个bool方法时,它的返回值将被视为该类实例的布尔值。此外,__bool__并不是影响实例布尔真值的唯一方法。如果类没有定义bool方法,Python也会尝试调用len方法(即对任意序列对象调用len函数),通过结果是否为0来判断实例是否为真。所以这个功能有什么用?看看这段代码:classUserCollection(object):def__init__(self,users):self._users=usersusers=UserCollection([piglei,raymond])iflen(users._users)>0:print("There'ssomeusersincollection!")上面代码中通过users._users的长度来判断UserCollection是否有内容。事实上,上面的分支可以通过在UserCollection中添加len魔法方法来变得更简单:UserCollection([piglei,raymond])#定义了__len__方法后,UserCollection对象本身就可以进行布尔判断ifusers:print("There'ssomeusersincollection!")通过定义魔术方法len和bool,我们可以让类自己控制它要显示的布尔真假值,让代码更pythonic。3.在条件判断中使用all()/any()all()和any()这两个函数非常适合在条件判断中使用。这两个函数接受一个可迭代对象并返回一个布尔值,其中:all(seq):只有当seq中的所有对象都是布尔值真时才返回True,否则返回Falseany(seq):只要seq中的任何对象都是布尔值就返回True,否则返回False如果我们有以下代码:defall_numbers_gt_10(numbers):"""仅当序列中的所有数字都大于10时才返回True"""如果不是数字:返回Falseforninnumbers:ifn<=10:returnFalsereturnTrue如果使用all()内置函数和一个简单的生成器表达式,上面的代码可以写成如下:defall_numbers_gt_10_2(numbers):returnbool(numbers)andall(n>10forninnumbers)简单高效,不会损失可用性。4.使用try/while/for/for中中分支让我们我们:defdo_stuff():first_thing_successed=falsetry:do_the_first_thing()first_thing_thing_successed=trueextureextiviondectexextivionextivionextivionextivionextivionextectee:dothesecondthingonlyiffirst_thingsuccessfulcompletediffirst_thing_successed:returndo_the_second_thing()在函数do_stuff中,我们希望只有在do_the_first_thing()调用成功(即不抛出任何异常)函数调用时才继续做第二件事。为此,我们需要定义一个附加变量first_thing_successed作为标志。其实我们可以用更简单的方式实现同??样的效果:else块结束:添加else分支后,该分支下的do_the_second_thing()只有在try下面的所有语句都正常执行后才会执行(即没有异常,没有return,break等).同样,Python中的for/while循环也支持添加else分支,即当循环使用的迭代对象正常耗尽,或者while循环使用的条件变量变为False时,执行else分支下的代码.常见1.与None值的比较在Python中,比较变量有两种方式:==和is,两者在含义上有根本区别:==:表示两者所指向的值是否一致is:表示两者是否指向内存中相同的内容,即id(x)是否等于id(y)None是Python语言中的单例对象。如果要判断一个变量是否为None,记得使用is而不是==,因为只有is才能严格表示一个变量是否为None。否则可能会出现如下情况:>>>classFoo(object):...def__eq__(self,other):...returnTrue...>>>foo=Foo()>>>foo==NoneTrue在上面的代码中,Foo类通过自定义eq魔术方法轻松满足条件==None。所以,当你想检查一个变量是否为None时,使用is而不是==。2、注意and和or的运算优先级。看下面两个表达式。猜猜它们是否具有相同的值?>>>(TrueorFalse)andFalse>>>TrueorFalseandFalse答案是:不一样,它们的值分别是False和True,你猜对了吗?问题的症结在于:and运算符的优先级高于or。所以上面的第二个表达式实际上看起来像Trueor(FalseandFalse)toPython。所以结果是True而不是False。在写包含多个and和or的表达式时,要特别注意and和or的运算优先级。即使执行优先级正是您所需要的,您也可以添加额外的括号以使代码更清晰。结论代码中的分支语句是不可避免的。我们在写代码的时候,需要特别注意它的可读性,以免给其他看到代码的人带来麻烦。