原做者:pigleihtml
原文连接:www.zlovezl.cn/articles/py…python
我一直以为编程某种意义上是一门『手艺』,由于优雅而高效的代码,就如同完美的手工艺品同样让人赏心悦目。程序员
在雕琢代码的过程当中,有大工程:好比应该用什么架构、哪一种设计模式。也有更多的小细节,好比什么时候使用异常(Exceptions)、或怎么给变量起名。那些真正优秀的代码,正是由无数优秀的细节造就的。golang
『Python 工匠』这个系列文章,是个人一次小小尝试。它专一于分享 Python 编程中的一些偏**『小』**的东西。但愿可以帮到每一位编程路上的匠人。chrome
系列文章:编程
编写条件分支代码是编码过程当中不可或缺的一部分。设计模式
若是用道路来作比喻,现实世界中的代码历来都不是一条笔直的高速公路,而更像是由无数个岔路口组成的某个市区地图。咱们编码者就像是驾驶员,须要告诉咱们的程序,下个路口须要往左仍是往右。bash
编写优秀的条件分支代码很是重要,由于糟糕、复杂的分支处理很是容易让人困惑,从而下降代码质量。因此,这篇文章将会种重点谈谈在 Python 中编写分支代码应该注意的地方。架构
Python 支持最为常见的 if/else
条件分支语句,不过它缺乏在其余编程语言中常见的 switch/case
语句。app
除此以外,Python 还为 for/while
循环以及 try/except
语句提供了 else 分支,在一些特殊的场景下,它们能够大显身手。
下面我会从 最佳实践
、常见技巧
、常见陷阱
三个方面讲一下若是编写优秀的条件分支代码。
若是这篇文章只能删减成一句话就结束,那么那句话必定是**“要竭尽所能的避免分支嵌套”**。
过深的分支嵌套是不少编程新手最容易犯的错误之一。假若有一位新手 JavaScript 程序员写了不少层分支嵌套,那么你可能会看到一层又一层的大括号:if { if { if { ... }}}
。俗称*“嵌套 if 地狱(Nested If Statement Hell)”*。
可是由于 Python 使用了缩进来代替 {}
,因此过深的嵌套分支会产生比其余语言下更为严重的后果。好比过多的缩进层次很容易就会让代码超过 PEP8 中规定的每行字数限制。让咱们看看这段代码:
def buy_fruit(nerd, store):
"""去水果店买苹果 - 先得看看店是否是在营业 - 若是有苹果的话,就买 1 个 - 若是钱不够,就回家取钱再来 """
if store.is_open():
if store.has_stocks("apple"):
if nerd.can_afford(store.price("apple", amount=1)):
nerd.buy(store, "apple", amount=1)
return
else:
nerd.go_home_and_get_money()
return buy_fruit(nerd, store)
else:
raise MadAtNoFruit("no apple in store!")
else:
raise MadAtNoFruit("store is closed!")
复制代码
上面这段代码最大的问题,就是过于直接翻译了原始的条件分支要求,致使短短十几行代码包含了有三层嵌套分支。
这样的代码可读性和维护性都不好。不过咱们能够用一个很简单的技巧:“提早结束” 来优化这段代码:
def buy_fruit(nerd, store):
if not store.is_open():
raise MadAtNoFruit("store is closed!")
if not store.has_stocks("apple"):
raise MadAtNoFruit("no apple in store!")
if nerd.can_afford(store.price("apple", amount=1)):
nerd.buy(store, "apple", amount=1)
return
else:
nerd.go_home_and_get_money()
return buy_fruit(nerd, store)
复制代码
“提早结束”指:**在函数内使用 return
或 raise
等语句提早在分支内结束函数。**好比,在新的 buy_fruit
函数里,当分支条件不知足时,咱们直接抛出异常,结束这段这代码分支。这样的代码没有嵌套分支,更直接也更易读。
若是条件分支里的表达式过于复杂,出现了太多的 not/and/or
,那么这段代码的可读性就会大打折扣,好比下面这段代码:
# 若是活动还在开放,而且活动剩余名额大于 10,为全部性别为女性,或者级别大于 3
# 的活跃用户发放 10000 个金币
if activity.is_active and activity.remaining > 10 and \
user.is_active and (user.sex == 'female' or user.level > 3):
user.add_coins(10000)
return
复制代码
对于这样的代码,咱们能够考虑将具体的分支逻辑封装成函数或者方法,来达到简化代码的目的:
if activity.allow_new_user() and user.match_activity_condition():
user.add_coins(10000)
return
复制代码
事实上,将代码改写后,以前的注释文字其实也能够去掉了。**由于后面这段代码已经达到了自说明的目的。**至于具体的 什么样的用户知足活动条件? 这种问题,就应由具体的 match_activity_condition()
方法来回答了。
Hint: 恰当的封装不光直接改善了代码的可读性,事实上,若是上面的活动判断逻辑在代码中出现了不止一次的话,封装更是必须的。否则重复代码会极大的破坏这段逻辑的可维护性。
重复代码是代码质量的天敌,而条件分支语句又很是容易成为重复代码的重灾区。因此,当咱们编写条件分支语句时,须要特别留意,不要生产没必要要的重复代码。
让咱们看下这个例子:
# 对于新用户,建立新的用户资料,不然更新旧资料
if user.no_profile_exists:
create_user_profile(
username=user.username,
email=user.email,
age=user.age,
address=user.address,
# 对于新建用户,将用户的积分置为 0
points=0,
created=now(),
)
else:
update_user_profile(
username=user.username,
email=user.email,
age=user.age,
address=user.address,
updated=now(),
)
复制代码
在上面的代码中,咱们能够一眼看出,在不一样的分支下,程序调用了不一样的函数,作了不同的事情。可是,由于那些重复代码的存在,咱们却很难简单的区分出,两者的不一样点到底在哪。
其实,得益于 Python 的动态特性,咱们能够简单的改写一下上面的代码,让可读性能够获得显著的提高:
if user.no_profile_exists:
profile_func = create_user_profile
extra_args = {'points': 0, '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
)
复制代码
当你编写分支代码时,请额外关注由分支产生的重复代码块,若是能够简单的消灭它们,那就不要迟疑。
三元表达式是 Python 2.5 版本后才支持的语法。在那以前,Python 社区一度认为三元表达式没有必要,咱们须要使用 x and a or b
的方式来模拟它。[注]
事实是,在不少状况下,使用普通的 if/else
语句的代码可读性确实更好。盲目追求三元表达式很容易诱惑你写出复杂、可读性差的代码。
因此,请记得只用三元表达式处理简单的逻辑分支。
language = "python" if you.favor("dynamic") else "golang"
复制代码
对于绝大多数状况,仍是使用普通的 if/else
语句吧。
在作分支判断时,咱们有时候会写成这样的代码:
# 若是用户没有登陆或者用户没有使用 chrome,拒绝提供服务
if not user.has_logged_in or not user.is_from_chrome:
return "our service is only available for chrome logged in user"
复制代码
第一眼看到代码时,是否是须要思考一会才能理解它想干吗?这是由于上面的逻辑表达式里面出现了 2 个 not
和 1 个 or
。而咱们人类刚好不擅长处理过多的“否认”以及“或”这种逻辑关系。
这个时候,就该 德摩根定律 出场了。通俗的说,德摩根定律就是 not A or not B
等价于 not (A and B)
。经过这样的转换,上面的代码能够改写成这样:
if not (user.has_logged_in and user.is_from_chrome):
return "our service is only open for chrome logged in user"
复制代码
怎么样,代码是否是易读了不少?记住德摩根定律,不少时候它对于简化条件分支里的代码逻辑很是有用。
咱们常说,在 Python 里,“万物皆对象”。其实,不光“万物皆对象”,咱们还能够利用不少魔法方法*(文档中称为:user-defined method)*,来自定义对象的各类行为。咱们能够用不少在别的语言里面没法作到、有些魔法的方式来影响代码的执行。
好比,Python 的全部对象都有本身的“布尔真假”:
None
, 0
, False
, []
, ()
, {}
, set()
, frozenset()
, ... ...0
的数值、True
,非空的序列、元组,普通的用户类实例,... ...经过内建函数 bool()
,你能够很方便的查看某个对象的布尔真假。而 Python 进行条件分支判断时用到的也是这个值:
>>> bool(object())
True
复制代码
重点来了,虽然全部用户类实例的布尔值都是真。可是 Python 提供了改变这个行为的办法:自定义类的 __bool__
魔法方法 (在 Python 2.X 版本中为 __nonzero__
)。当类定义了 __bool__
方法后,它的返回值将会被看成类实例的布尔值。
另外,__bool__
不是影响实例布尔真假的惟一方法。若是类没有定义 __bool__
方法,Python 还会尝试调用 __len__
方法*(也就是对任何序列对象调用 len
函数)*,经过结果是否为 0
判断实例真假。
那么这个特性有什么用呢?看看下面这段代码:
class UserCollection(object):
def __init__(self, users):
self._users = users
users = UserCollection([piglei, raymond])
if len(users._users) > 0:
print("There's some users in collection!")
复制代码
上面的代码里,判断 UserCollection
是否有内容时用到了 users._users
的长度。其实,经过为 UserCollection
添加 __len__
魔法方法,上面的分支能够变得更简单:
class UserCollection:
def __init__(self, users):
self._users = users
def __len__(self):
return len(self._users)
users = UserCollection([piglei, raymond])
# 定义了 __len__ 方法后,UserCollection 对象自己就能够被用于布尔判断了
if users:
print("There's some users in collection!")
复制代码
经过定义魔法方法 __len__
和 __bool__
,咱们可让类本身控制想要表现出的布尔真假值,让代码变得更 pythonic。
all()
和 any()
两个函数很是适合在条件判断中使用。这两个函数接受一个可迭代对象,返回一个布尔值,其中:
all(seq)
:仅当 seq
中全部对象都为布尔真时返回 True
,不然返回 False
any(seq)
:只要 seq
中任何一个对象为布尔真就返回 True
,不然返回 False
假如咱们有下面这段代码:
def all_numbers_gt_10(numbers):
"""仅当序列中全部数字大于 10 时,返回 True """
if not numbers:
return False
for n in numbers:
if n <= 10:
return False
return True
复制代码
若是使用 all()
内建函数,再配合一个简单的生成器表达式,上面的代码能够写成这样:
def all_numbers_gt_10_2(numbers):
return bool(numbers) and all(n > 10 for n in numbers)
复制代码
简单、高效,同时也没有损失可用性。
让咱们看看这个函数:
def do_stuff():
first_thing_successed = False
try:
do_the_first_thing()
first_thing_successed = True
except Exception as e:
print("Error while calling do_some_thing")
return
# 仅当 first_thing 成功完成时,作第二件事
if first_thing_successed:
return do_the_second_thing()
复制代码
在函数 do_stuff
中,咱们但愿只有当 do_the_first_thing()
成功调用后*(也就是不抛出任何异常)*,才继续作第二个函数调用。为了作到这一点,咱们须要定义一个额外的变量 first_thing_successed
来做为标记。
其实,咱们能够用更简单的方法达到一样的效果:
def do_stuff():
try:
do_the_first_thing()
except Exception as e:
print("Error while calling do_some_thing")
return
else:
return do_the_second_thing()
复制代码
在 try
语句块最后追加上 else
分支后,分支下的do_the_second_thing()
便只会在 try 下面的全部语句正常执行(也就是没有异常,没有 return、break 等)完成后执行。
相似的,Python 里的 for/while
循环也支持添加 else
分支,它们表示:当循环使用的迭代对象被正常耗尽、或 while 循环使用的条件变量变为 False 后才执行 else 分支下的代码。
在 Python 中,有两种比较变量的方法:==
和 is
,两者在含义上有着根本的区别:
==
:表示两者所指向的的值是否一致is
:表示两者是否指向内存中的同一分内容,也就是 id(x)
是否等于 id(y)
None
在 Python 语言中是一个单例对象,若是你要判断某个变量是否为 None 时,记得使用 is
而不是 ==
,由于只有 is
才能在严格意义上表示某个变量是不是 None。
不然,可能出现下面这样的状况:
>>> class Foo(object):
... def __eq__(self, other):
... return True
...
>>> foo = Foo()
>>> foo == None
True
复制代码
在上面代码中,Foo 这个类经过自定义 __eq__
魔法方法的方式,很容易就知足了 == None
这个条件。
因此,当你要判断某个变量是否为 None 时,请使用 is
而不是 ==
。
看看下面这两个表达式,猜猜它们的值同样吗?
>>> (True or False) and False
>>> True or False and False
复制代码
答案是:不同,它们的值分别是 False
和 True
,你猜对了吗?
问题的关键在于:and
运算符的优先级大于 or
。所以上面的第二个表达式在 Python 看来其实是 True or (False and False)
。因此结果是 True
而不是 False
。
在编写包含多个 and
和 or
的表达式时,请额外注意 and
和 or
的运算优先级。即便执行优先级正好是你须要的那样,你也能够加上额外的括号来让代码更清晰。
以上就是『Python 工匠』系列文章的第二篇。不知道文章的内容是否对你的胃口。
代码内的分支语句不可避免,咱们在编写代码时,须要尤为注意它的可读性,避免对其余看到代码的人形成困扰。
看完文章的你,有没有什么想吐槽的?请留言告诉我吧。
x and a or b
不是总能给你正确的结果,只有当 a 与 b 的布尔值为真时,这个表达式才能正常工做,这是由逻辑运算的短路特性决定的。你能够在命令行中运行 True and None or 0
试试看,结果是 0 而非 None。文章更新记录:
- 2018.04.08:在与 @geishu 的讨论后,调整了“运算优先符”使用的代码样例
- 2018.04.10:根据 @dongweiming 的建议,添加注解说明 "x and y or c" 表达式的陷阱