注:
- 本文仅讨论 PEP8 规范未明确的细节, 并提供个人风格参考
- 风格来自 Google Python Style Guide, 并根据个人喜好做了细微调整 (这些调整的目的是为了进一步提升视觉美感)
文章目录
格式说明
推荐程度
# Good: 表示这是一个好的示范.
# Better: 表示这是一个更好的示范.
# Best: 表示这是一个最佳示范.
# Tricky: 表示这是一个更复杂的技巧, 它可以让代码变得更加精炼, 但也降低了易读性.
# Not Bad: 相对来说不算太好的做法, 您仍然可以将其作为一种示范. 通常来说这类做法不太严谨, 但胜在写法更加轻便, 而且仍然能保持形式美观.
# Not Good: 表示这是一个不太好的做法, 但不强烈拒绝你这么做 (如果你喜欢这种做法的话).
# Bad: 表示这是一个不好的做法, 你应该避免这样做.
# Worse: 表示这是一个更糟糕的做法, 你应该尽量避免这样做.
# Worst: 表示这是一个极其反面的示例, 在任何时候都应该拒绝这样做.
箭头 ^
标记
print(a + b )
^
这是一个位置标记, 它用于更明确地指出哪里出现了不规范的做法.
Lint: 对于不规范的做法, 我们会以 'Lint' 开头, 予以说明.
说明与提示
Note: 对这种做法予以详细的说明, 或者解释这样做的理由. 以 'Note' 开头.
Lint: 对不正确的做法予以提示, 指出哪里不正确/不合理. 以 'Lint' 开头.
风格详解
块注释
功能概述
# Good
def my_func():
""" A one line summary of the function, terminated by a period. """
Note: 开头和结尾要留一个空格.
这样做的好处是, 与 Python 的 4 空格缩进风格保持一致, 使排版更整齐.
# Good
def my_func():
""" This is a longer summary, its length exceeds the single line limit, so
we take a line break like this.
"""
Note: 注意概述在换行时, 仍然保持与上一行相同的缩进.
这也是为何之前建议开头和结尾要留空格的原因.
# Not Bad
def my_func():
""" This is a longer summary, its length exceeds the single line limit, so
we take a line break like this. """
^-^
Note: 当概述产生换行时, 将结尾的三引号留到末尾可作为另一种选择.
# Not Good
def my_func():
""" A one line summary of the function, terminated by a period """
Lint: 概述的末尾句点不建议被省略. ^
# Bad
def my_func():
"""A one line summary of the function, terminated by a period."""
^ ^
Lint: 您应该在开头和结尾留一个空格的空间.
措辞风格
# Good
def get_rows():
""" Fetches rows from a big table. """
^-----^
Note: 使用声明式的措辞.
# Not Bad
def get_rows():
""" Fetch rows from a big table. """
^---^
Lint: 减少使用命令式的措辞.
详细描述
# Good
def my_func():
""" A one line summary of the function, terminated by a period.
Leave one blank line above. Here provides an overall description of this
module. Optionally, it may also contain usage examples and so on.
"""
Note: 详细描述与概述之间留一个空行做缓冲.
# Not Bad
def my_func():
"""
No blank line if there's no summary above, this is not often to use though.
"""
Note: 当没有概述时, 可取消所留的空行, 这样使文本在适当的时候更紧凑, 仍然保持了形式美观.
# Not Good
def my_func():
""" A one line summary of the function, terminated by a period.
Leave one blank line above. Here provides an overall description of this
module. Optionally, it may also contain usage examples and so on.
"""
Lint: 您应该在详细描述与概述之间留一个空行做缓冲.
# Bad
def my_func():
""" A one line summary of the function, terminated by a period.
Leave one blank line above. Here provides an overall description of this module. Optionally, it may also contain usage examples and so on.
"""
Lint: 请不要让单行超过 80, 100 或 120 字符. 该字符数限制取决于您的个人偏好和屏幕宽度.
如果您在使用等宽中文字符 (例如等距更纱黑体), 那么可将一个汉字宽度看作两个字母看到.
参考或引用
class Parent:
""" This is parent class.
Args:
name: A company or an organization name.
"""
class Child:
"""
See `Parent` class.
"""
Note: 通常, 参考 xxx 以 'See' 开头, 后跟要参考的对象.
参考对象可以是其他类/方法/函数, 也可以是外部文档, 或者网络链接 (如下方示例).
def draw(self):
""" Draw a plot on the canvas.
See 'docs/samples/plot on canvas.png'.
https://segmentfault.com/a/1190000015659597?utm_source=tag-newest
"""
特定字段
def list_files(idir, suffix=None, recursive=False):
""" List out files under the directory.
Args:
idir (str): The entrance directory.
suffix ([None|tuple]): The extension of filename, starts with a period.
Example: ('.png', '.jpg', '.bmp')
recursive (bool):
True: Find all files, include files under subfolders.
False: Find files only in current folder.
Returns:
[filepath, ...]
Raises:
FileNotFoundError: Please check if `idir` exists.
"""
pass
class Person:
""" Summary of class here.
Attributes:
name (str): Full name of person.
age (int):
gender (['male'|'female']):
Raises:
AssertionError: The `age` is invalid.
IndexError: The `gender` is undefined.
"""
def __init__(self, name, age, gender: int):
self.name = name
self.age = age
assert self.age > 0
self.gender = ('male', 'female')[gender]
完整的特定字段清单如下:
Order | Section | Note |
---|---|---|
1 | References/Refer | 参考指向其他类/方法/函数, 也可以是外部文档, 或者网络链接. |
1 | See | 相比 Refer 更宽松, 更自然的引用方式, 不需要严格的格式要求. |
2 | Args | 列出每个参数名, 后跟空格 + 括号 + 类型 (可选), 然后跟冒号 + 描述 (可选). |
2 | Attributes/Attr | 列出类的公共属性, 格式与 Args 相同. |
3 | Example(s)/E.g. | 一个简短的示例, 可以插入在 Args 中 (在参数描述中换行并空 4 格书写), 或者单独作为一个 Section 出现. |
3 | Usage(s) | 一个用法演示, 与 Example(s) 相比, 通常来说内容更完整, 情景更具体, 更易于让人举一反三的模仿. 且通常以多行的代码块形式出现. |
3 | IO/In & Out | 当方法涉及文件输入输出时, 建议以此字段描述. |
4 | Returns | 描述返回值. |
4 | Yields | 描述迭代返回值. |
5 | Raises | 列出可能产生的报错和错误的来源. |
更多示例如下所示 (您可以看到它在实际使用中的效果, 注意示例中可能存在 “Not Bad” 的写法):
行注释
单行注释
# Good
# This is a single line comment.
# The period at the end of comment is not necessary
Note: 末尾的句点是非必须的, 可以省略.
# Not Bad
# this is a single line comment
^
Note: 您可以选择全小写的风格, 如果这更偏向于您的喜好.
行尾注释
# Good
a = round(10 / 3, 2) # got a percent number
b = f'{a}%' # text format
Note: 行尾注释的第一个字母可以是大写, 也可以是小写. 取决于它是短语还是完整的句子, 但不需要刻意区分.
您需要避免大小写混用, 这会让注释看起来比较杂乱. 通常在相近的语句块中坚持一种格式是好的.
同样, 末尾的句点是非必须的, 可以省略.
对前一行注释
# Good
print(f'{unused_img_cnt / total_img_cnt * 100}%')
# Tested in 2020-11-21, the percent is 44.61%, almost half of images are unused.
Note: 对于目标代码, 无论在它前一行写注释还是后一行写注释都很常见, 但对阅读来说并不方便, 因为无法立
即分清它是指向上一行代码还是下一行.
为了更清晰地表示指向上一行, 我们对行首的空格进行了调整. 确保注释内容的开始位置是 4 的整数倍
(在 IDE 中按下 Tab 键, 可以很容易做到, 不需要我们数要敲击的空格数).
更多示例如下所示 (您可以看到它在实际使用中的效果, 确实能够提升易读性):
更多
本章节展示了更多适合强迫症的编程风格, 您可以根据自己的偏好有选择地采用.
变量命名习惯
对称好于不对称
注: 仅建议在局部范围尝试.
# == Good ==
benchmark = Benchmark()
select = Select()
provide = Provide()
# == Obsessive ==
bcmk = Benchmark()
slct = Select()
prvd = Provide()
# ------------------------------------------------
# == Good ==
class Foo:
def fast_calc():
pass
def std_calc(): # std: standard
pass
# == Obsessive 1 ==
class Foo:
def fst_calc(): # fst: fast
pass
def std_calc(): # std: standard
pass
# == Obsessive 2 ==
class Foo:
def fast_calc():
pass
def stnd_calc(): # stnd: standard
pass
语义明确好于语义简洁
# == Bad ==
er = ExcelReader()
ew = ExcelWriter()
fr = FileReader()
fw = FileWriter()
# == Good ==
reader1 = ExcelReader()
writer1 = ExcelWriter()
reader2 = FileReader()
writer2 = FileWriter()
# == Better ==
exl_reader = ExcelReader()
exl_writer = ExcelWriter()
file_reader = FileReader()
file_writer = FileWriter()
格式一致好于格式不一致
class MyFinder:
def find_one(self, x):
pass
def find_last(self, x):
pass
# == Not Good ==
def findall(self, x):
pass
# == Good ==
def find_all(self, x):
pass
使用 i-, o- (或者 r-, w-) 前缀表示 “输入”, “输出”
# (建议在局部变量使用)
for ifile, ofile in file_io.items():
ilist = get_list_from_file(ifile)
olist = handle_list(ilist)
write_result(olist, ofile)
目录路径末尾不加斜杠
idir = '../data'
odir = '../output'
for name in name_list:
ifile = f'{idir}/{name}.html'
ofile = f'{odir}/{name}.json'
...
为什么?
显式的斜杠更易于阅读. 这点类似于百分号, 查看下面的例子:
# == Bad ==
def calc_percent_1(m, n):
return str(round(m / n * 100, 2)) + '%'
# == Good ==
def calc_percent_2(m, n):
return round(m / n * 100, 2)
print('当前的工作进度是 {}'.format(calc_percent_1(1, 3)))
print('当前的工作进度是 {}%'.format(calc_percent_2(1, 3)))
前者在易读性上更差一些. 特别是当计算百分比的函数和 print 在不同的 py 文件时, 前者更容易让人忽略掉它是百分数的事实. (而且 calc_percent_1
在函数 “单一职能” 的表达上也不如 calc_percent_2
)
路径的斜杠的使用原则和这个百分号示例有着相同之处. 这是为什么不推荐在目录路径末尾加斜杠的原因.
附录
风格选型: 为什么 docstring 采用了基于 Google 的风格?
Python docstring 主流的有五种风格:
- Plain
- reStructuredText
- Numpy
- Epytext
下面仅从个人角度说一下对每个选择的考量:
- Plain 没有特定的风格指导, 不利于 pydoc 生成规范化的注释文档, 被排除
- reStructuredText (1) code fence 是用双 ``var`` 包裹, 与我的 Markdown 习惯 `var` 不一致; (2) 当手动补充参数时, 写起来比较麻烦 (比如
:param xxx: ...
, 打得多了就感觉有些费事); (3):param type xxx: ...
中的 type, Pycharm 似乎只能识别基本类型, 不能识别自定义类型; (4):param xxx: ...\n:type xxx:
这样分开写也不太符合自己的喜好, 所以被排除 - Numpy 使用连续的短横线作为分隔符 (如下图), 个人感觉会让代码不太好看 (参数少了显得换行 “泛滥”, 参数多了各种长短不一的横线看起来杂乱), 而且多数中文字体的字宽不严格等于两个短横线, 这就导致换用字体可能引起短横线的长度与中文长度照不上的烦恼, 会让代码看起来更乱, 被排除
- Epytext 没有深入了解过, 相较于其他风格有些偏冷门, 被排除
综上所述, Google 风格是个人先做排除法选中的, 再加上 Google 品牌效应, 良好严谨的 风格文档 (包括 中文文档), 以及社区的活跃和支持 (例如, Pycharm 的 autodoc 插件默认是以 Google 风格来解析的), 于是最终选择了它.
参考
- Google Python Style Guide https://google.github.io/styleguide/pyguide.html
- Python Docstring 风格和写法学习 - 飞鸟的笔记本 https://www.cnblogs.com/ryuasuka/p/11085387.html