1 分钟阅读

一些代码风格笔记

前两天写 sql 的时候,被几个自动格式化的几个缩进逼疯了。我也突然意识到代码风格对我们编程的时候到底有多大的影响。我平时写个人项目偏多,也经常被一些难看的代码格式弄得好难受,看别人的代码也经常觉得代码过于丑陋,很多空格和缩进上的不同使得团队协作非常不方便。

这篇指南参考了一些专业团队的代码协作方式,算是给代码样式做一个学习笔记。下面的内容主要是我平时遇到的一些规范。详细的内容可以参考相应的官网。

Python 风格指南

原文参考Python 风格指南

很多团队都会使用 BlackPying 作为自动格式化的工具(我平时使用的就是 Black

Python 语言规范

Lint

pylint 是在 Python 代码中寻找 bug 和格式问题的工具。但 pylint 并不完美,有些死板的警告并不适合我们灵活的代码,有时候就需要我们手动进行抑制,或者直接忽略。例如:

def do_PUT(self): # WSGI 接口名,所以 pylint: disable=invalid-name

异常

定义:异常时一种跳出正常的控制流,以处理错误或其他异常情况的方法。

永远不要用 except: 语句来捕获所有异常,因为这种写法会使得 Python 变得非常宽容。相应的,try/except 中的代码量也应该尽量小,避免真正的错误被掩盖。

全局变量

定义:在程序运行时可以发生变化的模块级变量和类属性。

避免使用全局变量。在特殊情况下需要用到全局变量时,应将全局变量声明为模块级变量或者类属性,并在名称前加 _ 以表示为内部状态。如需从外部访问全局变量,必须通过公有函数或类方法实现。

嵌套/局部/内部类和函数

可以谨慎使用. 尽量避免使用嵌套函数和嵌套类, 除非需要捕获 selfcls 以外的局部变量. 不要仅仅为了隐藏一个函数而使用嵌套函数. 应将需要隐藏的函数定义在模块级别, 并加上 _ 作为标注.

默认迭代器和操作符

优先使用返回迭代器的方法, 而非返回列表的方法, 不过注意使用迭代器时 不能修改容器.

正确:

for key in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in adict.items(): ...

错误:

for key in adict.keys(): ...
for line in afile.readlines(): ...

函数与方法装饰器

定义: 装饰器作用在函数和方法上.

仅在有显著优势时, 审慎地使用装饰器. 不得使用 staticmethod, 除非为了兼容老代码库地 API 不得已而为之. 应把静态方法改写为模块级函数. 例如:

正确:

# utils.py

def to_snake_case(name: str) -> str:
    return name.replace("-", "_").lower()
    
# 调用方式

from utils import to_snake_case

result = to_snake_case("Hello-World")

错误:

# utils.py

class StringUtils:
    @staticmethod
    def to_snake_case(name: str) -> str:
        return name.replace("-", "_").lower()


# 调用方式
from utils import StringUtils

result = StringUtils.to_snake_case("Hello-World")

这是因为, 如果一个函数不依赖于类的状态, 实际上可以提取出来, 不放在类里面实现.

Python 风格规范

分号

不要在行尾加分号, 也不要用分号将两条语句合并到一行 (C++工程师暴怒)

行宽

最大行狂是 80 个字符. 当然也有例外, 比如一些 URL, 路径名以及长的标志. 不要用反斜杠表示显式续航. 应该利用 Python 的圆括号, 中括号和花括号的隐式续行.

括号

使用括号时宁缺毋滥. 可以把元组括起来, 但不强制. 不要在返回语句或条件语句中使用括号, 除非用于隐式续航或者元组.

缩进

用 4 个空格作为缩进. 不要使用制表符. 使用隐式续行时, 应该把括起来的元素垂直对齐, 或者添加 4 个空格悬挂缩进.

注释和文档字符串 (docstring)

模块

每个文件应该包含一个许可协议模板. 文件的开头是文档字符串,其中应该描述模块内容和用法.

"""模块或程序的一行概述, 以句号结尾.

留一个空行. 接下来应该写模块或程序的总体描述. 也可以选择简要描述导出的类和函数,
和/或描述使用示例.

经典的使用示例:

foo = ClassFoo()
bar = foo.FunctionBar()
"""

函数和方法

一个函数的基本内容应包括:

  • 基本说明:
  • Args: (可以换行)
  • Returns:
  • Raises:

例如:

def fetch_smalltable_rows(
    table_handle: smalltable.Table,
    keys: Sequence[bytes | str],
    require_all_keys: bool = False,
) -> Mapping[bytes, tuple[str, ...]]:
    """从 Smalltable 获取数据行.

    从 table_handle 代表的 Table 实例中检索指定键值对应的行. 如果键值是字符串,
    字符串将用 UTF-8 编码.

    参数:
        table_handle: 处于打开状态的 smalltable.Table 实例.
        keys: 一个字符串序列, 代表要获取的行的键值. 字符串将用 UTF-8 编码.
        require_all_keys: 如果为 True, 只返回那些所有键值都有对应数据的
            行.

    返回:
        一个字典, 把键值映射到行数据上. 行数据是字符串构成的元组. 例如:

        {b'Serak': ('Rigel VII', 'Preparer'),
         b'Zim': ('Irk', 'Invader'),
         b'Lrrr': ('Omicron Persei 8', 'Emperor')}

        返回的键值一定是字节串. 如果字典中没有 keys 参数中的某个键值, 说明
        表格中没有找到这一行 (且 require_all_keys 一定是 false).

    抛出:
        IOError: 访问 smalltable 时出现错误.
    """

应包括 Attributes (属性)

例如:

class SampleClass(object):
    """这里是类的概述.

    这里是更多信息....
    这里是更多信息....

    属性:
        likes_spam: 布尔值, 表示我们是否喜欢午餐肉.
        eggs: 用整数记录的下蛋的数量.
    """

    def __init__(self, likes_spam = False):
        """用某某某初始化 SampleClass."""
        self.likes_spam = likes_spam
        self.eggs = 0

    def public_method(self):
        """执行某某操作."""

字符串

应使用 f-string, % 运算符或 format 方法来格式化字符串. 可以用 + 实现单词拼接, 不能用来实现格式化

导入

导入语句也有一定顺序:

  1. 导入 Python 的 __future__.
  2. 导入 Python 的标准库
  3. 导入第三方模块和包
  4. 导入代码仓库中的子包
  5. 已废弃的规则: 导入应用专属的\与该文件属于同一个子包的模块.

命名:

  • 模块名:module_name;
  • 包名:package_name;
  • 类名:ClassName;
  • 方法名:method_name;
  • 异常名:ExceptionName;
  • 函数名:function_name,query_proper_noun_for_thing,send_acronym_via_https;
  • 全局常量名:GLOBAL_CONSTANT_NAME;
  • 全局变量名:global_var_name;
  • 实例名:instance_var_name;
  • 函数参数名:function_parameter_name;
  • 局部变量名:local_var_name.

分类:

更新时间: