Python⼯匠:编写条件分⽀代码的技巧
『Python ⼯匠』是什么?
我⼀直觉得编程某种意义上是⼀门『⼿艺』,因为优雅⽽⾼效的代码,就如同完美的⼿⼯艺品⼀样让⼈赏⼼悦⽬。
在雕琢代码的过程中,有⼤⼯程:⽐如应该⽤什么架构、哪种设计模式。也有更多的⼩细节,⽐如何时使⽤异常(Exceptions)、或怎么给变量起名。那些真正优秀的代码,正是由⽆数优秀的细节造就的。
『Python ⼯匠』这个系列⽂章,是我的⼀次⼩⼩尝试。它专注于分享 Python 编程中的⼀些偏『⼩』的东西。希望能够帮到每⼀位编程路上的匠⼈。
序⾔
编写条件分⽀代码是编码过程中不可或缺的⼀部分。
如果⽤道路来做⽐喻,现实世界中的代码从来都不是⼀条笔直的⾼速公路,⽽更像是由⽆数个岔路⼝组成的某个市区地图。我们编码者就像是驾驶员,需要告诉我们的程序,下个路⼝需要往左还是往右。
编写优秀的条件分⽀代码⾮常重要,因为糟糕、复杂的分⽀处理⾮常容易让⼈困惑,从⽽降低代码质量。所以,这篇⽂章将会种重点谈谈在Python 中编写分⽀代码应该注意的地⽅。
Python ⾥的分⽀代码
Python ⽀持最为常见的 if/else 条件分⽀语句,不过它缺少在其他编程语⾔中常见的 switch/case 语句。
除此之外,Python 还为 for/while 循环以及 try/except 语句提供了 else 分⽀,在⼀些特殊的场景下,它们可以⼤显⾝⼿。
下⾯我会从 最佳实践、常见技巧、常见陷阱 三个⽅⾯讲⼀下如果编写优秀的条件分⽀代码。
最佳实践
1. 避免多层分⽀嵌套
如果这篇⽂章只能删减成⼀句话就结束,那么那句话⼀定是“要竭尽所能的避免分⽀嵌套”。
过深的分⽀嵌套是很多编程新⼿最容易犯的错误之⼀。假如有⼀位新⼿ JavaScript 程序员写了很多层
分⽀嵌套,那么你可能会看到⼀层⼜⼀层的⼤括号:if { if { if { ... }}}。俗称“嵌套 if 地狱(Nested If Statement Hell)”。
但是因为 Python 使⽤了缩进来代替 {},所以过深的嵌套分⽀会产⽣⽐其他语⾔下更为严重的后果。⽐如过多的缩进层次很容易就会让代码超过  中规定的每⾏字数限制。让我们看看这段代码:
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:
<_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:
<_home_and_get_money()
return buy_fruit(nerd, store)
“提前结束”指:在函数内使⽤ return 或 raise 等语句提前在分⽀内结束函数。⽐如,在新的 buy_fruit 函数⾥,当分⽀条件不满⾜时,我们直接抛出异常,结束这段这代码分⽀。这样的代码没有嵌套分⽀,更直接也更易读。
2. 封装那些过于复杂的逻辑判断
如果条件分⽀⾥的表达式过于复杂,出现了太多的 not/and/or,那么这段代码的可读性就会⼤打折扣,⽐如下⾯这段代码:
# 如果活动还在开放,并且活动剩余名额⼤于 10,为所有性别为⼥性,或者级别⼤于 3
# 的活跃⽤户发放 10000 个⾦币
if activity.is_active aining > 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: 恰当的封装不光直接改善了代码的可读性,事实上,如果上⾯的活动判断逻辑在代码中出现了不⽌⼀次的话,封装更是必须的。
不然重复代码会极⼤的破坏这段逻辑的可维护性。
3. 留意不同分⽀下的重复代码
重复代码是代码质量的天敌,⽽条件分⽀语句⼜⾮常容易成为重复代码的重灾区。所以,当我们编写条件分⽀语句时,需要特别留意,不要⽣产不必要的重复代码。
让我们看下这个例⼦:
# 对于新⽤户,创建新的⽤户资料,否则更新旧资料
_profile_exists:
create_user_profile(
username=user.username,
ail,
age=user.age,
address=user.address,
# 对于新建⽤户,将⽤户的积分置为 0
points=0,
created=now(),
)
else:
update_user_profile(
username=user.username,
ail,
age=user.age,
address=user.address,
updated=now(),
)
在上⾯的代码中,我们可以⼀眼看出,在不同的分⽀下,程序调⽤了不同的函数,做了不⼀样的事情。但是,因为那些重复代码的存在,我们却很难简单的区分出,⼆者的不同点到底在哪。
其实,得益于 Python 的动态特性,我们可以简单的改写⼀下上⾯的代码,让可读性可以得到显著的提升:
_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,
ail,
age=user.age,
address=user.address,
**extra_args
)
当你编写分⽀代码时,请额外关注由分⽀产⽣的重复代码块,如果可以简单的消灭它们,那就不要迟疑。
4. 谨慎使⽤三元表达式
三元表达式是 Python 2.5 版本后才⽀持的语法。在那之前,Python 社区⼀度认为三元表达式没有必要,我们需要使⽤ x and a or b 的⽅式来模拟它。
事实是,在很多情况下,使⽤普通的 if/else 语句的代码可读性确实更好。盲⽬追求三元表达式很容易诱惑你写出复杂、可读性差的代码。
所以,请记得只⽤三元表达式处理简单的逻辑分⽀。
language = "python" if you.favor("dynamic") else "golang"
对于绝⼤多数情况,还是使⽤普通的 if/else 语句吧。
常见技巧
1. 使⽤“德摩根定律”
在做分⽀判断时,我们有时候会写成这样的代码:
# 如果⽤户没有登录或者⽤户没有使⽤ 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"
怎么样,代码是不是易读了很多?记住德摩根定律,很多时候它对于简化条件分⽀⾥的代码逻辑⾮常有⽤。
2. ⾃定义对象的“布尔真假”
我们常说,在 Python ⾥,“万物皆对象”。其实,不光“万物皆对象”,我们还可以利⽤很多魔法⽅法(⽂档中称为:),来⾃定义对象的各种⾏为。我们可以⽤很多在别的语⾔⾥⾯⽆法做到、有些魔法的⽅式来影响代码的执⾏。
⽐如,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!")
python新手代码错了应该怎么改通过定义魔法⽅法 __len__ 和 __bool__ ,我们可以让类⾃⼰控制想要表现出的布尔真假值,让代码变得更 pythonic。
3. 在条件判断中使⽤ all() / any()
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)
简单、⾼效,同时也没有损失可⽤性。
4. 使⽤ try/while/for 中 else 分⽀
让我们看看这个函数:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。