Python实践提升-条件分支控制流
从某种角度来看,编程这件事,其实就是把真实世界里的逻辑用代码的方式书写出来。
而真实世界里的逻辑通常很复杂,包含许许多多先决条件和结果分支,无法用一句简单的“因为……所以……”来概括。如果画成地图,这些逻辑不会是只有几条高速公路的郊区,而更像是包含无数个岔路口的闹市区。
为了表现这些真实世界里的复杂逻辑,程序员们写出了一条条分支语句。比如简单的“如果用户是会员,跳过广告播放”:
if user.is_active_member():
skip_ads()
return True
else:
print('你不是会员,无法跳过广告。')
return False
或者复杂一些的:
if user.is_active_member():
if user.membership_expires_in(30):
print('会员将在 30 天内过期,请及时续费,将在 3 秒后跳过广告')
skip_ads_with_delay(3)
return True
skip_ads()
return True
elif user.region != 'CN':
print('非中国区无法跳过广告')
return False
else:
print('你不是会员,无法跳过广告。')
return False
当条件分支变得越来越复杂,代码的可读性也会变得越来越差。所以,掌握如何写出好的条件分支代码非常重要,它可以帮助我们用更简洁、更清晰的代码来表达复杂逻辑。本章将会谈谈如何在 Python 中写出更好的条件分支代码。
4.1 基础知识
4.1.1 分支惯用写法
在 Python 里写条件分支语句,听上去是件挺简单的事。这是因为严格说来 Python 只有一种条件分支语法——if/elif/else1:
1在 Python 3.10 版本发布后,这个说法其实已不再成立。Python 在 3.10 版本里引入了一种新的分支控制结构:结构化模式匹配(structural pattern matching)。这种新结构启用了 match/case 关键字,实现了类似 C 语言中的 switch/case 语法。但和传统 switch 语句比起来,Python 的模式匹配功能要强大得多(语法也复杂得多)。因为本书的编写环境是 Python 3.8,所以我不会对“结构化模式匹配”做太多介绍。如果你对它感兴趣,可以阅读 PEP-634 了解更多内容。
#标准条件分支语句
if condition:
...
elif another_condition:
...
else:
...
当我们编写分支时,第一件要注意的事情,就是不要显式地和布尔值做比较:
#不推荐的写法
#if user.is_active_member() == True:
#推荐写法
if user.is_active_member():
绝大多数情况下,在分支判断语句里写 == True 都没有必要,删掉它代码会更短也更易读。但这条原则也有例外,比如你确实想让分支仅当值是 True 时才执行。不过即便这样,写 if == True 仍然是有问题的,我会在 4.1.3 节解释这一点。
省略零值判断
当你编写 if 分支时,如果需要判断某个类型的对象是否是零值,可能会把代码写成下面这样:
if containers_count == 0:
...
if fruits_list != []:
...
这种判断语句其实可以变得更简单,因为当某个对象作为主角出现在 if 分支里时,解释器会主动对它进行“真值测试”,也就是调用 bool() 函数获取它的布尔值。而在计算布尔值时,每类对象都有着各自的规则,比如整型和列表的规则如下:
#数字 0 的布尔值为 False,其他值为 True
>>> bool(0), bool(123)
(False, True)
#空列表的布尔值为 False,其他值为 True
>>> bool([]), bool([1, 2, 3])
(False, True)
正因如此,当我们需要在条件语句里做空值判断时,可以直接把代码简写成下面这样:
if not containers_count:
...
if fruits_list:
...
这样的条件判断更简洁,也更符合 Python 社区的习惯。不过在你使用这种写法时,请不要忘记一点,这样写其实隐晦地放宽了分支判断的成立条件:
#更精准:只有为 0 的时候,才会满足分支条件
if containers_count == 0:
....
#更宽泛:当 containers_count 的值为 0、None、空字符串等时,都可以满足分支条件
if not containers_count:
...
请时刻注意,不要因为过度追求简写而引入其他逻辑问题。
除整型外,其他内置类型的布尔值规则如下。
布尔值为假:None、0、False、[]、()、{}、set()、frozenset(),等等。
布尔值为真:非 0 的数值、True,非空的序列、元组、字典,用户定义的类和实例,等等。
把否定逻辑移入表达式内
在构造布尔逻辑表达式时,你可以用 not 关键字来表达“否定”含义:
>>> i = 10
>>> i > 8
True
>>> not i > 8
False
不过在写代码时,我们有时会过于喜欢用 not 关键字,反倒忘记了运算符本身就可以表达否定逻辑。最后,代码里会出现许多下面这种判断语句:
if not number < 10:
...
if not current_user is None:
...
if not index == 1:
...
这样的代码,就好比你在看到一个人沿着楼梯往上走时,不说“他在上楼”,而非说“他在做和下楼相反的事情”。如果把否定逻辑移入表达式内,它们通通可以改成下面这样:
if number >= 10:
...
if current_user is not None:
...
if index != 1:
...
这样的代码逻辑表达得更直接,也更好理解。
尽可能让三元表达式保持简单
除了标准分支外,Python 还为我们提供了一种浓缩版的条件分支——三元表达式:
#语法:
#true_value if <expression> else false_value
language = "python" if you.favor("dynamic") else "golang"
当你在编写三元表达式时,请参考 3.3.6 节的两个“不要”里的建议,不要盲目追求用一个表达式来表达过于复杂的逻辑。有时,平淡普通的分支语句远远胜过花哨复杂的三元表达式。
4.1.2 修改对象的布尔值
上一节提过,当我们把某个对象用于分支判断时,解释器会对它进行“真值测试”,计算出它的布尔值,而所有用户自定义的类和类实例的计算结果都是 True:
>>> class Foo:
... pass
...
>>> bool(Foo)
True
>>> bool(Foo())
True
这个现象符合逻辑,但有时会显得有点儿死板。如果我们稍微改动一下这个默认行为,就能写出更优雅的代码。
看看下面这个例子:
class UserCollection:
"""用于保存多个用户的集合工具类"""
def __init__(self, users):
self.items = users
users = UserCollection(['piglei', 'raymond'])
#仅当用户列表里面有数据时,打印语句
if len(users.items) > 0:
print("There's some users in collection!")
在上面这段代码里,我需要判断 users 对象是否真的有内容,因此里面的分支判断语句用到了 len(users.items) > 0 这样的表达式:判断对象内 items 的长度是否大于 0。
但其实,上面的分支判断语句可以变得更简单。只要给 UserCollection 类实现 len 魔法方法,users 对象就可以直接用于“真值测试”:
class UserCollection:
"""用于保存多个用户的集合工具类"""
def __init__(self, users):
self.items = users
def __len__(self):
return len(self.items)
users = UserCollection(['piglei', 'raymond'])
#不再需要手动判断对象内部 items 的长度
if users:
print("There's some users in collection!")
为类定义 len 魔法方法,实际上就是为它实现了 Python 世界的长度协议:
>>> users = UserCollection([])
>>> len(users)
0
>>> users = UserCollection(['piglei', 'raymond'])
>>> len(users)
2
Python 在计算这类对象的布尔值时,会受 len(users) 的结果影响——假如长度为 0,布尔值为 False,反之为 True。因此当例子中的 UserCollection 类实现了 len 后,整个条件判断语句就得到了简化。
不过,定义 len 并非影响布尔值结果的唯一办法。除了 len 以外,还有一个魔法方法 bool 和对象的布尔值息息相关。
为对象定义 bool 方法后,对它进行布尔值运算会直接返回该方法的调用结果。举个例子