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 的运算优先级

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

Python

>>> (**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 编程语言要掌握的技能之一:』系列文章的第二篇。不知道文章的内容是否对你的胃口。 代码内的分支语句不可避免,我们在编写代码时,需要尤其注意它的可读性,避免对其他看到代码的人造成困扰。 看完文章的你,有没有什么想吐槽的?请留言告诉我吧。

更多Python技术文章请关注2019,Python技术技术持续更新(附教程)

Python 编程语言要掌握的技能之一:编写条件分支代码的技巧的更多相关文章

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

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

  2. Python :编写条件分支代码的技巧

    『Python 工匠』是什么? 我一直觉得编程某种意义是一门『手艺』,因为优雅而高效的代码,就如同完美的手工艺品一样让人赏心悦目. 在雕琢代码的过程中,有大工程:比如应该用什么架构.哪种设计模式.也有 ...

  3. Python 编程语言要掌握的技能之一:使用数字与字符串的技巧

    最佳实践 1. 少写数字字面量 “数字字面量(integer literal)” 是指那些直接出现在代码里的数字.它们分布在代码里的各个角落,比如代码 del users[0] 里的 0 就是一个数字 ...

  4. Python 编程语言要掌握的技能之一:善用变量来改善代码质量

    如何为变量起名 在计算机科学领域,有一句著名的格言(俏皮话): There are only two hard things in Computer Science: cache invalidati ...

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

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

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

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

  7. 编写高质量代码改善python程序91个建议学习01

    编写高质量代码改善python程序91个建议学习 第一章 建议1:理解pythonic的相关概念 狭隘的理解:它是高级动态的脚本编程语言,拥有很多强大的库,是解释从上往下执行的 特点: 美胜丑,显胜隐 ...

  8. 中小学Python编程语言教学

    中小学Python编程语言教学 作为一名高中信息技术老师,被技术的发展潮流推动着,不断更新教学内容和方法,以适应快速发展的信息社会. 以前的中小学信息技术课程,老师们各显神通,身怀绝技,教PS,Fal ...

  9. 编写高质量代码–改善python程序的建议(二)

    原文发表在我的博客主页,转载请注明出处! 建议七:利用assert语句来发现问题断言(assert)在很多语言中都存在,它主要为调试程序服务,能够快速方便地检查程序的异常或者发现不恰当的输入等,可防止 ...

随机推荐

  1. Ubuntu 16.04下配置 Nginx 与 Node.js 以及服务的部署

    第一步:安装nginx sudo apt-get update sudo apt-get install nginx 如果遇到依赖问题,尝试执行sudo apt-get -f install命令 第二 ...

  2. 拒绝黑盒应用-Spring Boot 应用可视化监控

    图文简介 逻辑关系 效果演示 快速开始 1.Spring Boot 应用暴露监控指标[版本 1.5.7.RELEASE] 首先,添加依赖如下依赖: <dependency> <gro ...

  3. redis集群之Codis

    在大数据高并发场景下,单个 Redis 实例往往会显得捉襟见肘.首先体现在内存上,单个 Redis 的内存不宜过大,内存太大会导致 rdb 文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢 ...

  4. Spring Cloud Feign初接触

    最近想使用下Feign,然后简单了解了一下,简单的搭了个demo. 首先简单介绍一下Feign,它是一个Http请求客户端,类似HttpClient,具体里面实现还没去看,知道它是一个请求客户端就行, ...

  5. JVM(4) 类文件结构

    一.实现“平台无关性” 字节码(ByteCode)存储格式和虚拟机是实现语言无关性的基础.Java虚拟机不和包括Java在内的任何语言绑定,它只与“Clas”文件这种特定的二进制文件格式所关联,Cla ...

  6. 配置IIS服务器提供APP文件下载

    [IIS7.IIS7.5] 1)打开IIS服务管理器,左边点到计算机(也可设置特定网站) 2)右边功能项中找到MIME类型,双击打开3)右键“添加”4)扩展名中填写".apk",M ...

  7. split分割文件与数据

    split主要用途:在之前计算机发展的时候,我们必须分割文件,才能将大量数据放入多张软盘中,而今我们分割文件有了其他的目的,比如提高可读性,生成日志以及发送有大小限制的E-mail附件. 工作原理:s ...

  8. 使用linq实现回调函数

    通过输入的Id找到parentId是该Id的列表,然后找到parentId是上面那个Id的列表,以此类推,找到第一目录下所有子目录的的列表. 通过传入第一目录的Id,得到该目录下的所有子目录. 通过回 ...

  9. 《Effective Java》 读书笔记(八)避免使用Finalizer和Cleaner机制

    Finalizer和Cleaner并不等同于C++中的析构函数,是不确定多久会被调用的,甚至有时候可能不会被调用,因此除了作为一个安全网或者终止非关键的本地资源,不应该在Finalizer或Clean ...

  10. 建设城市(city):组合数,容斥原理

    想模一大堆人呢.考场上AC的大仙. 估计没人想给这题好好写一个题解吧,因为它的确挺简单的... 但是它对我来说一点都不简单啊!!! 至少出题人用脚写题解的时候肯定认为这道题是送分题了 容斥,枚举至少有 ...