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 · 5897 次点击
    这是一个创建于 3088 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址: 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   ·   实用小工具   ·   5078 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 03:53 · PVG 11:53 · LAX 19:53 · JFK 22:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.