首页
学习
活动
专区
工具
TVP
发布

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

序言

编写条件分支代码是编码过程中不可或缺的一部分。 如果用道路来做比喻,现实世界中的代码从来都不是一条笔直的高速公路,而更像是由无数个岔路口组成的某个市区地图。我们编码者就像是驾驶员,需要告诉我们的程序,下个路口需要往左还是往右。 编写优秀的条件分支代码非常重要,因为糟糕、复杂的分支处理非常容易让人困惑,从而降低代码质量。所以,这篇文章将会种重点谈谈在 Python 中编写分支代码应该注意的地方。

Python 里的分支代码

Python 支持最为常见的条件分支语句,不过它缺少在其他编程语言中常见的语句。 除此之外,Python 还为循环以及语句提供了 else 分支,在一些特殊的场景下,它们可以大显身手。 下面我会从、、三个方面讲一下如果编写优秀的条件分支代码。

最佳实践

1. 避免多层分支嵌套

如果这篇文章只能删减成一句话就结束,那么那句话一定是“要竭尽所能的避免分支嵌套”。 过深的分支嵌套是很多编程新手最容易犯的错误之一。假如有一位新手 JavaScript 程序员写了很多层分支嵌套,那么你可能会看到一层又一层的大括号:。俗称“嵌套 if 地狱(Nested If Statement Hell)”。 但是因为 Python 使用了缩进来代替,所以过深的嵌套分支会产生比其他语言下更为严重的后果。比如过多的缩进层次很容易就会让代码超过 PEP8 中规定的每行字数限制。让我们看看这段代码:

def buy_fruit(nerd,store):

"""去水果店买苹果

- 先得看看店是不是在营业

- 如果有苹果的话,就买 1 个

- 如果钱不够,就回家取钱再来

"""

ifstore.is_open():

ifstore.has_stocks("apple"):

ifnerd.can_afford(store.price("apple",amount=1)):

nerd.buy(store,"apple",amount=1)

return

else:

nerd.go_home_and_get_money()

returnbuy_fruit(nerd,store)

else:

raise MadAtNoFruit("no apple in store!")

else:

raise MadAtNoFruit("store is closed!")

上面这段代码最大的问题,就是过于直接翻译了原始的条件分支要求,导致短短十几行代码包含了有三层嵌套分支。 这样的代码可读性和维护性都很差。不过我们可以用一个很简单的技巧:“提前结束”来优化这段代码:

def buy_fruit(nerd,store):

ifnotstore.is_open():

raise MadAtNoFruit("store is closed!")

ifnotstore.has_stocks("apple"):

raise MadAtNoFruit("no apple in store!")

ifnerd.can_afford(store.price("apple",amount=1)):

nerd.buy(store,"apple",amount=1)

return

else:

nerd.go_home_and_get_money()

returnbuy_fruit(nerd,store)

“提前结束”指:在函数内使用 或 等语句提前在分支内结束函数。比如,在新的 函数里,当分支条件不满足时,我们直接抛出异常,结束这段这代码分支。这样的代码没有嵌套分支,更直接也更易读。

2. 封装那些过于复杂的逻辑判断

如果条件分支里的表达式过于复杂,出现了太多的 ,那么这段代码的可读性就会大打折扣,比如下面这段代码:

# 如果活动还在开放,并且活动剩余名额大于 10,为所有性别为女性,或者级别大于 3

# 的活跃用户发放 10000 个金币

ifactivity.is_activeandactivity.remaining>10and\

user.is_activeand(user.sex=='female'oruser.level>3):

user.add_coins(10000)

return

对于这样的代码,我们可以考虑将具体的分支逻辑封装成函数或者方法,来达到简化代码的目的:

ifactivity.allow_new_user()anduser.match_activity_condition():

user.add_coins(10000)

return

事实上,将代码改写后,之前的注释文字其实也可以去掉了。因为后面这段代码已经达到了自说明的目的。至于具体的什么样的用户满足活动条件?这种问题,就应由具体的 方法来回答了。

Hint:恰当的封装不光直接改善了代码的可读性,事实上,如果上面的活动判断逻辑在代码中出现了不止一次的话,封装更是必须的。不然重复代码会极大的破坏这段逻辑的可维护性。

3. 留意不同分支下的重复代码

重复代码是代码质量的天敌,而条件分支语句又非常容易成为重复代码的重灾区。所以,当我们编写条件分支语句时,需要特别留意,不要生产不必要的重复代码。 让我们看下这个例子:

# 对于新用户,创建新的用户资料,否则更新旧资料

ifuser.no_profile_exists:

create_user_profile(

username=user.username,

email=user.email,

age=user.age,

address=user.address,

# 对于新建用户,将用户的积分置为 0

points=,

created=now(),

)

else:

update_user_profile(

username=user.username,

email=user.email,

age=user.age,

address=user.address,

updated=now(),

)

在上面的代码中,我们可以一眼看出,在不同的分支下,程序调用了不同的函数,做了不一样的事情。但是,因为那些重复代码的存在,我们却很难简单的区分出,二者的不同点到底在哪。其实,得益于 Python 的动态特性,我们可以简单的改写一下上面的代码,让可读性可以得到显著的提升:

ifuser.no_profile_exists:

profile_func=create_user_profile

extra_args={'points':,'created':now()}

else:

profile_func=update_user_profile

extra_args={'updated':now()}

profile_func(

username=user.username,

email=user.email,

age=user.age,

address=user.address,

**extra_args

)

当你编写分支代码时,请额外关注由分支产生的重复代码块,如果可以简单的消灭它们,那就不要迟疑。

4. 谨慎使用三元表达式

三元表达式是 Python 2.5 版本后才支持的语法。在那之前,Python 社区一度认为三元表达式没有必要,我们需要使用 的方式来模拟它。[注] 事实是,在很多情况下,使用普通的 语句的代码可读性确实更好。盲目追求三元表达式很容易诱惑你写出复杂、可读性差的代码。 所以,请记得只用三元表达式处理简单的逻辑分支。

language = "python" if you.favor("dynamic") else "golang"

对于绝大多数情况,还是使用普通的 语句吧。

常见技巧

1. 使用“德摩根定律”

在做分支判断时,我们有时候会写成这样的代码:

# 如果用户没有登录或者用户没有使用 chrome,拒绝提供服务

ifnotuser.has_logged_inornotuser.is_from_chrome:

return"our service is only available for chrome logged in user"

第一眼看到代码时,是不是需要思考一会才能理解它想干嘛?这是因为上面的逻辑表达式里面出现了 2 个和 1 个。而我们人类恰好不擅长处理过多的“否定”以及“或”这种逻辑关系。 这个时候,就该德摩根定律出场了。通俗的说,德摩根定律就是等价于。通过这样的转换,上面的代码可以改写成这样:

ifnot(user.has_logged_inanduser.is_from_chrome):

return"our service is only open for chrome logged in user"

怎么样,代码是不是易读了很多?记住德摩根定律,很多时候它对于简化条件分支里的代码逻辑非常有用。

2. 自定义对象的“布尔真假”

我们常说,在 Python 里,“万物皆对象”。其实,不光“万物皆对象”,我们还可以利用很多魔法方法(文档中称为:user-defined method),来自定义对象的各种行为。我们可以用很多在别的语言里面无法做到、有些魔法的方式来影响代码的执行。 比如,Python 的所有对象都有自己的“布尔真假”:

布尔值为假的对象:,,,,,,,, … …

布尔值为真的对象:非的数值、,非空的序列、元组,普通的用户类实例,… …

通过内建函数,你可以很方便的查看某个对象的布尔真假。而 Python 进行条件分支判断时用到的也是这个值:

>>>bool(object())

True

重点来了,虽然所有用户类实例的布尔值都是真。但是 Python 提供了改变这个行为的办法:自定义类的魔法方法(在 Python 2.X 版本中为)。当类定义了方法后,它的返回值将会被当作类实例的布尔值。 另外,不是影响实例布尔真假的唯一方法。如果类没有定义方法,Python 还会尝试调用方法(也就是对任何序列对象调用函数),通过结果是否为判断实例真假。 那么这个特性有什么用呢?看看下面这段代码:

classUserCollection(object):

def __init__(self,users):

self._users=users

users=UserCollection([piglei,raymond])

iflen(users._users)>:

print("There's some users in collection!")

上面的代码里,判断是否有内容时用到了的长度。其实,通过为添加魔法方法,上面的分支可以变得更简单:

classUserCollection:

def __init__(self,users):

self._users=users

def __len__(self):

returnlen(self._users)

users=UserCollection([piglei,raymond])

# 定义了 __len__ 方法后,UserCollection 对象本身就可以被用于布尔判断了

ifusers:

print("There's some users in collection!")

通过定义魔法方法 和 ,我们可以让类自己控制想要表现出的布尔真假值,让代码变得更 pythonic。

3. 在条件判断中使用 all() / any()

和两个函数非常适合在条件判断中使用。这两个函数接受一个可迭代对象,返回一个布尔值,其中:

:仅当中所有对象都为布尔真时返回,否则返回

:只要中任何一个对象为布尔真就返回,否则返回

假如我们有下面这段代码:

def all_numbers_gt_10(numbers):

"""仅当序列中所有数字大于 10 时,返回 True

"""

ifnotnumbers:

returnFalse

forninnumbers:

returnFalse

returnTrue

如果使用内建函数,再配合一个简单的生成器表达式,上面的代码可以写成这样:

def all_numbers_gt_10_2(numbers):

returnbool(numbers)andall(n>10forninnumbers)

简单、高效,同时也没有损失可用性。

4. 使用 try/while/for 中 else 分支

让我们看看这个函数:

def do_stuff():

first_thing_successed=False

try:

do_the_first_thing()

first_thing_successed=True

except Exceptionase:

print("Error while calling do_some_thing")

return

# 仅当 first_thing 成功完成时,做第二件事

iffirst_thing_successed:

returndo_the_second_thing()

在函数中,我们希望只有当成功调用后(也就是不抛出任何异常),才继续做第二个函数调用。为了做到这一点,我们需要定义一个额外的变量来作为标记。 其实,我们可以用更简单的方法达到同样的效果:

def do_stuff():

try:

do_the_first_thing()

except Exceptionase:

print("Error while calling do_some_thing")

return

else:

returndo_the_second_thing()

在语句块最后追加上分支后,分支下的便只会在try 下面的所有语句正常执行(也就是没有异常,没有 return、break 等)完成后执行。 类似的,Python 里的循环也支持添加分支,它们表示:当循环使用的迭代对象被正常耗尽、或 while 循环使用的条件变量变为 False 后才执行 else 分支下的代码。

常见陷阱

1. 与 None 值的比较

在 Python 中,有两种比较变量的方法:和,二者在含义上有着根本的区别:

:表示二者所指向的的是否一致

:表示二者是否指向内存中的同一份内容,也就是是否等于

在 Python 语言中是一个单例对象,如果你要判断某个变量是否为 None 时,记得使用而不是,因为只有才能在严格意义上表示某个变量是否是 None。 否则,可能出现下面这样的情况:

>>>classFoo(object):

...def __eq__(self,other):

...returnTrue

...

>>>foo=Foo()

>>>foo==None

True

在上面代码中,Foo 这个类通过自定义 魔法方法的方式,很容易就满足了 这个条件。所以,当你要判断某个变量是否为 None 时,请使用 而不是 。

2. 留意 and 和 or 的运算优先级

看看下面这两个表达式,猜猜它们的值一样吗?

>>>(TrueorFalse)andFalse

>>>TrueorFalseandFalse

答案是:不一样,它们的值分别是和,你猜对了吗? 问题的关键在于:运算符的优先级大于。因此上面的第二个表达式在 Python 看来实际上是。所以结果是而不是。 在编写包含多个和的表达式时,请额外注意和的运算优先级。即使执行优先级正好是你需要的那样,你也可以加上额外的括号来让代码更清晰。

结语

代码内的分支语句不可避免,我们在编写代码时,需要尤其注意它的可读性,避免对其他看到代码的人造成困扰。 看完文章的你,有没有什么想吐槽的?请留言告诉我吧。

注解

事实上不是总能给你正确的结果,只有当 a 与 b 的布尔值为真时,这个表达式才能正常工作,这是由逻辑运算的短路特性决定的。你可以在命令行中运行试试看,结果是 0 而非 None。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180729B1DOYL00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券