
python工匠学习笔记(变量与注释)
1.1基础知识
1.1.1变量常见用法
01变量解包
1 | # 变量解包(unpacking)是Python里的一种特殊赋值操作,允许我们把一个可迭代对象(比如列表)的所有成员,一次性赋值给多个变量: |
1 | # 和常规的切片赋值语句比起来,动态解包语法要直观许多,上面的例子可以完全等价为 |
1 | # 解包操作也可以在任何循环语句里使用 |
02单下划线变量名
1 | '''在常用的诸多变量名中,单下划线是比较特殊的一个。它常作为一个无意义的占位符出现在赋值语句中。 |
1.1.2 给变量注明类型
由于Python是动态类型语言,使用变量时不需要做任何类型声明。这是Python相比其他语言的一个重要优势:它减少了我们的心智负担,让写代码变得更容易。尤其对于许多编程新手来说,“不用声明类型”无疑会让学Python这件事变得简单很多。但任何事物都有其两面性。动态类型所带来的缺点是代码的可读性会因此大打折扣。
为了解决动态类型带来的可读性问题,最常见的办法就是在函数文档(docstring)里做文章。我们可以把每个函数参数的类型与说明全都写在函数文档里。
下面是增加了Python官方推荐的Sphinx格式文档后的效果:
1 | def remove invalid(items): |
在上面的函数文档里,type items:注明了items是个整型列表。任何人只要读到这份文档,马上就能知道参数类型,不用再猜来猜去了。
当然,标注类型的办法肯定不止上面这一种。在Python3.5版本2以后,你可以用类型注解功能来直接注明变量类型。相比编写 Sphinx格式文档,我其实更推荐使用类型注解,因为它是Python的内置功能,而且正在变得越来越流行。
1 | #要使用类型注解,只需在变量后添加类型,并用冒号隔开即可比如func(value:str)表示函数的value参数为字符串类型。下面是给remove_invalid)函数添加类型注解后的样子: |
1.1.3 变量命名原则
01遵循PEP8原则
给变量起名主要有两种流派:
- 一是通过大小写界定单词的驼峰命名派CamelCase
- 二是通过下划线连接的蛇形命名派 snake case。
这两种流派没有明显的优劣之分,似乎与个人喜好有关。为了让不同开发者写出的代码风格尽量保持统一,Python制定了官方的编码风格指南:PEP8。这份风格指南里有许多详细的风格建议,比如应该用4个空格缩进,每行不超过79个字符,等等。其中,当然也包含变量的命名规范:
- a.对于普通变量,使用蛇形命名法,比如max_value;
- b.对于常量,采用全大写字母,使用下划线连接,比如 MAX VALUE;
- c.如果变量标记为“仅内部使用”,为其增加下划线前缀,比如local_var;
- d.当名字与Python关键字冲突时,在变量末尾追加下划线,比如class_。
- 除变量名以外,PEP8中还有许多其他命名规范,比如类名应该使用驼峰风格(FooClass)、函数应该使用蛇形风格(bar function),等等。给变量起名的第一条原则,就是一定要在格式上遵循以上规范。
02描述性要强
写作过程中的一项重要工作,就是为句子斟酌恰当的词语。不同词语的描述性强弱不同,比如“冬天的梅花”就比“花”的描述性更强。而变量名和普通词语一样,同样有描述性强弱之分假如代码大量使用描述性弱的变量名,读者就很难理解代码的含义。
述性弱的名字:看不懂在做什么 value process(s.strip())
描述性强的名字:尝试从用户输入里解析出一个用户名 usemname=extract usemame(input_string.strip())所以,在可接受的长度范围内,变量名所指向的内容描述得越精确越好
03要尽量简短
刚刚说到,变量名的描述性要尽量强,但描述性越强,通常名字也就越长。
假如不加思考地实践“描述性原则”,那你的代码里可能会充斥着how_many_points_.needed_for_user_level3这种名字,简直像条蛇一样长:
1 | def upgrade to level3(user): |
假如一个特别长的名字重复出现,读者不会认为它足够精确,反而会觉得啰俊难读。既然如此,怎么才能在保证描述性的前提下,让名字尽量简短易读呢?我认为个中诀窍在于:为变量命名要结合代码情境和上下文。
比如在上面的代码里,upgrade to levels3(user)函数已经通过自己的名称、文档表明了其目的,那在函数内部,我们完全可以把how_many_points_.needed_for_user_level:3直接删减成 level3_points。
04要匹配类型
- a.匹配布尔值类型的变量名布尔值(boo)是一种很简单的类型,它只有两个可能的值:“是”(True)或“不是”(False)。因此,给布尔值变量起名有一个原则:一定要让读到变量的人觉得它只有“肯定”和“否定”两种可能。举例来说,is、has这些非黑即白的词就很适合用来修饰这类名字。
- b.匹配int/loat类型的变量名当人们看到和数字有关的名字时,自然就会认定它们是int或float类型。这些名字可简单分为以下几种常见类型:
- a.释义为数字的所有单词,比如pot(端口号)、 age(年龄)、radius(半径)等;
- b.使用以id结尾的单词,比如user id、host id;
- c.使用以length/count开头或者结尾的单词,比如 length of username、max length、users count.
- c.匹配其他类型的变量名至于剩下的字符串(str)、列表(Iist)、字典(dict)等其他值类型,我们很难归纳出一个“由名字猜测类型”的统一公式。拿headers这个名字来说,它既可能是一个装满头信息的列表(List[Header]),也可能是一个包含头信息的字典(Dict[str,Header])。
05超短名
在众多变量名里,有一类非常特别,那就是只有一两个字母的短名字。这些短名字一般可分为两类,一类是那些大家约定俗成的短名字,比如:
- a.数组索引三剑客i、jk
- b.某个整数n
- c.某个字符串s
- d.某个异常e
- e.文件对象fp
我并不反对使用这类短名字,我自己也经常用,因为它们写起来的确很方便。但如果条件允许,建议尽量用更精确的名字替代。比如,在表示用户输入的字符串时,用input str替代s会更明确一些。另一类短名字,则是对一些其他常用名的缩写。比如,在使用Diango框架做国际化内容翻译时,常常会用到gettext方法。为了方便,我们常把gettext缩写成_:如果你的项目中有一些长名字反复出现,可以效仿上面的方式,为它们设置一些短名字作为别名。这样可以让代码变得更紧凑、更易读。但同一个项目内的超短缩写不宜太多,否则会适得其反。1
2from django.utils.translation import gettext as _
print(_('待翻译文字'))
06其他技巧
- 在同一段代码内,不要出现多个相似的变量名,比如同时使用users、users1、users:3这种序列
- 可以尝试换词来简化复合变量名,比如用is_special来代替is_not_normal
- 如果你苦思冥想都想不出一个合适的名字,请打开 GitHub,到其他人的开源项目里找找灵感吧!
1.1.4注释基础知识
注释(comment)是代码非常重要的组成部分。通常来说,注释泛指那些不影响代码实际行为的文字,它们主要起额外说明作用。 Python里的注释主要分为两种,一种是最常见的代码内注释,通过在行首输入#号来表示:
1 | #用户输入可能会有空格,使用strip去掉空格 |
当注释包含多行内容时,同样使用#号:
1 | #使用strip(0去掉空格的好处: |
除使用#的注释外,另一种注释则是我们前面看到过的函数(类)文档(docstring),这些文档也称接口注释(interface comment)
1 | class Person: |
接口注释有好几种流行的风格,比如Sphinx文档风格、Google风格等,其中Sphinx文档风格目前应用得最为广泛。上面的Person类的接口注释就属于Sphinx文档风格。虽然注释一般不影响代码的执行效果,却会极大地影响代码的可读性。在编写注释时,编程新手们常常会犯同类型的错误,以下是我整理的最常见的3种:
错误1.用注释屏蔽代码
有时,人们会把注释当作临时屏蔽代码的工具。当某些代码暂时不需要执行时,就把它们都注释了,未来需要时再解除注释。
1 | #源码里有大段大段暂时不需要执行的代码 |
其实根本没必要这么做。这些被临时注释掉的大段内容,对于阅读代码的人来说是一种干扰,没有任何意义。对于不再需要的代码,我们应该直接把它们删掉,而不是注释掉。如果未来有人真的需要用到这些旧代码,他直接去Gt仓库历史里就能找到,毕竟版本控制系统就是专门干这个的。
错误2.用注释复述代码
在编写注释时,新手常犯的另一类错误是用注释复述代码。就像这样:
1 | #调用strip()去掉空格 |
上面代码里的注释完全是冗余的,因为读者从代码本身就能读到注释里的信息。好的注释应该像下面这样:
1 | #如果直接把带空格的输入传递到后端敖处理,可能会造成后端服务崩溃 |
注释作为代码之外的说明性文字,应该尽量提供那些读者无法从代码里读出来的信息。描述代码为什么要这么做,而不是简单复述代码本身。
除了描述“为什么”的解释性注释外,还有一种注释也很常见:指引性注释。这种注释并不直接复述代码,而是简明扼要地概括代码功能,起到“代码导读”的作用。比如,以下代码里的注释就属于指引性注释:
1 | #初始化访问服务的client对象 |
指引性注释并不提供代码里读不到的东西假如没有注释,耐心读完所有代码,你也能知道代码做了什么事儿。指引性注释的主要作用是降低代码的认知成本,让我们能更容易理解代码的意图。
在编写指引性注释时,有一点需要注意,那就是你得判断何时该写注释,何时该将代码提炼为独立的函数(或方法)。比如上面的代码,其实可以通过抽象两个新函数改成下面这样:
1 | service_client = make_client() |
这么改以后,代码里的指引性注释就可以删掉了,因为有意义的函数名已经达到了概括和指引的作用。正是因为如此,一部分人认为:只要代码里有指引性注释,就说明代码的可读性不高,无法“自说明”,一定得抽象新函数把其优化成第二种样子。
但我倒认为事情没那么绝对。无论代码写得多好,多么“自说明”,同读代码相比,读注释通常让人觉得更轻松。注释会让人们觉得亲切(尤其当注释是中文时),高质量的指引性注释确实会让代码更易读。有时抽象一个新函数,不见得就一定比一行注释加上几行代码更好。
错误3.弄错接口注释的受众
在编写接口注释时,人们有时会写出下面这样的内容:
1 | def resize_image(image,size): |
上面这段注释虽然有些夸张,但像它一样的注释在项目中其实并不少见。这段接口注释最主要的问题在于过多阐述了函数的实现细节,提供了太多其他人并不关心的内容。
接口文档主要是给函数(或类)的使用者看的,它最主要的存在价值,是让人们不用逐行阅读函数代码,也能很快通过文档知道该如何使用这个函数,以及在使用时有什么注意事项。在编写接口文档时,我们应该站在函数设计者的角度,着重描述函数的功能、参数说明等。而函数自身的实现细节,比如调用了哪个第三方模块、为何有性能问题等,无须放在接口文档里。对于上面的resize_image()函数来说,文档里提供以下内容就足够了:
1 | def resize_image(image,size): |
至于那些使用了Pilot模块、为何有内存问题的细节说明,全都可以丢进函数内部的代码注释里。
1.2案例故事
下面是Python程序员小R去其他公司面试的故事:奇怪的冒泡排序算法
上午10点,在T公司的会议室里,小R正在参加一场他准备了好几天的技术面试。整体来说,他在这场面试中的表现还不错。无论坐在小R对面的面试官提出什么问题,他都能侃侃而谈、对答如流。从单体应用聊到微服务,从虚拟机聊到云计算,每一块小R都说得滴水不漏。就在他认为自己胜券在握,可以通过这家自己憧憬已久的公司面试时,对面的面试官突然说道:“技术问题我问得差不多了。最后有一道编程题,希望你可以做一下。”
题目:冒泡排序算法
请用Python语言实现冒泡排序算法,把较大的数字放在后面。注意:默认所有的偶数都比奇数大。
1 | >>>numbers=[23,32,1,3,4,19,20,2,4] |
“冒泡排序,这不是所有排序算法里最简单的一种吗?虽然加了一点儿变化,但看起来没有什么难度啊。”小R一边在心里这么想着,一边打开编辑器开始写代码。五分钟后,他把笔记本电脑递给面试官并说道:“写完了!”
小R写的冒泡排序函数:
1 | def magic_bubble_sort(numbers): |
这段代码没有任何多余的逻辑,可以通过所有的测试用例。面试官看着小R演示完函数功能后,盯着代码似乎想说点儿什么,但最后只是微微点了点头,说:“好,今天的面试就到这儿吧,有后续面试我再通知你。“
小R高高兴兴地回到家,一心觉得这次面试稳了,可没想到他后来却再也没接到任何后续面试的通知。
问题出在哪里究竟是哪里出了问题呢?小R思来想去,觉得自己回答问题时表现挺好,最有可能出问题的是最后一道编程题,肯定是漏掉了什么边界情况没处理。于是他找到一位有着十年编程经验的前辈小Q,凭着记忆把题目和自己的答案还原给对方看。题目大概就是这样,这是我当时写的代码。Q哥,你帮忙看看,我是不是有什么情况没考虑到?”小R问道。
小Q盯着他写的代码,足足两分钟没说一句话,然后突然开口道:“小R啊,你这个函数功能实现得没毛病,就是实在太难看懂了。”“总共就10行代码。难看懂?怎么会呢?”小R在心里泛起了嘀咕。这时,前辈小Q说道:“这样,你把笔记本电脑给我,我来给你稍微改改这段代码,然后你再看看。”三分钟后,小Q把修改过的代码递了过来:
1 | # 小Q修改后的冒泡排序函数 |
小R盯着这段代码,发现它的核心逻辑和之前没有任何不同。但不知为何,这段代码看上去就是比自己写的代码更舒服。小R若有所思,好像一下明白了自己没通过面试的原因。故事讲完了。看上去,前辈小Q只是在小R的代码之上做了些“无关痛痒”的改动,但正是这些“无关痛痒”的改动,改善了代码的观感,提升了整个函数的可读性。
“无关痛痒”的改动和小R写的代码相比,前辈小Q的新代码主要进行了以下改进:
- 变量名变成了可读的、有意义的名字,比如在旧代码里,”停止位”是无意义的j,新代码里变成了stop_position。
- 增加了有意义的临时变量,比如current/next代表前一个/后一个元素、{}_is_even代表元素是否为偶数、 should_swap代表是否应该交换元素。
- 多了一点儿恰到好处的指引性注释,比如说明交换元素顺序的详细条件。
这些变化让整段代码变得更易读,也让整个算法变得更好理解。所以,哪怕是一段不到10行代码的简单函数,对变量和注释的不同处理方式,也会让代码发生质的变化。
1.3编程建议
1.3.1 保持变量的一致性
在使用变量时,你需要保证它在两个方面的一致性:名字一致性与类型一致性。名字一致性是指在同一个项目(或者模块、函数)中,对一类事物的称呼不要变来变去。如果你把项目里的“用户头像”叫作 user_avatar_url,那么在其他地方就别把它改成user_profile_url。否则会让读代码的人犯迷糊:“user_avatar_url和user_profile_url到底是不是一个东西?”类型一致性则是指不要把同一个变量重复指向不同类型的值,举个例子:
1 | def foo(): |
在foo()函数的作用域内,users变量被使用了两次:第一次指向字典,第二次则变成了列表。虽然Python的类型系统允许我们这么做,但这样做其实有很多坏处,比如变量的辨识度会因此降低,还很容易引入bug。
1.3.2 变量定义尽量靠近使用
包括我自己在内的很多人在初学编程时有一种很不好的习惯喜欢把所有变量初始化定义写在一起,放在函数最前面,就像下面这样:
1 | def generate_trip_png(trip): |
之所以这么写代码,是因为我们觉得“初始化变量”语句是类以的,应该将其归类到一起,放到最前面,这样代码会整洁很多。但是,这样的代码只是看上去整洁,它的可读性不会得到任何提升,反而会变差。在组织代码时,我们应该谨记:总是从代码的职责出发,而不是其他东西。比如,在上面的generate_trip_png()函数里,代码的职责主要分为三块:
- 初始化waypoints数据
- 处理markers数据
- 计算marker_count
那代码可以这么调整:
1 | def generate_trip_png(trip): |
通过把变量定义移动到每段“各司其职”的代码头部,大大缩短了变量从初始化到被使用的“距离”。当读者阅读代码时,可以更容易理解代码的逻辑,而不是来回翻阅代码,心想:“这个变量是什么时候定义的?是干什么用的?
1.3.3 定义临时变量提升可读性
随着业务逻辑变得复杂,我们的代码里也会经常出现一些复杂的表达式,就像下面这样:
1 | #为所有性别为女或者级别大于3的活跃用户发放10000个金币 |
看见if后面那一长串代码了吗?有点儿难读对不对?但这也没办法,毕竟产品经理就是明明白白这么跟我说的一业务罗辑如此。逻辑虽然如此,不代表我们就得把代码直白地写成这样。如果把后面的复杂表达式赋值为一个临时变量,代码可以变得更易读:
1 | #为所有性别为女或者级别大于3的活跃用户发放10000个金币 |
在新代码里,“计算用户合规的表达式”和“判断合规发送金币的条件分支”这两段代码不再直接杂糅在一起,而是添加了一个可读性强的变量user_is_elegible作为缓冲。不论是代码的可读性还是可维护性,都因为这个变量而增强了。
直接翻译业务逻辑的代码,大多不是好代码。优秀的程序设计需要在理解原需求的基础上,恰到好处地抽象,只有这样才能同时满足可读性和可扩展性方面的需求。抽象有许多种方式,比如定义新函数、定义新类型,“定义一个临时变量”是诸多方式里不太起眼的一个,但用得恰当的话效果也很巧妙。
1.3.4 同一作用域内不要有太多变量
通常来说,函数越长,用到的变量也会越多。但是人脑的记忆力是很有限的。研究表明,人类的短期记忆只能同时记住不超过10个名字。变量过多,代码肯定就会变得难读。
需要说明的一点是,大多数情况下,函数内变量的数量太多,通常意味着函数过于复杂,承担了太多职责。只有把复杂函数拆分为多个小函数,代码的整体复杂度才可能实现根本性的降低。
1.3.5 能不定义变量就别定义
前面提到过,定义临时变量可以提高代码的可读性。但有时,把不必要的东西赋值为临时变量,反而会让代码显得罗唆:
1 | def get_best_trip_by_user_id(user_id): |
在编写代码时,我们会下意识地定义很多变量,好为未来调整代码做准备。但其实,你所想的未来也许永远不会来。上面这段代码里的三个临时变量完全可以去掉,变成下面这样:
1 | def get best trip_by_user_id(user_id): |
这样的代码就像删掉赘语的句子,变得更精练、更易读。所以,不必为了那些未来可能出现的变动,栖牲代码此时此刻的可读性。如果以后需要定义变量,那就以后再做吧!
1.3.6 不要使用locals()
locals()是Python的一个内置函数,调用它会返回当前作用域中的所有局部变量:
1 | def foo(): |
在有些场景下,我们需要一次性拿到当前作用域下的所有(或绝大部分)变量,比如在渲染Django模板时:
1 | def render_trip_page(request,user_id,trip_id): |
看上去使用locals()函数正合适,假如调用locals(),上面的代码会简化许多:
1 | def render_trip_page(request,user_id,trip_id): |
第一眼看上去非常”简洁”,但是,这样的代码真的更好吗?答案并非如此。locals()看似简洁,但其他人在阅读代码时,为了搞明白模板渲染到底用了哪些变量,必须记住当前作用域里的所有变量。假如函数非常复杂,“记住所有局部变量”简直是个不可能完成的任务。使用locals()还有一个缺点,那就是它会把一些并没有真正使用的变量也一并暴露。因此,比起使用locals(),建议老老实实把代码写成这样:
1 | return render(request,'trip.html', { |
Python之禅:
b显式优于隐式在Python命令行中输入import this,你可以看到Tim Peters写的一段编程原则:The Zen of Python(“Python之禅”)。这些原则字字珠玑,里面蕴藏着许多Python编程智慧。“Python之禅”中有一句”Explicit is better than implicit’”(显式优于隐式),这条原则完全可以套用到 locals()的例子上Hocals(0实在是太隐晦了,直接写出变量名显然更好。
1.3.7 空行也是一种“注释”
代码里的注释不只是那些常规的描述性语句,有时候,没有一个字符的空行,也算得上一种特殊的“注释”。在写代码时,我们可以适当地在代码中插入空行,把代码按不同的逻辑块分隔开,这样能有效提升代码的可读性。
1.3.8 先写注释,后写代码
在编写了许多函数以后,我总结出了一个值得推广的好习惯:
先写注释,后写代码。每个函数的名称与接口注释(也就是docstring),其实是一种比函数内部代码更为抽象的东西。你需要在函数名和短短几行注释里,把函数内代码所做的事情,高度浓缩地表达清楚。
正因如此,接口注释其实完全可以当成一种协助你设计函数的前置工具。这个工具的用法很简单:假如你没法通过几行注释把函数职责描述清楚,那么整个函数的合理性就应该打一个问号。
举个例子,你在编辑器里写下了def process_.user(…):,准备实现一个名为process_user的新函数。在编写函数注释时,你发现在写了好几行文字后,仍然没法把process_user()的职责描述清楚,因为它可以同时完成好多件不同的事情。这时你就应该意识到,process_user()函数承担了太多职责,解决办法就是直接删掉它,设计更多单一职责的子函数来替代之。
先写注释的另一个好处是:不会漏掉任何应该写的注释。我常常在审查代码时发现,一些关键函数的docstring位置一片空白,而那里本该备注详尽的接口注释。每当遇到这种情况,我都会不厌其烦地请代码提交者补充和完善接口注释。
为什么大家总会漏掉注释?我的一个猜测是:程序员在编写函数时,总是跳过接口注释直接开始写代码。而当写完代码,实现函数的所有功能后,他就对这个函数失去了兴趣。这时,他最不愿意做的事,就是回过头去补写函数的接口注释,即便写了,也只是草草对付了事。
如果遵守“先写注释,后写代码”的习惯,我们就能完全避免上面的问题。要养成这个习惯其实很简单:在写出一句有说服力的接口注释前,别写任何函数代码。
1.4总结
在一段代码里,变量和注释是最接近自然语言的东西。因此,好的变量名、简明扼要的注释,都可以显著提升代码的质量。在给变量起名时,请尽量使用描述性强的名字,但也得注意别过了头。
从小R的面试故事来看,即使是两段功能完全一样的代码,也会因为变量和注释的区别,给其他人截然不同的感觉。因此,要想让你的代码给人留下“漂亮”的第一印象,请记得在变量和注释上多下功夫。
以下是本章要点知识总结。
- 变量和注释决定“第一印象”
- 变量和注释是代码里最接近自然语言的东西,它们的可读性非常重要
- 即使是实现同一个算法,变量和注释不一样,给人的感觉也会截然不同
- 基础知识
- Python的变量赋值语法非常灵活,可以使用*variables星号表达式灵活赋值
- 编写注释的两个要点:不要用来屏蔽代码,而是用来解释“为什么”
- 接口注释是为使用者而写,因此应该简明扼要地描述函数职责,而不必包含太多内部细节。
- 可以用Sphinx格式文档或类型注解给变量标明类型
- 变量名字很重要
- 给变量起名要遵循PEP8原则,代码的其他部分也同样如此
- 尽量给变量起描述性强的名字,但评价描述性也需要结合场景
- 在保证描述性的前提下,变量名要尽量短
- 变量名要匹配它所表达的类型
- 可以使用一两个字母的超短名字,但注意不要过度使用
- 代码组织技巧
- 按照代码的职责来组织代码:让变量定义靠近使用
- 适当定义临时变量可以提升代码的可读性
- 不必要的变量会让代码显得冗长、啰唆
- 同一个作用域内不要有太多变量,解决办法:提炼数据类、拆分函数
- 空行也是一种特殊的“注释”,适当的空行可以让代码更易读
- 代码可维护性技巧
- 保持变量在两个方面的一致性:名字一致性与类型一致性
- 显式优于隐式:不要使用locals()批量获取变量
- 把接口注释当成一种函数设计工具:先写注释,后写代码