Python 工匠:善用变量来改善代码质量 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
piglei
V2EX    Python

Python 工匠:善用变量来改善代码质量

  •  
  •   piglei
    piglei 2016-07-13 11:19:41 +08:00 6475 次点击
    这是一个创建于 3440 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址: http://www.zlovezl.cn/articles/python-using-variables-well/


    『 Python 工匠』是什么?

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

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

    『 Python 工匠』这个系列文章,是我的一次小小尝试。它专注于分享 Python 编程中的一些偏『小』的东西。希望能够帮到每一位编程路上的匠人。

    变量和代码质量

    作为『 Python 工匠』系列文章的第一篇,我想先谈谈 『变量( Variables )』。因为如何定义和使用变量,一直都是学习任何一门编程语言最先要掌握的技能之一。

    变量用的好或不好,和代码质量有着非常重要的联系。在关于变量的诸多问题中,为变量起一个好名字尤其重要。

    如何为变量起名

    在计算机科学领域,有一句著名的格言(俏皮话):

    There are only two hard things in Computer Science: cache invalidation and naming things. 在计算机科学领域只有两件难事:缓存过期 和 给东西起名字

    -- Phil Karlton

    第一个『缓存过期问题』的难度不用多说,任何用过缓存的人都会懂。至于第二个『给东西起名字』这事的难度,我也是深有体会。在我的职业生涯里,度过的作为黑暗的下午之一,就是坐在显示器前抓耳挠腮为一个新项目起一个合适的名字。

    编程时起的最多的名字,还数各种变量。给变量起一个好名字很重要,因为好的变量命名可以极大的提高代码整体可读性。

    下面几点,是我总结的为变量起名时,最好遵守的基本原则。

    1. 变量名要有描述性,不能太宽泛

    可接受的长度范围内,变量名能把它所指向的内容描述的越精确越好。所以,尽量不要用那些过于宽泛的词来作为你的变量名:

    • GOOD: day_of_week, hosts_to_reboot, expired_cards
    • BAD: day, host, cards, temp

    2. 变量名最好让人能猜出类型

    老司机们都知道, Python 是一门动态类型语言,它(至少在 PEP 484 出现前)没有变量类型声明。所以当你看到一个变量时,除了通过上下文猜测,没法轻易知道它是什么类型。

    不过,人们对于变量名和变量类型的关系,通常会有一些直觉上的约定,我把它们总结在了下面。

    『什么样的名字会被当成 bool 类型?』

    布尔类型变量的最大特点是:它只存在两个可能的值『是』『不是』。所以,用 ishas 等非黑即白的词修饰的变量名,会是个不错的选择。原则就是:让读到变量名的人觉得这个变量只会有『是』或『不是』两种值

    下面是几个不错的示例:

    • is_superuser:『是否超级用户』,只会有两种值:是 /不是
    • has_error:『有没有错误』,只会有两种值:有 /没有
    • allow_vip:『是否允许 VIP 』,只会有两种值:允许 /不允许
    • use_msgpack:『是否使用 msgpack 』,只会有两种值:使用 /不使用
    • debug:『是否开启调试模式』,被当做 bool 主要是因为约定俗成

    『什么样的名字会被当成 int/float 类型?』

    人们看到和数字相关的名字,都会默认他们是 int/float 类型,下面这些是比较常见的:

    • 释义为数字的所有单词,比如:port (端口号)age (年龄)radius (半径) 等等
    • 使用 _id 结尾的单词,比如:user_idhost_id
    • 使用 length/count 开头或者结尾的单词,比如: length_of_usernamemax_lengthusers_count

    注意:不要使用普通的复数来表示一个 int 类型变量,比如 applestrips,最好用 number_of_applestrips_count 来替代。

    其他类型

    对于 str 、 list 、 tuple 、 dict 这些复杂类型,很难有一个统一的规则让我们可以通过名字去猜测变量类型。比如 headers,既可能是一个头信息列表,也可能是包含头信息的 dict 。

    对于这些类型的变量名,最推荐的方式,就是编写规范的文档,在函数和方法的 document string 中,使用 sphinx 格式(Python 官方文档使用的文档工具)来标注所有变量的类型。

    3. 适当使用『匈牙利命名法』

    第一次知道『匈牙利命名法』,是在 Joel on Software 的一篇博文中。简而言之,匈牙利命名法就是把变量的『类型』缩写,放到变量名的最前面。

    关键在于,这里说的变量『类型』,并非指传统意义上的 int/str/list 这种类型,而是指那些和你的代码业务逻辑相关的类型。

    比如,在你的代码中有两个变量:studentsteachers,他们指向的内容都是一个包含 Person 对象的 list 。使用『匈牙利命名法』后,可以把这两个名字改写成这样:

    students -> pl_students teachers -> pl_teachers

    pl 是 person list 的首字母缩写。变量名被加上前缀后,当你看到以 pl_ 打头的变量时,就能知道它所指向的值类型了。

    很多情况下,使用『匈牙利命名法』是一个不错的注意,它可以改善你的代码可读性,尤其在那些变量众多、同一类型多次出现时。注意不要滥用就好。

    4. 变量名尽量短,但是绝对不要太短

    在前面,我们提到要让变量名有描述性。如果不给这条原则加上任何限制,那么你很有可能写出这种描述性极强的变量名:how_much_points_need_for_level2。如果代码中充斥着这种过长的变量名,对于代码可读性来说是个灾难。

    一个好的变量名,长度应该控制在 两到三个单词左右。比如上面的名字,可以缩写为 points_level2

    绝大多数情况下,都应该避免使用那些只有一两个字母的短名字,比如数组索引三剑客 ijk,用有明确含义的名字,比如 persion_index 来代替它们总是会更好一些。

    使用短名字的例外情况

    有时,不能使用短名字的原则也会有一些例外。当一些意义明确但是较长的变量名重复出现时,为了让代码更简洁,使用短名字缩写是完全可以的。但是为了降低理解成本,同一段代码内最好不要使用太多这种短名字。

    比如在 Python 中导入模块时,就经常用到短名字作为别名,像 Django i18n 翻译时常用的 gettext 方法通常会被缩写成 _ 来使用( from django.utils.translation import ugettext as _)

    5. 其他注意事项

    其他一些给变量命名的注意事项:

    • 同一段代码内不要使用过于相似的变量名,比如同时出现 usersusers1user3 这种序列
    • 不要使用带否定含义的变量名,用 is_special 代替 is_not_normal

    更好的使用变量

    前面讲了如何为变量取一个好名字,下面我们谈谈在日常使用变量时,应该注意的一些小细节。

    1. 保持一致性

    如果你在一个方法内里面把图片变量叫做 photo,在其他的地方就不要把它改成 image,这样只会让代码的阅读者困惑:『imagephoto 到底是不是同一个东西?』

    另外,虽然 Python 是动态类型语言,但那也不意味着你可以用同一个变量名一会表示 str 类型,过会又换成 list 。同一个变量名指代的变量类型,也需要保持一致性。

    2. 尽量不要用 globals()/locals()

    也许你第一次发现 globals()/locals() 这对内建函数时很兴奋,迫不及待的写下下面这种极端『简洁』的代码:

    def render(request, user_id, trip_id): user = User.objects.get(id=user_id) trip = get_object_or_404(Trip, pk=trip_id) is_suggested = is_suggested(user, trip) # 利用 locals() 节约了三行代码,我是个天才! return render(request, 'trip.html', locals()) 

    千万不要这么做,这样只会让读到这段代码的人(包括三个月后的你自己)痛恨你,因为他需要记住这个函数内定义的所有变量(想想这个函数增长到两百行会怎么样?),更别提 locals() 还会把一些不必要的变量传递出去。

    更何况, The Zen of Python ( Python 之禅) 说的清清楚楚:Explicit is better than implicit.(显式优于隐式)。还是老老实实把代码改成这样吧:

     return render(request, 'trip.html', { 'user': user, 'trip': trip, 'is_suggested': is_suggested }) 

    3. 变量定义尽量靠近使用

    这个原则属于老生常谈了。很多人(包括我)在刚开始学习编程时,会有一个习惯。就是把所有的变量定义写在一起,放在函数或方法的最前面。

    def generate_trip_png(trip): path = [] markers = [] photo_markers = [] text_markers = [] marker_count = 0 point_count = 0 ... ... 

    这样做只会让你的代码『看上去很整洁』,但是对提高代码可读性没有任何帮助。

    更好的做法是,让变量定义尽量靠近使用。那样当你阅读代码时,可以更好的理解代码的逻辑,而不是费劲的去想这个变量到底是什么、哪里定义的?

    4. 合理使用 dict 来让函数返回多个值

    Python 的函数可以返回多个值:

    def latlon_to_address(lat, lon): return country, province, city # 利用多返回值一次定义多个变量 country, province, city = latlon_to_address(lat, lon) 

    但是,这样的用法会产生一个小问题:如果某一天, latlon_to_address 函数需要返回『城区( District )』时怎么办?

    如果是上面这种写法,你需要找到所有调用 latlon_to_address 的地方,补上多出来的这个变量,否则 ValueError: too many values to unpack 就会找上你:

    country, province, city, district = latlon_to_address(lat, lon) # 或者忽略多出来的返回值 country, province, city, _ = latlon_to_address(lat, lon) 

    对于这种多返回值可能会变动的情况,使用 dict 作为返回值会更方便一些,当你新增返回值时,不会对之前的函数调用产生任何破坏性的影响:

    def latlon_to_address(lat, lon): return { 'country': country, 'province': province, 'city': city } addr_dict = latlon_to_address(lat, lon) 

    这样做的坏处也有,代码兼容性虽然增加了,但是你不能继续用之前 x, y = f() 的方式一次定义多个变量了。取舍在于你自己。

    5. 控制单个函数内的变量数量

    人脑的能力是有限的,研究表明,人类的短期记忆只能同时记住不超过十个名字。所以,当你的某个函数过长(一般来说,超过一屏的的函数就会被认为有点过长了),包含了太多变量时。请及时把它拆分为多个小函数吧。

    6. 及时删掉那些没用的变量

    这条原则非常简单,也很容易做到。但是如果没有遵守,那它对你的代码质量的打击是毁灭级的。会让阅读你代码的人有一种被愚弄的感觉。

    def fancy_func(): # 读者心理:嗯,这里定义了一个 fancy_vars fancy_vars = get_fancy() ... ...(一大堆代码过后) # 读者心理:这里就结束了?之前的 fancy_vars 去哪了?被猫吃了吗? return result 

    所以,请打开 IDE 的智能提示,及时清理掉那些定义了但是没有使用的变量吧。

    7. 能不定义变量就不定义

    有时候,我们定义变量时的心理活动是这样的:『嗯,这个值未来说不定会修改 /二次使用』,让我们先把它定义成变量吧!

    def get_best_trip_by_user_id(user_id): user = get_user(user_id) trip = get_best_trip(user_id) result = { 'user': user, 'trip': trip } return result 

    其实,你所想的『未来』永远不会来,这段代码里的三个临时变量完全可以去掉,变成这样:

    def get_best_trip_by_user_id(user_id): return { 'user': get_user(user_id), 'trip': get_best_trip(user_id) } 

    没有必要为了那些可能出现的变动,牺牲代码当前的可读性。如果以后有定义变量的需求,那就以后再加吧。

    结语

    碎碎念了一大堆,不知道有多少人能够坚持到最后。变量作为程序语言的重要组成部分,值得我们在定义和使用它时,多花一丁点时间思考一下,那样会让你的代码变得更优秀。

    这是『 Python 工匠』系列文章的第一篇,不知道看完文章的你,有没有什么想吐槽的?请留言告诉我吧。

    38 条回复    2018-04-18 15:38:50 +08:00
    mornlight
        1
    mornlight  
       2016-07-13 12:03:15 +08:00
    看了遍,我产生了一个问题,为什么「缓存过期」是个难题?

    另外,对匈牙利命名法存疑,我一般用 teacher_list 而不用 pl_teachers ,前缀需要多一步思考来明确代表什么。
    feiyuanqiu
        2
    feiyuanqiu  
       2016-07-13 12:33:02 +08:00 via iPhone
    不针对这帖,只想说这些代码大全早都总结过了的东西,为什么总还是有人一遍遍地重复发文章来讲呢
    piglei
        3
    piglei  
    OP
       2016-07-13 13:13:21 +08:00
    @mornlight 因为缓存过期确实就是有辣么难啊,
    piglei
        4
    piglei  
    OP
       2016-07-13 13:16:51 +08:00   1
    @mornlight 没注意,上条评论直接发出去了,类型缩写放前面的好处是变量名字都是靠左对齐的,人的阅读顺序也是从左向右,当多个长度不一的变量名一起出现时,可读性比类型在后面要好一些。当然,类型在后面更符合人的阅读习惯。
    9hills
        5
    9hills  
       2016-07-13 13:18:48 +08:00 via Android
    还不错
    piglei
        6
    piglei  
    OP
       2016-07-13 13:19:31 +08:00
    @feiyuanqiu 谢谢你的反馈。怎么说呢,最早写这个文章,主要是想针对用 Python 编程时写一些变量相关的事情。但写的时候,又没忍住加了一些正如你所说老生常谈的那套东西。以后我会尝试针对性更强一些。
    shyling
        7
    shyling  
       2016-07-13 13:32:33 +08:00 via iPad
    看了整篇。。。难道做好封装不重要?
    hqingyi
        8
    hqingyi  
       2016-07-13 13:40:41 +08:00
    总结的挺好的,对于“能不定义变量就不定义”这点还是有点小看法的,有时候抽取出变量的目的就是为了提高代码的可读性呃
    skydiver
        9
    skydiver  
       2016-07-13 13:44:28 +08:00
    什么年代了还匈牙利命名法……
    romoo
        10
    romoo  
       2016-07-13 14:06:52 +08:00
    @piglei 你也开始用 IDE 了么
    piglei
        11
    piglei  
    OP
       2016-07-13 14:19:12 +08:00
    @romoo 并没有啊,只是写文时说 IDE 方便点,嘿嘿。
    不过我现在不是 vim 党了,我现在是 neovim 党。
    piglei
        12
    piglei  
    OP
       2016-07-13 14:21:20 +08:00
    @skydiver 咋说呢,我觉得还是有一定用处的,尤其是当项目用到 pandas 、 matplotlib 、 wxpython 这种控件 /类型特别多的东西时。
    piglei
        13
    piglei  
    OP
       2016-07-13 14:24:07 +08:00
    @shyling 我好像没有提到封装相关的事情呀?

    @hqingyi 是的,如果使用变量的目的是为了提高可读性,完全是应该提倡的。我所指的只是那种没有必要的定义,比如下一行就用到的一个**能简单得到的值**,就没有必要再为其定义一个临时变量了。
    tscat
        14
    tscat  
       2016-07-13 14:29:30 +08:00 via iPhone
    写的不错,但是我觉得 I j k 用起来已经很约定俗成了吧。
    piglei
        15
    piglei  
    OP
       2016-07-13 14:39:57 +08:00
    @tscat 确实是,这是我能够接受使用极短变量名的情况之一:约定俗成。写文的时候把它们拉出来只是因为当时脑子里一下就冒出它们来了,只怪它们三兄弟太魔性了。 :)
    shyling
        16
    shyling  
       2016-07-13 14:44:31 +08:00 via iPad
    @shyling 不是。。。我是觉得有些长变量名长方法 /函数名就是封装的问题。。。
    piglei
        17
    piglei  
    OP
       2016-07-13 14:48:52 +08:00
    @shyling 嗯,说白了,是不是就是指代码所在上下文环境过于混乱 /复杂,促生了那些过长的名字。
    piglei
        18
    piglei  
    OP
       2016-07-13 14:49:22 +08:00
    @shyling 手一抖,又发出去了。不知道我是不是 get 到了你的点。
    shyling
        19
    shyling  
       2016-07-13 15:35:41 +08:00 via iPad
    CharlesL
        20
    CharlesL  
       2016-07-13 16:35:11 +08:00
    最近初学 python ,主要是统计日志相关的数据,读 python 代码的时候,包括自己写的时候,如果变量名字定义的比较随便,就不太容易理解变量的类型是什么,得仔细看上下文。这段时间写 python 代码,养成了个习惯, list 类型的在变量后边加个 list ,如 user_id_list , tuple 的加 tuple , dict 类型的加 dict ,数字类型的加 num 或 count 。
    还有一个地方, list 里边存储 list 或者 tuple 、 dict 里边放 list ,这种多种类型嵌套的时候,读写数据的时候老是搞错。。。
    piglei
        21
    piglei  
    OP
       2016-07-13 16:41:42 +08:00
    @CharlesL 你这算是自己定义了一套变量名规则来尝试弥补 Python 缺失的 type hint 功能啦(和我提到的『匈牙利命名法』思路类似),如果项目一直都是你一个人在写,它能解决问题,也算是不错的办法。不过如果项目成员较多,推行这种命名模式就比较麻烦一些。

    有空可以读读 [PEP 484]( https://www.python.org/dev/peps/pep-0484/),这个主要就是想解决你所碰到的问题。
    jmc891205
        22
    jmc891205  
       2016-07-13 16:46:06 +08:00   1
    我会用 ii, jj, kk 代替 i, j, k
    否则想在编辑器里搜索 i, j, k 的时候真是一场灾难。。。
    piglei
        23
    piglei  
    OP
       2016-07-13 17:39:12 +08:00
    @jmc891205 你可以搜索正则 '\bi\b',用 \b 表示单词边界,我一般都这么干。
    alphadog619
        24
    alphadog619  
       2016-07-13 17:48:25 +08:00
    谢谢!
    lll9p
        25
    lll9p  
       2016-07-13 17:53:59 +08:00
    还有不要写一行式代码,昨天被自己坑了
    piglei
        26
    piglei  
    OP
       2016-07-13 19:35:22 +08:00 via iPhone
    @lll9p 嗯,毕竟 readability counts 可读性第一。
    zhicheng
        27
    zhicheng  
       2016-07-13 20:20:57 +08:00 via Android
    1. 不要返回“是否错误”的错误,而是直接返回错误。这是常识。
    2. allow_vip 存在歧义,至少是 allow_vip_xxx , xxx 是动词,不过一般 ACL 是个矩阵,所以很少会出现这样的定义。
    pppy
        28
    pppy  
       2016-07-13 20:21:02 +08:00
    我觉得总结的挺好,虽然想《编写可读性代码》、《代码大全》以及《代码整洁之道》也有讲。但是温故而知新,
    总会遗忘,多看看,多写写,就会融入到每一行代码里(《编写可读性代码》我就读了好几遍
    piglei
        29
    piglei  
    OP
       2016-07-13 21:07:57 +08:00 via iPhone
    @zhicheng 您挺好玩的,我不爱和人抬杠。您就当我是一个不懂异常“常识”的人吧。
    zhicheng
        30
    zhicheng  
       2016-07-13 21:41:13 +08:00
    @piglei 那您就当我是一个不懂异常和你“抬杠”的人吧。
    mo2mo
        31
    mo2mo  
       2016-07-13 22:00:25 +08:00
    写得挺好的,赞一个
    Garantion
        32
    Garantion  
       2016-07-13 22:50:05 +08:00
    写的很好~支持一下~
    markocen
        33
    markocen  
       2016-07-14 02:58:38 +08:00
    编程两大世界难题: Naming things and Cache invalidation
    jmc891205
        34
    jmc891205  
       2016-07-14 11:11:56 +08:00
    @piglei vi 的查找好像不支持\b ?
    piglei
        35
    piglei  
    OP
       2016-07-14 11:44:01 +08:00   1
    @jmc891205 vi 这样搜单词边界 `:/\<i\>`
    jmc891205
        36
    jmc891205  
       2016-07-14 14:41:36 +08:00
    @piglei 谢啦~
    jdz100
        37
    jdz100  
       2016-07-18 16:42:50 +08:00
    有收获,谢谢!
    data2world
        38
    data2world  
       2018-04-18 15:38:50 +08:00
    @jmc891205 这样一样很让人困惑,,不知道你怎么想的,,,
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2749 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 41ms UTC 06:27 PVG 14:27 LAX 22:27 JFK 01:27
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86