这篇文章总结条件判断的一些高级用法,总结下条件分支控制流使用策略。
当你编写if分支时,如果需要判断某个类型的对象是否是零值,可能会把代码写成下面。
if containers_count == 0
if fruits_list != []
这种判断语句其实可以变得更简单,因为某个对象作为主角出现在if分支里,解释器会主动对它进行“真值测试”,也就是调用bool()函数获取他的布尔值。
每类对象都有着各自的规则。
布尔值为假:None、0、False、[]、()、{}、set()、frozenset()、等
布尔值为真:非0的数值、True、非空的列表、元组、字典、用户定义的类和实例等等
if not containers_count:
if not fruits_list
在构造布尔逻辑表达式时,你可以用not关键字来表达否定含义。
i > 8
not i>8
有时候过于使用not关键字,反倒忘记了运算符本身就可以表达否定逻辑。这样的代码,就好比你在看到一个人沿着楼梯往上走时,不说“他在上楼”,非要说“他在做和下楼相反的事情”
if not number < 10
if not index == 1
如果把否定逻辑移入表达式内,他们可以改成下面这样。
if number >= 10
if current_user is not None
除了标准的分支,python提供了浓缩的表达式,三元表达式
# 三元表达式语法
true_value if <expression> else false_value
class UserCollection:
def __init__(self, users):
self.items = users
users = UserCollection(['xiaoming', 'xiaozhang'])
if len(users.items) > 0:
print(f'这个属性有值:{users.items}')
上面的示例判断user对象是否真的有内容,因此在分支判断语句用到了len(users.items) > 0 这样的表达式,其实这个可以变得更简单。
只要给UserCollection类实现__len__魔法方法,users对象就可以直接用于“真值测试”
class userCollection_v2:
def __init__(self, users):
self.items = users
# 为类定义len魔法方法,python在计算这类对象的布尔值时,会受len(users)的结果影响,假如长度为0,布尔值为False,反之为True
def __len__(self):
return len(self.items)
users = userCollection_v2(['xiaoming', 'xiaozhang'])
# 不在需要手动判断对象内部items的长度
if users:
print(f'这个属性有值:{users.items}')
不过定义len并非影响布尔值结果的唯一办法,还有一个魔法方法__bool__和对象的布尔值息息相关。
为对象定义__bool__方法后,对它进行布尔值运算会直接返回该方法的调用结果。
class ScoreJudger:
def __init__(self, score):
self.score = score
# 仅当分数大于60时为真
def __bool__(self):
return self.score >= 60
if ScoreJudger(60):
print('true')
else:
print('false')
如果一个类中同时定义了__len__和__bool__两个方法,解释器会优先使用__bool__方法的执行结果。
当我们判断两个对象是否相等时,可以使用双等号和is来判断,但是他们是有区别的。因此我们需要了解它的区别在根据场景选择使用哪个方式。
# is与==
x,y,z = 1, 1, 2
print('x == y的结果:', x == y)
print('x == z的结果:', x == z)
# 重写eq方法
class EqualWithAnything:
def __eq__(self, other):
'''
判断 x == y
:param other: y的值
:return:
'''
return True
foo = EqualWithAnything()
print('重写eq魔法方法后,上面定义的EqualWithAnything对象,在和任何对象做==计算时都会返回True。', foo == None)
当你想判断对象是否为None时,应该使用is运算符,它的行为不会被重写。
print('is判断对象是否为None', foo is None)
# 输出结果
False
x = None
print('有且仅有真正的None 才能通过is判断', x is None)
# 输出结果
True
既然is在进行比较时更严格,为什么不把所有判断都用is来替代那?
这是因为,除了None,True,False这三个内置对象以外,其他类型对象在python中并不是严格以单例模式存在的,即便值一致他们在内存中仍然可以是两个对象。
因此当你需要判断某个对象是否是None,True,False时,使用is,其他情况下使用 ==
从一份电影数据中提取电影信息,按照评分rating的值把电影划分为S、A、B、C、等级;按照指定顺序输出电影信息。
import random
movies = [{'name': '侠客行', 'year': 2008, 'rating': '9'},
{'name': '西游记', 'year': 1983, 'rating': '9'},
{'name': '厨神', 'year': 2010, 'rating': '7'},
{'name': '喜来乐', 'year': 2006, 'rating': '6.9'},
{'name': '康熙来了', 'year': 2021, 'rating': '8'}
]
class Movies:
def __init__(self, name, year, rating):
self.name = name
self.year = year
self.rating = rating
@property
def rank(self):
'''
根据评分对电影分级
-S:8.5分以上
-A: 8~8.5
-B: 7~8
-C: 6~7
-D: 6分以下
'''
rating_num = float(self.rating)
if rating_num >= 8.5:
return 'S'
elif rating_num > 8:
return 'A'
elif rating_num > 7:
return 'B'
elif rating_num > 6:
return 'C'
else:
return 'D'
def get_scorted_movies(movies, sorting_type):
'''
电影列表排序并返回结果
:param movies: 对象列表
:param sorting_type: 排序选项
:return:
'''
if sorting_type == "name":
sorted_movies = sorted(movies, key=lambda movie: movie.name.lower())
elif sorting_type == "rating":
sorted_movies = sorted(movies, key=lambda movie: float(movie.rating), reverse=True)
elif sorting_type == "year":
sorted_movies = sorted(movies, key=lambda movie: movie.year, reverse=True)
elif sorting_type == "random":
sorted_movies = sorted(movies, key=lambda movie: random.random())
else:
raise RuntimeError(f'Unknown sorting type: {sorting_type}')
return sorted_movies
def main():
'''
为了把上面代码串起来,在main函数里实现了接收排序选项,解析电影数据,排序并打印电影列表功能。
:return:
'''
all_sorting_type = ('name', 'rating', 'year', 'random')
sorting_type = input('Please input sorting type:')
if sorting_type not in all_sorting_type:
print('Sorry,"{}" is not a valid sorting type,please choose from'
'"{}",exit now'.format(sorting_type, '/'.join(all_sorting_type))
)
return
# 初始化电影数据对象
movies_items = []
for movie_json in movies:
movie = Movies(**movie_json)
movies_items.append(movie)
# 排序输出电影列表
sorted_movies = get_scorted_movies(movies_items,sorting_type)
for movie in sorted_movies:
print(
f'-[{movie.rank} {movie.name} ({movie.year}) | rating: {movie.rating}]'
)
if __name__ == '__main__':
main()
# 输出结果
Please input sorting type:name
-[S 侠客行 (2008) | rating: 9]
-[C 厨神 (2010) | rating: 7]
-[C 喜来乐 (2006) | rating: 6.9]
-[B 康熙来了 (2021) | rating: 8]
-[S 西游记 (1983) | rating: 9]
上面的代码完成了一个小工具,虽然这个工具实现了功能,在他的代码里隐藏着两大段可以简化的条件分支代码,如果使用前档的方式可以使分支消失。
第一个需要优化的分支判断是rank方法属性中的分支。
@property
def rank(self):
'''
根据评分对电影分级
-S:8.5分以上
-A: 8~8.5
-B: 7~8
-C: 6~7
-D: 6分以下
'''
rating_num = float(self.rating)
if rating_num >= 8.5:
return 'S'
elif rating_num > 8:
return 'A'
elif rating_num > 7:
return 'B'
elif rating_num > 6:
return 'C'
else:
return 'D'
仔细观察这段代码,你会发现它有一个明细的规律,每个if/elif 语句后,都跟着一个评分的分界点,这个分界点把评分分成不同的分段。要优化这段代码,先把所有分界点收集起来,放在一个元组里。
breakpoints = [6, 7, 8, 8.5]
breakpoints已经是一个排好序的元组,所以我们可以直接使用bisect模块来实现查找功能,bisect是python内置的二分算法模块,他有一个同名函数bisect,可以用来在有序列表里做二分查找。
import bisect
# 用二分查找的容器必须是排好序的
breakpoints = [6, 7, 8, 8.5]
print('使用二分查找算法,根据值返回索引位置:', bisect.bisect(breakpoints, 7))
# 将分界点定义成元组,并引入bisect,之前的分支代码可以简化成下面的样子。
class Movies2:
def __init__(self, name, year, rating):
self.name = name
self.year = year
self.rating = rating
@property
def rank(self):
breakpoints = (6, 7, 8, 8.5)
grades = ('D', 'C', 'B', 'A', 'S')
index = bisect.bisect(breakpoints,float(self.rating))
return grades[index]
在get_sorted_movies()函数里,同样有一段分支代码,这段代码有两个明显的特点。
def get_scorted_movies(movies, sorting_type):
'''
电影列表排序并返回结果
:param movies: 对象列表
:param sorting_type: 排序选项
:return:
'''
if sorting_type == "name":
sorted_movies = sorted(movies, key=lambda movie: movie.name.lower())
elif sorting_type == "rating":
sorted_movies = sorted(movies, key=lambda movie: float(movie.rating), reverse=True)
elif sorting_type == "year":
sorted_movies = sorted(movies, key=lambda movie: movie.year, reverse=True)
elif sorting_type == "random":
sorted_movies = sorted(movies, key=lambda movie: random.random())
else:
raise RuntimeError(f'Unknown sorting type: {sorting_type}')
return sorted_movies
字典优化判断分支代码
def get_scorted_movies(movies, sorting_type):
'''
电影列表排序并返回结果
:param movies: 对象列表
:param sorting_type: 排序选项
:return:
'''
# 将判断的条件封装到字典中
sorting_algos = {
# sorting_type: (key_func, reverse)
'name': (lambda movie: movie.name.lower(), False),
'rating': (lambda movie: float(movie.rating), True),
'year': (lambda movie: movie.year, True)
}
try:
# 通过key获取字典中value的值,值的类型是元组因此可以获取两个变量
key_func, reverse = sorting_algos[sorting_type]
except KeyError:
raise RuntimeError(f'Unknown sorting type: {sorting_type}')
sorted_movies = sorted(movies, key=key_func, reverse=reverse)
return sorted_movies
在编写代码时,有事会下意识地编写一段大同小异的条件分支语句,多数情况下,他们只是对业务逻辑的一种直译,使我们对业务逻辑理解处在第一层的表现。
如果进一步深入业务逻辑,从中总结规律,那么条件分支代码就可以另一种更精简,更易扩展的方式替代。
class Movies2:
def __init__(self, name, year, rating):
self.name = name
self.year = year
self.rating = rating
@property
def rank(self):
breakpoints = (6, 7, 8, 8.5)
grades = ('D', 'C', 'B', 'A', 'S')
index = bisect.bisect(breakpoints, float(self.rating))
return grades[index]
def get_scorted_movies(movies, sorting_type):
'''
电影列表排序并返回结果
:param movies: 对象列表
:param sorting_type: 排序选项
:return:
'''
# 将判断的条件封装到字典中
sorting_algos = {
# sorting_type: (key_func, reverse)
'name': (lambda movie: movie.name.lower(), False),
'rating': (lambda movie: float(movie.rating), True),
'year': (lambda movie: movie.year, True)
}
try:
# 通过key获取字典中value的值,值的类型是元组因此可以获取两个变量
key_func, reverse = sorting_algos[sorting_type]
except KeyError:
raise RuntimeError(f'Unknown sorting type: {sorting_type}')
sorted_movies = sorted(movies, key=key_func, reverse=reverse)
return sorted_movies
在使用分支语句时要竭尽所能避免分支嵌套。每当业务逻辑变得越来越复杂,条件分支就会越来越多,嵌套越来越深。当代码有了多层嵌套后,可读性和可维护性就会直线下降。这是因为读代码的人很难在深层嵌套里搞清楚,如果不满足某个条件会发生什么。
下面来看一个卖水果的案例,在这个案例中就嵌套了三层分支判断语句,可读性就很差。
def buy_fruit(nerd, store):
'''
去水果店买水果流程:
- 先看看水果店是否在营业
- 如果有苹果就买一个
- 如果钱不够,就回家取钱再来
:param nerd:
:param store:
:return:
'''
# 水果店在营业状态
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 MadAtNotFruit("no apple in store!")
else:
raise MadAtNotFruit("store is closed!")
我们可以使用提前返回技巧简化多层分支嵌套,提前返回指的是:当你在编写分支时,首先找到那些会中断执行的条件,把它们移动到函数的最前面,然后在分支里使用return或raise结束执行。
通过提前返回,buy_fruit_v2函数变得扁平了,整个逻辑变得更直接,更容易理解了。
def buy_fruit_v2(nerd, store):
'''
去水果店买水果流程:
- 先看看水果店是否在营业
- 如果有苹果就买一个
- 如果钱不够,就回家取钱再来
:param nerd:
:param store:
:return:
'''
# 水果店没有开门则结束流程
if not store.is_open():
raise MadAtNotFruit("store is closed!")
# 水果店没有苹果则结束流程
if not store.has_stocks("apple"):
raise MadAtNotFruit("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)
假如某个分支的条件非常复杂,当我们把它翻译成代码时,一个包含大量not/and/or的复杂表达式就会横空出世了,看起来像是一个难懂的数学公式,代码可读性会随着表达式复杂度而下降。
# 如果活动还在开放,并且活动名额大于10,为所有性别为女性或者级别大于3
# 的用户发放1000个金币
if(
activity.is_active
and activity.remaining > 10
and user.is_active
and (user.sex == 'female' or user.level > 3)
):
user.add_coins(1000)
return
针对上面这个案例,需要对条件表达式简化,把它们封装成函数或者对应的类方法,这样才能提升分支代码的可读性。
if activity.allow_new_user() and user.match_activity_condition():
user.add_coins(1000)
return
在编写条件分支语句时,有些操作会因为业务逻辑相似性导致代码也很相似。有时这种相似的代码时完全重复的代码,有时则是调用函数时的重复参数。
假如不同分支下的代码过于相似,读者就很难区分不同分支下行为有什么差异。如果在编写代码时降低这种相似性就能有效的提升可读性。
下面是一个分支内代码相似的案例
# 仅当分组处于活跃时,允许用户加入分组
if group.is_active:
user = get_user_by_id(request.user_id)
user.join(group)
log_user_activiry(user, target=group, type=ActivityType.JOINED_GROUP)
else:
user = get_user_by_id(request.user_id)
log_user_activiry(user, target=group, type=ActivityType.JOINED_GROUP_FAILED)
把重复的代码移到分支外,降量降低分支内代码的相似性。
user = get_user_by_id(request.user_id)
if group.is_active:
user.join(group)
activity_type = ActivityType.JOINED_GROUP
else:
activity_type = ActivityType.JOINED_GROUP_FAILED
log_user_activiry(user, target=group, type=ActivityType.JOINED_GROUP)
判断用户是否存在,如果用户存在则更新用户信息,不存在则创建用户,创建和更新函数的参数有部分是重复的。很难一下看出二者的核心不同点是什么。
# 如果用户存在则更新用户信息,不存在则创建用户,创建和更新函数的参数有部分是重复的。很难分辨出他们的区别。
user = True
def create_user(id, name, sex, age):
print(f'创建用户信息:id:{id}, name:{name}, sex:{sex}, age:{age}')
def update_user(id, name, sex, address):
print(f'更新用户信息:id:{id}, name:{name}, sex:{sex}, address:{address}')
if user == False:
create_user(1, 'jery', 'man', 13)
else:
update_user(1, 'jery', 'man', '中关村')
为了降低函数参数的相似性,可以使用python函数的动态关键字参数(**kwargs),简化上面的代码。
user = True
def create_user(id, name, sex, age):
print(f'创建用户信息:id:{id}, name:{name}, sex:{sex}, age:{age}')
def update_user(id, name, sex, address):
print(f'更新用户信息:id:{id}, name:{name}, sex:{sex}, address:{address}')
if user == False:
# 将函数赋值给一个变量,由该变量为函数传递参数
_update_or_create = create_user
extra_args = {'age': 13}
else:
_update_or_create = update_user
extra_args = {'address': '中关村'}
_update_or_create(
id=1,
name='jery',
sex='man',
**extra_args,
)
如果遇到下面这样的代码,同时用了2个not和1个or,是不是需要思考一会才能弄明白他想干什么?这是因为人类恰巧不擅长处理这样有过多否定的逻辑关系。
根据德摩根定律,not A or not B 等价于 not (A and B)
当你的代码出现太多的否定,请尝试使用德摩根定律来化繁为简吧。
if not user.has_logged_in or not user.is_from_chrome:
return True
# 使用德摩根定律来优化代码
if not (user.has_logged_in and user.is_from_chrome):
return True
all()函数和any()这两个函数接收一个可迭代对象作为参数,返回一个布尔值结果。
这两个函数在构建条件表达式时可以实现特殊作用,他们的功能如下:
判断一个列表里所有的数字是不是都大于10 ,下面是普通的写法。
def all_number_gt_10(number: list):
if not number:
return False
for n in number:
if n <= 10:
return False
return True
如果使用all()内置函数,上面的代码可以简化为下面的样子
def all_number_gt_10_v2(number: list):
# boo():当number为空时直接返回False
return bool(number) and all(n > 10 for n in number)
b = all_number_gt_10_v2([])
print(f'列表中的数值是否全部大于10:{b}')
在使用and和or来构建逻辑表达式,要主要他们的优先级,先看下面的例子。
他们运算结果一样吗,答案是不一样的。他们的值分别是False和True
出现这个结果的原因是:and运算符优先级高于or运算符。
(True or False) and False
True or False and False
or运算符有一个有趣的特性就是”短路求值“特性,比如下面的例子里 1/0永远也不会执行。
True or (1 / 0)
因为这个特性在很多场景下可以简化代码,比如下面的例子。
context = {}
# 仅当extra_context不为None时,将其追加进contex字典中。
if extra_context:
context.update(extra_context)
# 使用or可以简化为一行代码
context.update(extra_context or {})
在对象不为空时就是extra_context自身,如果为None时就变成{}
因为or计算的是变量的布尔值真假,所以不光是None,0、[]、{}、以及其他布尔值为假的东西,都会在or运算中忽略。
下面来看一个or陷阱的例子
它的目的是判断timeout为None时,使用60作为默认值,但假如 config.timeout 的值被置为0秒,timeout也会被赋值60,正确的配置反而被忽略了。
timeout = config.timeout or 60