摘要:为了让团队的代码风格保持一致,使得个人的代码便于团队维护,我们根据项目实际开发场景制定了本规范。

一、 概述

通过制定本规范,希望达成如下目标:

  1. 代码的可读性 即使在注释和文档不那么全面的情况下也可以知道代码的大概功能。
  2. 风格保持一致 团队的代码风格保持一致,使得个人的代码便于团队维护。
  3. 培养良好的编码习惯 避免犯一些简单但是又很难查的错误。

二、 细则

2.1. 空行和换行

  1. 用两行空行分割顶层函数和类的定义;
  2. 类内方法的定义用单个空行分割;
  3. 函数内逻辑无关段落之间空一行;
  4. if/for/while 语句中,即使执行语句只有一句,也必须另起一行;
  5. 不要将多句语句写在同一行,尽管使用 ; 合法;
  6. 单行代码最好不要超过 100 个字符,可以使用圆括号(推荐)或反斜杠续行。

    • 配置 PyCharm 显示列宽提示:File->Settings->Editor->Code Style->Right margin
    • 配置 VIM 显示列宽提示:set colorcolumn=100

反例:

def main():
    logger.info("Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions. And before you ask: It's BSD licensed!")
    if len(sys.argv) > 1 and sys.argv[1] == "Flask is a microframework for Python based on Werkzeug":
        pass

正例:

def main():
    logger.info("Flask is a microframework for Python based on Werkzeug, Jinja"
        " 2 and good intentions. And before you ask: It's BSD licensed!")
        # 注意为了避免容易和下面的缩进混淆,上面是2倍缩进
    if (len(sys.argv) > 1 and sys.argv[1] ==
            "Flask is a microframework for Python based on Werkzeug"):
        pass

2.2. 编码

文件保存为 utf8 编码格式(文件头只写 # -*- coding: utf-8 -*- 是不够的),便于中文注释的正确显示。

参考:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

2.3. 导入

2.3.1. 通常应该在单独的行中导入

No:

import os, sys

Yes:

import os
import sys

2.3.2. 尽量避免 from xxx import * 用法

2.3.3. Imports 通常被放置在文件的顶部

  1. 仅在模块注释和文档字符串之后,在模块的全局变量和常量之前;
  2. Imports 应该按序成组放置,且每组导入之间放置一个空行,每组最好按字母序排列:

    • 先标准库的导入
    • 再导入第二方、三方包
    • 最后导入本应用的包

      import os
      import sys
      
      import memcache
      import MySQLdb
      
      from myproject.lib.dblogic import get_user
      
  3. 对于内部包的导入是不推荐使用相对导入的,对所有导入都要使用包的绝对路径;
  4. 从一个包含类的模块中导入类时,通常可以写成这样:

    from MyClass import MyClass
    from foo.bar.YourClass import YourClass
    

2.4. 文件头

设置编码方式

# -*- coding: utf-8 -*-

如果是程序入口文件,则文件最顶部还需要加上 shebang ,尾部加上 main 入口函数:

#!/usr/bin/env python

def main():
    ...

if __name__ == '__main__':
    main()

2.5. 注释

单行的注释用 #开头,并在 # 后面加一个空格;成段的注释用 """(三引号)。

如果代码内容修改了,及时更新注释。

如果注释无法做到及时更新,删除掉注释更好。

2.6. 命名风格

2.6.1. 目录

全小写单词或缩写,多个单词不用下划线间隔,比如:roomshow/

2.6.2. 文件

全小写单词或缩写,多个单词不用下划线间隔,比如:userclient.py

2.6.3. 类

每个单词首字母大写的单词或缩写,多个单词不用下划线间隔,比如:class AudioPass:

2.6.4. 函数

小写单词或缩写、下划线、数字组合,多个单词用下划线间隔,比如: update_user_age 如果是类里的私有函数以下划线开始,比如:def _private_func(self):

装饰器内部函数命名,采用加_前缀的方式,方便异常时显示 Traceback:

def cache(fn):
    @functools.wraps(fn)
    def _cache(*args, **kw):
        # do something here
        return fn(*args, **kw)
    return _cache

2.6.5. 变量

小写单词或缩写、下划线、数字组合,多个单词用下划线间隔,比如:conn_timeout。 若与关键字名字冲突,后缀加划线,尽量不要使用缩略等其他方式,比如:from_

常用命名包括:

  • has_xxx 表示是否存在xxx
  • is_xxx 表示是否xxx
  • can_xxx 表示是否能xxx,是否有权限做xxx
  • excex 表示异常
  • retresresp 表示返回
  • 单词缩写除非有公认的缩写,否则用全部单词,不要自己创造缩写。

2.6.6. 常量

全大写单词或缩写、下划线、数字组合,多个单词用下划线间隔,比如:

class Code:
    OK = 0
    ERROR = 1
    INVALID_PARAM = 2

2.7. 缩进

缩进采用 4 个空格缩进,在项目根目录配置 .editorconfig 文件:

# https://editorconfig.org
root=true

[*]
indent_style=space
indent_size=4
end_of_line=lf
charset=utf-8
insert_final_newline=true
trim_trailing_whitespace=true
max_line_length=100

[*.yaml]
indent_size=2

2.8. 函数、类、文件代码长度

函数、类、文件代码长度不要太长,如果太长拆分成多个。

函数建议 100 行之内,类或者文件 1000 行之内。

2.9. 空格

  1. 各种左右括号前不要加空格

    No: sum (1, 2), sum( 1, 2 ), { "age": 18 } Yes: sum(1, 2), list[1, 2], {"age": 18}

  2. 逗号、冒号、分号前不要加空格,后面要加一个空格

    No: sum(1,2), sum(1 , 2), {"age":18}, {"age" : 18} Yes: sum(1, 2), {"age": 18}

  3. 操作符左右各加一个空格,多行之间不要为了对齐增加空格

    No: age=18 Yes: age = 18

  4. 函数默认参数和 keyword 传参等号两边省略空格

    No: def get_user(name, age = 18): pass Yes: def get_user(name, age=18): pass

  5. 单行注释符后面空一个空格,前面空至少 2 个空格

    No: width += 1 #border width Yes: width += 1 # border width

三、 设计建议

3.1. 主循环要捕捉异常

避免程序因为没有考虑到的异常而直接退出。

3.2. None 比较

None 应该永远用:isis not 来做比较。

3.3. 对象类型的比较使用 isinstance

No: if type(obj) is type(1): Yes: if isinstance(obj, int):

3.4. 使用 startswith()、endswith()

No: if foo[:3] == "bar": Yes: if foo.startswith("bar"):

3.5. 空序列的判断

对于序列(字符串、列表、元组),使用空列表是 false 这个事实,因此 if not seqif seqif len(seq)if not len(seq) 好。

3.6. 布尔值比较

No: if greeting == True: Yes: if greeting:

3.7. if else判断

if 的地方一定要有 else 分支,做到不重不漏。如果 else 不需要做事,需要明确写个注释,表示关注到了。

3.8. 序列是否存在某个 key

No:if dict.has_key(key): Yes:if key in dict:

No: if name == "foo" or name == "bar": Yes: if name in ("foo", "bar"):

3.9. 注意不要在 for/while 循环中修改被遍历的对象

user_ids = [100, 200, 300]
for user_id in user_ids:
    print(user_id)
    if user_id == 200:
        user_ids.remove(user_id)

比如上面的代码将不会打印 300,正确的做法是重新拷贝一份:

user_ids = [100, 200, 300]
for user_id in user_ids[:]: # 或者 for user_id in copy.copy(user_ids):
    print(user_id)
    if user_id == 200:
        user_ids.remove(user_id)

3.10. 时间相关的函数

  • 获取当前时间:datetime.datetime.now(), time.time()
  • 当前时间格式化为字符串:time.strftime("%Y-%m-%d")
  • 字符串转成 datetime:datetime.datetime.strptime("2014-07-24", "%Y-%m-%d")
  • 时间戳转成 datetime:datetime.datetime.fromtimestamp(xxx)
  • Datetime 转成时间戳:time.mktime(xxx.timetuple())

3.11. Url访问

一定要设置超时时间,尽量使用 requests 库,如果要使用 urllib,则不要直接拼凑参数:

import urllib
import urllib2

def get_user(urs):
    url = "http://example.com/getuser"
    # 如果是用 GET 方式,注意不要直接拼url参数(url = url + '?name=' + name),
    # 要用 urlencode 或 quote 转义变量中可能包含的 /,& 等特殊字符,如下:
    # 1) url = url + '?' + urllib.urlencode(dict(urs=urs))
    # 2) url = url + '?urs=' + urllib.quote(str(urs))
    try:
       # urlopen 第 2 个位置参数不为 None 表示用 POST 方式
       fp = urllib2.urlopen(url, dict(name=name), timeout=5)
    except urllib2.HTTPError as exc:
       return exc.code, exc.read() # http 错误时返回
    return fp.code, fp.read() # http 正常时(2xx)返回

3.12. 日志

3.12.1 日志分级

  • DEBUG 用于程序调试,线上日志文件一般不会输出,但也应该尽量遵循日志的打印规范,方便临时打开 DEBUG 日志状态查看功能。
  • INFO 用于记录程序运转和状态的信息,如:当前的 CPU、内存、任务队列、一定单位时间内的任务耗时等。 用于记录一过性的,不会大量反复输出的信息。如:连接数据库成功、连接缓存成功等后,便于跟踪程序进展信息。 用于记录关键性的信息,增加、删除、修改等操作。如:修改密码、创建房间、删除好友等。 用于记录一些异常但对程序和功能没有影响的信息。如:协议参数错误等。
  • WARN 用于记录程序遇到一般性异常的情况,虽然暂时不影响程序和功能,但是极有可能运行在某种非正常的状态下,并且最终使程序功能异常。如:任务超时,请求过多等。
  • ERROR 用于记录程序遇到严重错误,影响到程序的基本功能。如:数据库缓存连接错误或者失败,网络错误,跟其他服务的连接断开,程序异常退出,系统整体内存不足、CPU 过高等。 用于记录一些关键性的逻辑异常。如:扣钱等逻辑不符合预期等。

可以参考日志的艺术

3.12.2 编写规范

INFO 及以上级别的日志必须打印较为完整的上下文信息,如请求的 user_id、参数、基本信息描述等。 WARNERROR 级别的日志尽可能记录程序异常的堆栈(如 Traceback),如错误发生在用户请求阶段必须记录用户当前请求的所有参数。 不应该打印的一些敏感信息,如用户密码等。

3.12.3 使用 logging.getLogger

使用 logging.getLogger(__name__) 来获取 logger

import logging

logger = logging.getLogger(__name__)

def main():
    logger.info("hello world")

四、 常见弃用写法

关键词 弃用写法 推荐写法
as except Exception, exc: except Exception as exc
in if map.has_key('foo'): if 'foo' in map:
or if name == 'cc' or name == '163': if name in ('cc', '163'):
== if foo[:3] == 'bar': if foo.startswith('bar'):
type if type(obj) is int: if isinstance(obj, int):
class class Foo: class Foo(object):
None if obj == None: if obj is None:
True if obj == True: if obj: or if obj is True:
traceback log.error('login error\n%s', traceback.format_exc()) log.exception('login error') or log.error('login error', exc_info=1)
log log.info('user login, urs=%s' % urs) log.info('user login, urs=%s', urs)

五、 其他

其他未提及的编码规范请参阅 PEP 8

可以使用 git pre commit hooks 来自动检查和进行修正。

其他相关规范:

本文来自《Python 编码风格规范》- 熊清亮的博客。

转载请注明原文链接:https://seealso.cn/spec/python-coding-style-specification