『Python 工匠』是什么?

我一直觉得编程某种意义是一门『手艺』,因为优雅而高效的代码,就如同完美的手工艺品一样让人赏心悦目。

在雕琢代码的过程中,有大工程:比如应该用什么架构、哪种设计模式。也有更多的小细节,比如何时使用异常(Exceptions)、或怎么给变量起名。那些真正优秀的代码,正是由无数优秀的细节造就的。

序言

编写条件分支代码是编码过程中不可或缺的一部分。

如果用道路来做比喻,现实世界中的代码从来都不是一条笔直的高速公路,而更像是由无数个岔路口组成的某个市区地图。我们编码者就像是驾驶员,需要告诉我们的程序,下个路口需要往左还是往右。

编写优秀的条件分支代码非常重要,因为糟糕、复杂的分支处理非常容易让人困惑,从而降低代码质量。所以,这篇文章将会种重点谈谈在 Python 中编写分支代码应该注意的地方。

Python 里的分支代码

Python 支持最为常见的 if/else 条件分支语句,不过它缺少在其他编程语言中常见的 switch/case 语句。

除此之外,Python 还为 for/while 循环以及 try/except 语句提供了 else 分支,在一些特殊的场景下,它们可以大显身手。

下面我会从 最佳实践、常见技巧、常见陷阱 三个方面讲一下如果编写优秀的条件分支代码。

最佳实践

1. 避免多层分支嵌套

如果这篇文章只能删减成一句话就结束,那么那句话一定是“要竭尽所能的避免分支嵌套”

过深的分支嵌套是很多编程新手最容易犯的错误之一。假如有一位新手 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 函数里,当分支条件不满足时,我们直接抛出异常,结束这段这代码分支。这样的代码没有嵌套分支,更直接也更易读。

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

如果条件分支里的表达式过于复杂,出现了太多的 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: 恰当的封装不光直接改善了代码的可读性,事实上,如果上面的活动判断逻辑在代码中出现了不止一次的话,封装更是必须的。不然重复代码会极大的破坏这段逻辑的可维护性。

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

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

让我们看下这个例子:

# 对于新用户,创建新的用户资料,否则更新旧资料
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
)

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

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 里,“万物皆对象”。其实,不光“万物皆对象”,我们还可以利用很多魔法方法(文档中称为: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。

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 分支

让我们看看这个函数:

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 分支下的代码。

常见陷阱

1. 与 None 值的比较

在 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 而不是 ==。

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

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

>>> (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 :编写条件分支代码的技巧的更多相关文章

  1. Python 工匠:编写条件分支代码的技巧

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由鹅厂优文发表于云+社区专栏 作者:朱雷 | 腾讯IEG高级工程师 『Python 工匠』是什么? 我一直觉得编程某种意义是一门『手艺』 ...

  2. Python 编程语言要掌握的技能之一:编写条件分支代码的技巧

    Python 里的分支代码 Python 支持最为常见的 if/else 条件分支语句,不过它缺少在其他编程语言中常见的 switch/case 语句. 除此之外,Python 还为 for/whil ...

  3. Sublime Text webstorm等编译器快速编写HTML/CSS代码的技巧

    <!DOCTYPE html> Sublime Text webstorm等编译器快速编写HTML/CSS代码的技巧--summer-rain博客园 xiayuhao 东风夜放花千树. 博 ...

  4. 利用Python编写Windows恶意代码!自娱自乐!勿用于非法用途!

    本文主要展示的是通过使用python和PyInstaller来构建恶意软件的一些poc. 利用Python编写Windows恶意代码!自娱自乐!勿用于非法用途!众所周知的,恶意软件如果影响到了他人的生 ...

  5. Sublime Text、webstorm等编译器快速编写HTML/CSS代码的技巧

    Sublime Text.webstorm等编译器,如果你从事Web前端开发的话,对这几款软件一定不会陌生.它使用仿CSS选择器的语法来生成代码,大大提高了HTML/CSS代码编写的速度,比如下面的演 ...

  6. python学习笔记04-了解操作符与条件分支

    先来了解一下条件操作符: 运算符 描述 示例 == 检查两个操作数的值是否相等,如果是则条件变为真. 如a=3,b=3则(a == b) 为 true. != 检查两个操作数的值是否相等,如果值不相等 ...

  7. 编写高质量代码--改善python程序的建议(六)

    原文发表在我的博客主页,转载请注明出处! 建议二十八:区别对待可变对象和不可变对象 python中一切皆对象,每一个对象都有一个唯一的标识符(id()).类型(type())以及值,对象根据其值能否修 ...

  8. Python教程(2.7)——条件分支

    像其它语言一样,Python也有条件分支. 例如,输入用户年龄,可能需要判断是否成年,并做出不同反应.这就需要用到条件分支. if条件分支 if条件分支的一般格式如下: if condition: s ...

  9. 编写高质量代码改善C#程序的157个建议——建议149:使用表驱动法避免过长的if和switch分支

    建议149:使用表驱动法避免过长的if和switch分支 随着代码变得复杂,我们很容易被过长的if和switch分支困扰. 一个类枚举类型Week如下: enum Week { Monday, Tue ...

随机推荐

  1. ICT测试点是干什么的, 怎么设置!

    简单理解:ICT类似如万用表,只是把表笔换成了测试针.那么问题就简单了,一颗普通的RLC元件,都必须有两个测试点才能够测试,当然同一个网络共用的节点用一个测试点就可以了. 详细描述: PCB设计时要看 ...

  2. EF Core 2.1 中的 Eager loading、Explicit loading和LazyLoading (转自MSDN)

    Entity Framework Core allows you to use the navigation properties in your model to load related enti ...

  3. springboot启动报错:Could not resolve placeholder

    SpringBoot1.5,项目启动报错: Could not resolve placeholder 很明显是找不到配置文件引起的,查看配置文件目录结构如下: 很正常呀. 完全可以加载applica ...

  4. 一点一点看JDK源码(五)java.util.ArrayList 后篇之Spliterator多线程遍历

    一点一点看JDK源码(五)java.util.ArrayList 后篇之Spliterator多线程遍历 liuyuhang原创,未经允许禁止转载 本文举例使用的是JDK8的API 目录:一点一点看J ...

  5. Subclass UICollectionViewFlowLayout,自定义流布局

    需求:为实现第一行显示一个,第二行以后显示两个 方案1:用系统自带的流布局,实现的效果是,若第二行只有一个,则系统默认会居中显示,不是左对齐(如下图),不符合项目要求. 方案2:自定义系统的UICol ...

  6. 一个 Safari 的 new Date() bug

    开发「bufpay.com 个人即时到账收款平台」后台套餐修改功能的时候碰到一个 new Date() bug. 既在 Safari 里面不支持 var t = new Date('2018-06-1 ...

  7. linux ping命令实践

          ping 解析       Linux系统的ping命令是常用的网络命令,它通常用来检测与目标主机的连通性,经常说"ping以下机器,看是否开着,不能打开网页时候,可以ping ...

  8. AppleScript 快速入门

    AppleScript 快速入门 AppleScript 顾名思义是苹果开发的一套脚本语言,利用 AppleScript 在 macOS 系统上可以对其他程序进行操作,点击按钮.发送消息.模拟自动化执 ...

  9. 【sql server常用操作{增删改查}】

    use DB_x   go   drop database DB_y   create database DB_y --创建数据库   on primary --指定主数据文件   (   name= ...

  10. FMX有一套自己的消息处理机制。类似这样:

    unit Unit1; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Var ...