第 1 章 变量与注释
朱雷
讲述:Alloy大小:10.16M时长:44:23
编程是一个通过代码来表达思想的过程。听上去挺神秘,但其实我们早就做过类似的事情——当年在小学课堂上写出第一篇 500 字的作文,同样也是在表达思想,只是二者方式不同,作文用的是词语和句子,而编程用的是代码。
但代码与作文之间也有相通之处,代码里也有许多“词语”和“句子”。大部分的变量名是词语,而大多数注释本身就是句子。当我们看到一段代码时,最先注意到的,不是代码有几层循环,用了什么模式,而是变量与注释,因为它们是代码里最接近自然语言的东西,最容易被大脑消化、理解。
正因如此,如果作者在编写变量和注释时含糊不清、语焉不详,其他人将很难搞清楚代码的真实意图。就拿下面这行代码来说:
你能告诉我这段代码在做什么吗?当我看到它时,是这么想的:
在 s 上调用 strip(),所以 s 可能是一个字符串?不过为什么要去掉两边的空格呢?
process(...),顾名思义,“处理”了一下 s,但具体是什么处理呢?
处理结果赋值给了 value,value 代表“值”,但“值”又是什么?
开头的注释就更别提了,它说的就是代码本身,对理解代码没有丝毫帮助。
最后的结论是:“将一个可能是字符串的东西两端的空格去掉,然后处理一下,最后赋值给某个不明物体。”我只能理解到这种程度了。
但同样是这段代码,如果我稍微调整一下变量的名字,加上一点点注释,就会变得截然不同:
新代码读上去是什么感觉?是不是代码意图变得容易理解多了?这就是变量与注释的魔力。
从计算机的角度来看,变量(variable)是用来从内存找到某个东西的标记。它叫“阿猫”“阿狗”还是“张三”“李四”,都无所谓。注释同样如此,计算机一点儿都不关心你的注释写得是否通顺,用词是否准确,因为它在执行代码时会忽略所有的注释。
但正是这些对计算机来说无关痛痒的东西,直接决定了人们对代码的“第一印象”。好的变量和注释并非为计算机而写,而是为每个阅读代码的人而写(当然也包括你自己)。变量与注释是作者表达思想的基础,是读者理解代码的第一道门,它们对代码质量的贡献毋庸置疑。
本章将对 Python 里的变量和注释做简单介绍,我会分享一些常用的变量命名原则,介绍编写代码注释的几种方式。在编程建议部分,我会列举一些与变量和注释有关的好习惯。
我们从变量和注释开始,学习如何写出给人留下美好“第一印象”的好代码吧!
1.1 基础知识
本节将介绍一些与变量和注释相关的基础知识。
1.1.1 变量常见用法
在 Python 里,定义一个变量特别简单:
因为 Python 是一门动态类型的语言,所以我们无须预先声明变量类型,直接对变量赋值即可。
你也可以在一行语句里同时操作多个变量,比如调换两个变量所指向的值:
❶ 交换两个变量
变量解包
变量解包(unpacking)是 Python 里的一种特殊赋值操作,允许我们把一个可迭代对象(比如列表)的所有成员,一次性赋值给多个变量:
假如在赋值语句左侧添加小括号 (...),甚至可以一次展开多层嵌套数据:
除了上面的普通解包外,Python 还支持更灵活的动态解包语法。只要用星号表达式(*variables)作为变量名,它便会贪婪 地捕获多个值对象,并将捕获到的内容作为列表赋值给 variables。
比如,下面 data 列表里的数据就分为三段:头为用户,尾为分数,中间的都是水果名称。通过把 *fruits 设置为中间的解包变量,我们就能一次性解包所有变量——fruits 会捕获 data 去头去尾后的所有成员:
和常规的切片赋值语句比起来,动态解包语法要直观许多:
上面的变量解包操作也可以在任何循环语句里使用:
单下划线变量名 _
在常用的诸多变量名中,单下划线 _ 是比较特殊的一个。它常作为一个无意义的占位符出现在赋值语句中。_ 这个名字本身没什么特别之处,这算是大家约定俗成的一种用法。
举个例子,假如你想在解包赋值时忽略某些变量,就可以使用 _ 作为变量名:
而在 Python 交互式命令行(直接执行 python 命令进入的交互环境)里,_ 变量还有一层特殊含义——默认保存我们输入的上个表达式的返回值:
❶ 此时的 _ 变量保存着上一个 .upper() 表达式的结果
1.1.2 给变量注明类型
前面说过,Python 是动态类型语言,使用变量时不需要做任何类型声明。在我看来,这是 Python 相比其他语言的一个重要优势:它减少了我们的心智负担,让写代码变得更容易。尤其对于许多编程新手来说,“不用声明类型”无疑会让学 Python 这件事变得简单很多。
但任何事物都有其两面性。动态类型所带来的缺点是代码的可读性会因此大打折扣。
试着读读下面这段代码:
你能告诉我,函数接收的 items 参数是什么类型吗?是一个装满数字的列表,还是一个装满字符串的集合?只看上面这点儿代码,我们根本无从得知。
为了解决动态类型带来的可读性问题,最常见的办法就是在函数文档(docstring)里做文章。我们可以把每个函数参数的类型与说明全都写在函数文档里。
下面是增加了 Python 官方推荐的 Sphinx 格式文档后的效果:
在上面的函数文档里,我用 :type items: 注明了 items 是个整型列表。任何人只要读到这份文档,马上就能知道参数类型,不用再猜来猜去了。
当然,标注类型的办法肯定不止上面这一种。在 Python 3.5 版本 以后,你可以用类型注解功能来直接注明变量类型。相比编写 Sphinx 格式文档,我其实更推荐使用类型注解,因为它是 Python 的内置功能,而且正在变得越来越流行。
要使用类型注解,只需在变量后添加类型,并用冒号隔开即可,比如 func(value: str) 表示函数的 value 参数为字符串类型。
下面是给 remove_invalid() 函数添加类型注解后的样子:
❶ List 表示参数为列表类型,[int] 表示里面的成员是整型
“类型注解”只是一种有关类型的注释,不提供任何校验功能。要校验类型正确性,需要使用其他静态类型检查工具(如 mypy 等)。
平心而论,不管是编写 Sphinx 格式文档,还是添加类型注解,都会增加编写代码的工作量。同样一段代码,标注变量类型比不标注一定要花费更多时间。
但从我的经验来看,这些额外的时间投入,会带来非常丰厚的回报:
代码更易读,读代码时可以直接看到变量类型;
大部分的现代化 IDE 会读取类型注解信息,提供更智能的输入提示;
类型注解配合 mypy 等静态类型检查工具,能提升代码正确性(13.1.5 节)。
因此,我强烈建议在多人参与的中大型 Python 项目里,至少使用一种类型注解方案——Sphinx 格式文档或官方类型注解都行。能直接看到变量类型的代码,总是会让人更安心。
在 10.1.1 节中,你会看到更详细的“类型注解”功能说明,以及更多启用了类型注解的代码。
1.1.3 变量命名原则
如果要从变量着手来破坏代码质量,办法多到数也数不清,比如定义了变量但是不用,或者定义 100 个全局变量,等等。但如果要在这些办法中选出破坏力最强的那个,非“给变量起个坏名字”莫属。
下面这段代码就是一个充斥着坏名字的“集大成”者。试着读读,看看你会有什么感受:
怎么样,是不是挠破头都看不懂它在做什么?坏名字对代码质量的破坏力可见一斑。
那么问题来了,既然大家都知道上面这样的代码不好,为何在程序世界里,每天都有类似的代码被写出来呢?我猜这是因为给变量起个好名字真的很难。在计算机科学领域,有一句广为流传的格言(俏皮话):
计算机科学领域只有两件难事:缓存失效和命名。
——Phil Karlton
这句话里虽然一半严肃一半玩笑,但“命名”有时真的会难到让人抓狂。我常常呆坐在显示器前,抓耳挠腮好几分钟,就是没法给变量想出一个合适的名字。
要给变量起个好名字,主要靠的是经验,有时还需加上一丁点儿灵感,但更重要的是遵守一些基本原则。下面就是我总结的几条变量命名的基本原则。
遵循 PEP 8 原则
给变量起名主要有两种流派:一是通过大小写界定单词的驼峰命名派 CamelCase,二是通过下划线连接的蛇形命名派 snake_case。这两种流派没有明显的优劣之分,似乎与个人喜好有关。
为了让不同开发者写出的代码风格尽量保持统一,Python 制定了官方的编码风格指南:PEP 8。这份风格指南里有许多详细的风格建议,比如应该用 4 个空格缩进,每行不超过 79 个字符,等等。其中,当然也包含变量的命名规范:
对于普通变量,使用蛇形命名法,比如 max_value;
对于常量,采用全大写字母,使用下划线连接,比如 MAX_VALUE;
如果变量标记为“仅内部使用”,为其增加下划线前缀,比如 _local_var;
当名字与 Python 关键字冲突时,在变量末尾追加下划线,比如 class_。
除变量名以外,PEP 8 中还有许多其他命名规范,比如类名应该使用驼峰风格(FooClass)、函数应该使用蛇形风格(bar_function),等等。给变量起名的第一条原则,就是一定要在格式上遵循以上规范。
PEP 8 是 Python 编码风格的事实标准。“代码符合 PEP 8 规范”应该作为对 Python 程序员的基本要求之一。假如一份代码的风格与 PEP 8 大相径庭,就基本不必继续讨论它优雅与否了。
描述性要强
写作过程中的一项重要工作,就是为句子斟酌恰当的词语。不同词语的描述性强弱不同,比如“冬天的梅花”就比“花”的描述性更强。而变量名和普通词语一样,同样有描述性强弱之分,假如代码大量使用描述性弱的变量名,读者就很难理解代码的含义。
本章开头的那两段代码可以很好地解释这个问题:
所以,在可接受的长度范围内,变量名所指向的内容描述得越精确越好。表 1-1 是一些具体的例子。
表 1-1 描述性弱和描述性强的变量名示例
看到表 1-1 中的示例,你可能会想:“也就是说左边的名字都不好,永远别用它们?”
当然不是这样。判断一个名字是否合适,一定要结合它所在的场景,脱离场景谈名字是片面的,是没有意义的。因此,在“说明”这一列中,我们强调了这个判断所适用的场景。
而在其他一些场景下,这里“描述性弱”的名字也可能是好名字,比如把一个数学公式的计算结果叫作 value,就非常恰当。
要尽量短
刚刚说到,变量名的描述性要尽量强,但描述性越强,通常名字也就越长(不信再看看表 1-1,第二列的名字就比第一列长)。假如不加思考地实践“描述性原则”,那你的代码里可能会充斥着 how_many_points_needed_for_user_level3 这种名字,简直像条蛇一样长:
假如一个特别长的名字重复出现,读者不会认为它足够精确,反而会觉得啰唆难读。既然如此,怎么才能在保证描述性的前提下,让名字尽量简短易读呢?
我认为个中诀窍在于:为变量命名要结合代码情境和上下文。比如在上面的代码里,upgrade_to_level3(user) 函数已经通过自己的名称、文档表明了其目的,那在函数内部,我们完全可以把 how_many_points_needed_for_user_level3 直接删减成 level3_points。
即使没用特别长的名字,相信读代码的人也肯定能明白,这里的 level3_points 指的就是“升到级别 3 所需要的积分”,而不是其他含义。
要匹配类型
虽然变量无须声明类型,但为了提升可读性,我们可以用类型注解语法给其加上类型。不过现实很残酷,到目前为止,大部分 Python 项目没有类型注解 ,因此当你看到一个变量时,除了通过上下文猜测,没法轻易知道它是什么类型。
但是,对于变量名和类型的关系,通常会有一些“直觉上”的约定。如果在起名时遵守这些约定,就可以建立变量名和类型间的匹配关系,让代码更容易理解。
匹配布尔值类型的变量名
布尔值(bool)是一种很简单的类型,它只有两个可能的值:“是”(True)或“不是”(False)。因此,给布尔值变量起名有一个原则:一定要让读到变量的人觉得它只有“肯定”和“否定”两种可能。举例来说,is、has 这些非黑即白的词就很适合用来修饰这类名字。
表 1-2 中给出了一些更详细的例子。
表 1-2 布尔值变量名示例
匹配 int/float 类型的变量名
当人们看到和数字有关的名字时,自然就会认定它们是 int 或 float 类型。这些名字可简单分为以下几种常见类型:
释义为数字的所有单词,比如 port(端口号)、age(年龄)、radius(半径)等;
使用以 _id 结尾的单词,比如 user_id、host_id;
使用以 length/count 开头或者结尾的单词,比如 length_of_username、max_length、users_count。
最好别拿一个名词的复数形式来作为 int 类型的变量名,比如 apples、trips 等,因为这类名字容易与那些装着 Apple 和 Trip 的普通容器对象(List[Apple]、List[Trip])混淆,建议用 number_of_apples 或 trips_count 这类复合词来作为 int 类型的名字。
匹配其他类型的变量名
至于剩下的字符串(str)、列表(list)、字典(dict)等其他值类型,我们很难归纳出一个“由名字猜测类型”的统一公式。拿 headers 这个名字来说,它既可能是一个装满头信息的列表(List[Header]),也可能是一个包含头信息的字典(Dict[str, Header])。
对于这些值类型,强烈建议使用我们在 1.1.2 节中提到的方案,在代码中明确标注它们的类型详情。
超短命名
在众多变量名里,有一类非常特别,那就是只有一两个字母的短名字。这些短名字一般可分为两类,一类是那些大家约定俗成的短名字,比如:
数组索引三剑客 i、j、k
某个整数 n
某个字符串 s
某个异常 e
文件对象 fp
我并不反对使用这类短名字,我自己也经常用,因为它们写起来的确很方便。但如果条件允许,建议尽量用更精确的名字替代。比如,在表示用户输入的字符串时,用 input_str 替代 s 会更明确一些。
另一类短名字,则是对一些其他常用名的缩写。比如,在使用 Django 框架做国际化内容翻译时,常常会用到 gettext 方法。为了方便,我们常把 gettext 缩写成 _:
如果你的项目中有一些长名字反复出现,可以效仿上面的方式,为它们设置一些短名字作为别名。这样可以让代码变得更紧凑、更易读。但同一个项目内的超短缩写不宜太多,否则会适得其反。
其他技巧
除了上面这些规则外,下面再分享几个给变量命名的小技巧:
在同一段代码内,不要出现多个相似的变量名,比如同时使用 users、users1、users3 这种序列;
可以尝试换词来简化复合变量名,比如用 is_special 来代替 is_not_normal;
如果你苦思冥想都想不出一个合适的名字,请打开 GitHub,到其他人的开源项目里找找灵感吧!
1.1.4 注释基础知识
注释(comment)是代码非常重要的组成部分。通常来说,注释泛指那些不影响代码实际行为的文字,它们主要起额外说明作用。
Python 里的注释主要分为两种,一种是最常见的代码内注释,通过在行首输入 # 号来表示:
当注释包含多行内容时,同样使用 # 号:
除使用 # 的注释外,另一种注释则是我们前面看到过的函数(类)文档(docstring),这些文档也称接口注释(interface comment)。
接口注释有好几种流行的风格,比如 Sphinx 文档风格、Google 风格等,其中 Sphinx 文档风格目前应用得最为广泛。上面的 Person 类的接口注释就属于 Sphinx 文档风格。
虽然注释一般不影响代码的执行效果,却会极大地影响代码的可读性。在编写注释时,编程新手们常常会犯同类型的错误,以下是我整理的最常见的 3 种。
用注释屏蔽代码
有时,人们会把注释当作临时屏蔽代码的工具。当某些代码暂时不需要执行时,就把它们都注释了,未来需要时再解除注释。
其实根本没必要这么做。这些被临时注释掉的大段内容,对于阅读代码的人来说是一种干扰,没有任何意义。对于不再需要的代码,我们应该直接把它们删掉,而不是注释掉。如果未来有人真的需要用到这些旧代码,他直接去 Git 仓库历史里就能找到,毕竟版本控制系统就是专门干这个的。
用注释复述代码
在编写注释时,新手常犯的另一类错误是用注释复述代码。就像这样:
上面代码里的注释完全是冗余的,因为读者从代码本身就能读到注释里的信息。好的注释应该像下面这样:
注释作为代码之外的说明性文字,应该尽量提供那些读者无法从代码里读出来的信息。描述代码为什么要这么做,而不是简单复述代码本身。
除了描述“为什么”的解释性注释外,还有一种注释也很常见:指引性注释。这种注释并不直接复述代码,而是简明扼要地概括代码功能,起到“代码导读”的作用。
比如,以下代码里的注释就属于指引性注释:
指引性注释并不提供代码里读不到的东西——假如没有注释,耐心读完所有代码,你也能知道代码做了什么事儿。指引性注释的主要作用是降低代码的认知成本,让我们能更容易理解代码的意图。
在编写指引性注释时,有一点需要注意,那就是你得判断何时该写注释,何时该将代码提炼为独立的函数(或方法)。比如上面的代码,其实可以通过抽象两个新函数改成下面这样:
这么改以后,代码里的指引性注释就可以删掉了,因为有意义的函数名已经达到了概括和指引的作用。
正是因为如此,一部分人认为:只要代码里有指引性注释,就说明代码的可读性不高,无法“自说明”,一定得抽象新函数把其优化成第二种样子。
但我倒认为事情没那么绝对。无论代码写得多好,多么“自说明”,同读代码相比,读注释通常让人觉得更轻松。注释会让人们觉得亲切(尤其当注释是中文时),高质量的指引性注释确实会让代码更易读。有时抽象一个新函数,不见得就一定比一行注释加上几行代码更好。
弄错接口注释的受众
在编写接口注释时,人们有时会写出下面这样的内容:
上面这段注释虽然有些夸张,但像它一样的注释在项目中其实并不少见。这段接口注释最主要的问题在于过多阐述了函数的实现细节,提供了太多其他人并不关心的内容。
接口文档主要是给函数(或类)的使用者看的,它最主要的存在价值,是让人们不用逐行阅读函数代码,也能很快通过文档知道该如何使用这个函数,以及在使用时有什么注意事项。
在编写接口文档时,我们应该站在函数设计者的角度,着重描述函数的功能、参数说明等。而函数自身的实现细节,比如调用了哪个第三方模块、为何有性能问题等,无须放在接口文档里。
对于上面的 resize_image() 函数来说,文档里提供以下内容就足够了:
至于那些使用了 Pilot 模块、为何有内存问题的细节说明,全都可以丢进函数内部的代码注释里。
1.2 案例故事
下面是 Python 程序员小 R 去其他公司面试的故事。
在本书剩下的案例故事里,你还会多次看到“小 R”的身影。
小 R 这个名字来自作者的英文名(Raymond)的首字母缩写。随着故事的不同,小 R 有时是一位 Python 初学者,有时又是一名有多年经验的 Python 老手。但无论扮演什么角色,他总会在每个故事里获得新的成长。
下面,我们看一看本书的第一个案例故事。
奇怪的冒泡排序算法
上午 10 点,在 T 公司的会议室里,小 R 正在参加一场他准备了好几天的技术面试。
整体来说,他在这场面试中的表现还不错。无论坐在小 R 对面的面试官提出什么问题,他都能侃侃而谈、对答如流。从单体应用聊到微服务,从虚拟机聊到云计算,每一块小 R 都说得滴水不漏。就在他认为自己胜券在握,可以通过这家自己憧憬已久的公司面试时,对面的面试官突然说道:“技术问题我问得差不多了。最后有一道编程题,希望你可以做一下。”
说完,面试官低头从包里拿出了一台笔记本电脑,递给了小 R。小 R 有些紧张地接过电脑,发现屏幕上是一道算法题。
题目 冒泡排序算法
请用 Python 语言实现冒泡排序算法,把较大的数字放在后面。注意:默认所有的偶数都比奇数大。
“冒泡排序,这不是所有排序算法里最简单的一种吗?虽然加了一点儿变化,但看起来没有什么难度啊。”小 R 一边在心里这么想着,一边打开编辑器开始写代码。
五分钟后,他把笔记本电脑递给面试官并说道:“写完了!”
代码清单 1-1 就是他写的代码。
代码清单 1-1 小 R 写的冒泡排序函数
这段代码没有任何多余的逻辑,可以通过所有的测试用例。面试官看着小 R 演示完函数功能后,盯着代码似乎想说点儿什么,但最后只是微微点了点头,说:“好,今天的面试就到这儿吧,有后续面试我再通知你。”
小 R 高高兴兴地回到家,一心觉得这次面试稳了,可没想到,他后来却再也没接到任何后续面试的通知。
问题出在哪里
究竟是哪里出了问题呢?小 R 思来想去,觉得自己回答问题时表现挺好,最有可能出问题的是最后一道编程题,肯定是漏掉了什么边界情况没处理。
于是他找到一位有着十年编程经验的前辈小 Q,凭着记忆把题目和自己的答案还原给对方看。
“题目大概就是这样,这是我当时写的代码。Q 哥,你帮忙看看,我是不是有什么情况没考虑到?”小 R 问道。
小 Q 盯着他写的代码,足足两分钟没说一句话,然后突然开口道:“小 R 啊,你这个函数功能实现得没毛病,就是实在太难看懂了。”
“总共就 10 行代码。难看懂?怎么会呢?”小 R 在心里泛起了嘀咕。这时,前辈小 Q 说道:“这样,你把笔记本电脑给我,我来给你稍微改改这段代码,然后你再看看。”
三分钟后,小 Q 把修改过的代码递了过来,如代码清单 1-2 所示。
代码清单 1-2 小 Q 修改后的冒泡排序函数
❶ 注意:此处变量名是 next_ 而非 next,这是因为已经有一个内置函数使用了 next 这个名字。PEP 8 规定在这种情况下,应该给变量名增加 _ 后缀来避免冲突
小 R 盯着这段代码,发现它的核心逻辑和之前没有任何不同。但不知为何,这段代码看上去就是比自己写的代码更舒服。小 R 若有所思,好像一下明白了自己没通过面试的原因。
故事讲完了。看上去,前辈小 Q 只是在小 R 的代码之上做了些“无关痛痒”的改动,但正是这些“无关痛痒”的改动,改善了代码的观感,提升了整个函数的可读性。
“无关痛痒”的改动
和小 R 写的代码相比,前辈小 Q 的新代码主要进行了以下改进。
(1) 变量名变成了可读的、有意义的名字,比如在旧代码里,“停止位”是无意义的 j,新代码里变成了 stop_position。
(2) 增加了有意义的临时变量,比如 current/next_ 代表前一个 / 后一个元素、{}_is_even 代表元素是否为偶数、should_swap 代表是否应该交换元素。
(3) 多了一点儿恰到好处的指引性注释,比如说明交换元素顺序的详细条件。
这些变化让整段代码变得更易读,也让整个算法变得更好理解。所以,哪怕是一段不到 10 行代码的简单函数,对变量和注释的不同处理方式,也会让代码发生质的变化。
1.3 编程建议
“编程建议”是本书大部分章节存在的板块,我将在其中分享与每章主题有关的一些编程建议、技巧,这里并没有什么高谈阔论的大道理,多是些专注细节、务实好用的小点子。比如定义临时变量有什么好处,为什么应该先写注释再写代码,等等。希望这些“小点子”能帮助你写出更棒的代码。
下面,我们一起来看看那些跟变量与注释有关的“小点子”吧。
1.3.1 保持变量的一致性
在使用变量时,你需要保证它在两个方面的一致性:名字一致性与类型一致性。
名字一致性是指在同一个项目(或者模块、函数)中,对一类事物的称呼不要变来变去。如果你把项目里的“用户头像”叫作 user_avatar_url,那么在其他地方就别把它改成 user_profile_url。否则会让读代码的人犯迷糊:“user_avatar_url 和 user_profile_url 到底是不是一个东西?”
类型一致性则是指不要把同一个变量重复指向不同类型的值,举个例子:
在 foo() 函数的作用域内,users 变量被使用了两次:第一次指向字典,第二次则变成了列表。虽然 Python 的类型系统允许我们这么做,但这样做其实有很多坏处,比如变量的辨识度会因此降低,还很容易引入 bug。
所以,我建议在这种情况下启用一个新变量:
如果使用 mypy 工具(13.1.5 节会详细讲解),它在静态检查时就会报出这种“变量类型不一致”的错误。对于上面的代码,mypy 就会输出 error: Incompatible types in assignment(变量赋值时类型不兼容)错误。
1.3.2 变量定义尽量靠近使用
包括我自己在内的很多人在初学编程时有一种很不好的习惯——喜欢把所有变量初始化定义写在一起,放在函数最前面,就像下面这样:
之所以这么写代码,是因为我们觉得“初始化变量”语句是类似的,应该将其归类到一起,放到最前面,这样代码会整洁很多。
但是,这样的代码只是看上去整洁,它的可读性不会得到任何提升,反而会变差。
在组织代码时,我们应该谨记:总是从代码的职责出发,而不是其他东西。比如,在上面的 generate_trip_png() 函数里,代码的职责主要分为三块:
初始化 waypoints 数据
处理 markers 数据
计算 marker_count
那代码可以这么调整:
通过把变量定义移动到每段“各司其职”的代码头部,大大缩短了变量从初始化到被使用的“距离”。当读者阅读代码时,可以更容易理解代码的逻辑,而不是来回翻阅代码,心想:“这个变量是什么时候定义的?是干什么用的?”
1.3.3 定义临时变量提升可读性
随着业务逻辑变得复杂,我们的代码里也会经常出现一些复杂的表达式,就像下面这样:
看见 if 后面那一长串代码了吗?有点儿难读对不对?但这也没办法,毕竟产品经理就是明明白白这么跟我说的——业务逻辑如此。
逻辑虽然如此,不代表我们就得把代码直白地写成这样。如果把后面的复杂表达式赋值为一个临时变量,代码可以变得更易读:
在新代码里,“计算用户合规的表达式”和“判断合规发送金币的条件分支”这两段代码不再直接杂糅在一起,而是添加了一个可读性强的变量 user_is_elegible 作为缓冲。不论是代码的可读性还是可维护性,都因为这个变量而增强了。
直接翻译业务逻辑的代码,大多不是好代码。优秀的程序设计需要在理解原需求的基础上,恰到好处地抽象,只有这样才能同时满足可读性和可扩展性方面的需求。抽象有许多种方式,比如定义新函数、定义新类型,“定义一个临时变量”是诸多方式里不太起眼的一个,但用得恰当的话效果也很巧妙。
1.3.4 同一作用域内不要有太多变量
通常来说,函数越长,用到的变量也会越多。但是人脑的记忆力是很有限的。研究表明,人类的短期记忆只能同时记住不超过 10 个名字。变量过多,代码肯定就会变得难读,以代码清单 1-3 为例。
代码清单 1-3 局部变量过多的函数
import_users_from_file() 函数里的变量数量就有点儿多,比如用来暂存用户的 _users,用来保存结果的 succeeded_count、failed_count 等。
要减少函数里的变量数量,最直接的方式是给这些变量分组,建立新的模型。比如,我们可以将代码里的 succeeded_count、failed_count 建模为 ImportedSummary 类,用 ImportedSummary.succeeded_count 来替代现有变量;对 _users 也可以执行同样的操作。相关操作如代码清单 1-4 所示。
代码清单 1-4 对局部变量分组并建模
通过增加两个数据类,函数内的变量被更有逻辑地组织了起来,数量变少了许多。
需要说明的一点是,大多数情况下,只是执行上面这样的操作是远远不够的。函数内变量的数量太多,通常意味着函数过于复杂,承担了太多职责。只有把复杂函数拆分为多个小函数,代码的整体复杂度才可能实现根本性的降低。
在 7.3.1 节中,你可以找到更多与函数复杂度有关的内容,看到更多与拆分函数相关的建议。
1.3.5 能不定义变量就别定义
前面提到过,定义临时变量可以提高代码的可读性。但有时,把不必要的东西赋值为临时变量,反而会让代码显得啰唆:
在编写代码时,我们会下意识地定义很多变量,好为未来调整代码做准备。但其实,你所想的未来也许永远不会来。上面这段代码里的三个临时变量完全可以去掉,变成下面这样:
这样的代码就像删掉赘语的句子,变得更精练、更易读。所以,不必为了那些未来可能出现的变动,牺牲代码此时此刻的可读性。如果以后需要定义变量,那就以后再做吧!
1.3.6 不要使用 locals()
locals() 是 Python 的一个内置函数,调用它会返回当前作用域中的所有局部变量:
在有些场景下,我们需要一次性拿到当前作用域下的所有(或绝大部分)变量,比如在渲染 Django 模板时:
看上去使用 locals() 函数正合适,假如调用 locals(),上面的代码会简化许多:
第一眼看上去非常“简洁”,但是,这样的代码真的更好吗?
答案并非如此。locals() 看似简洁,但其他人在阅读代码时,为了搞明白模板渲染到底用了哪些变量,必须记住当前作用域里的所有变量。假如函数非常复杂,“记住所有局部变量”简直是个不可能完成的任务。
使用 locals() 还有一个缺点,那就是它会把一些并没有真正使用的变量也一并暴露。
因此,比起使用 locals(),建议老老实实把代码写成这样:
Python 之禅:显式优于隐式
在 Python 命令行中输入 import this,你可以看到 Tim Peters 写的一段编程原则: The Zen of Python(“Python 之禅”)。这些原则字字珠玑,里面蕴藏着许多 Python 编程智慧。
“Python 之禅”中有一句“Explicit is better than implicit”(显式优于隐式),这条原则完全可以套用到 locals() 的例子上——locals() 实在是太隐晦了,直接写出变量名显然更好。
1.3.7 空行也是一种“注释”
代码里的注释不只是那些常规的描述性语句,有时候,没有一个字符的空行,也算得上一种特殊的“注释”。
在写代码时,我们可以适当地在代码中插入空行,把代码按不同的逻辑块分隔开,这样能有效提升代码的可读性。
举个例子,拿本章案例故事里的代码来说,假如删掉所有空行,代码会变成代码清单 1-5 这样,请你试着读读看。
代码清单 1-5 没有任何空行的冒泡排序(所有文字类注释已删除)
怎么样?是不是感觉代码特别局促,连喘口气的机会都找不到?这就是缺少空行导致的。只要在代码里加上一丁点儿空行(不多,就两行),函数的可读性马上会得到可观的提升,如代码清单 1-6 所示。
代码清单 1-6 增加了空行的冒泡排序
1.3.8 先写注释,后写代码
在编写了许多函数以后,我总结出了一个值得推广的好习惯:先写注释,后写代码。
每个函数的名称与接口注释(也就是 docstring),其实是一种比函数内部代码更为抽象的东西。你需要在函数名和短短几行注释里,把函数内代码所做的事情,高度浓缩地表达清楚。
正因如此,接口注释其实完全可以当成一种协助你设计函数的前置工具。这个工具的用法很简单:假如你没法通过几行注释把函数职责描述清楚,那么整个函数的合理性就应该打一个问号。
举个例子,你在编辑器里写下了 def process_user(...):,准备实现一个名为 process_user 的新函数。在编写函数注释时,你发现在写了好几行文字后,仍然没法把 process_user() 的职责描述清楚,因为它可以同时完成好多件不同的事情。
这时你就应该意识到,process_user() 函数承担了太多职责,解决办法就是直接删掉它,设计更多单一职责的子函数来替代之。
先写注释的另一个好处是:不会漏掉任何应该写的注释。
我常常在审查代码时发现,一些关键函数的 docstring 位置一片空白,而那里本该备注详尽的接口注释。每当遇到这种情况,我都会不厌其烦地请代码提交者补充和完善接口注释。
为什么大家总会漏掉注释?我的一个猜测是:程序员在编写函数时,总是跳过接口注释直接开始写代码。而当写完代码,实现函数的所有功能后,他就对这个函数失去了兴趣。这时,他最不愿意做的事,就是回过头去补写函数的接口注释,即便写了,也只是草草对付了事。
如果遵守“先写注释,后写代码”的习惯,我们就能完全避免上面的问题。要养成这个习惯其实很简单:在写出一句有说服力的接口注释前,别写任何函数代码。
1.4 总结
在一段代码里,变量和注释是最接近自然语言的东西。因此,好的变量名、简明扼要的注释,都可以显著提升代码的质量。在给变量起名时,请尽量使用描述性强的名字,但也得注意别过了头。
从小 R 的面试故事来看,即使是两段功能完全一样的代码,也会因为变量和注释的区别,给其他人截然不同的感觉。因此,要想让你的代码给人留下“漂亮”的第一印象,请记得在变量和注释上多下功夫。
以下是本章要点知识总结。
(1) 变量和注释决定“第一印象”
变量和注释是代码里最接近自然语言的东西,它们的可读性非常重要
即使是实现同一个算法,变量和注释不一样,给人的感觉也会截然不同
(2) 基础知识
Python 的变量赋值语法非常灵活,可以使用 *variables 星号表达式灵活赋值
编写注释的两个要点:不要用来屏蔽代码,而是用来解释“为什么”
接口注释是为使用者而写,因此应该简明扼要地描述函数职责,而不必包含太多内部细节
可以用 Sphinx 格式文档或类型注解给变量标明类型
(3) 变量名字很重要
给变量起名要遵循 PEP 8 原则,代码的其他部分也同样如此
尽量给变量起描述性强的名字,但评价描述性也需要结合场景
在保证描述性的前提下,变量名要尽量短
变量名要匹配它所表达的类型
可以使用一两个字母的超短名字,但注意不要过度使用
(4) 代码组织技巧
按照代码的职责来组织代码:让变量定义靠近使用
适当定义临时变量可以提升代码的可读性
不必要的变量会让代码显得冗长、啰唆
同一个作用域内不要有太多变量,解决办法:提炼数据类、拆分函数
空行也是一种特殊的“注释”,适当的空行可以让代码更易读
(5) 代码可维护性技巧
保持变量在两个方面的一致性:名字一致性与类型一致性
显式优于隐式:不要使用 locals() 批量获取变量
把接口注释当成一种函数设计工具:先写注释,后写代码
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
本文深入介绍了Python编程中变量与注释的重要性以及基础知识。良好的变量命名和清晰的注释对于代码的理解至关重要。文章介绍了Python中变量的定义和常见用法,包括变量解包和单下划线变量名的特殊用法。此外,还探讨了在Python中给变量注明类型的重要性,以及变量命名原则,包括遵循PEP 8原则和描述性要强等。通过示例代码和解释,读者可以快速了解Python编程中变量与注释的基础知识,并学习如何写出易于理解的代码。文章还提到了变量命名的原则,包括描述性强的名字、尽量短的名字以及匹配类型的名字。此外,还介绍了匹配布尔值类型、int/float类型和其他类型的变量名的建议。文章还强调了在代码中明确标注变量类型的重要性。此外,还分享了一些给变量命名的小技巧,如避免出现多个相似的变量名以及尝试换词来简化复合变量名。文章还介绍了Python中的注释基础知识,包括单行注释和多行注释的使用方法。总的来说,本文内容涵盖了Python编程中变量与注释的重要性和基础知识,对于初学者快速入门具有一定的参考价值。文章还提到了Python之禅中的编程原则,强调了显式优于隐式的重要性。此外,还介绍了空行在代码中的作用,以及先写注释后写代码的好习惯。通过本文的总结,读者可以快速了解Python编程中变量与注释的重要性和基础知识,以及一些编程技巧和原则的应用。
2024-01-04给文章提建议
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Python 工匠:案例、技巧与工程实践》
《Python 工匠:案例、技巧与工程实践》
立即购买
登录 后留言
精选留言
由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论
[1]:“贪婪”一词在计算机领域具有特殊含义。比方说,某个行为要捕获一批对象,它既可以选择捕获 1 个,也可以选择捕获 10 个,两种做法都合法,但它总是选择结果更多的那种:捕获 10 个,这种行为就称得上是“贪婪”。
[2]:具体来说,针对变量的类型注解语法是在 Python 3.6 版本引入的,而 3.5 版本只支持注解函数参数。
[3]:IDE 是 integrated development environment(集成开发环境)的缩写,在满足代码编辑的基本需求外,IDE 通常还集成了许多方便开发者的功能。常见的 Python IDE 有 PyCharm、VS Code 等。
[4]:相比之下,类型注解在开源领域的接受度更高一些,许多流行的 Python 开源项目(比如 Web 开发框架 Flask 和 Tornado 等),早早地给代码加上了类型注解。
[5]:世界上规模最大的开源项目源码托管网站。
[6]:“自说明”是指代码在命名、结构等方面都非常规范,可读性强。读者无须借助任何其他资料,只通过阅读代码本身就能理解代码意图。