跳至内容
Python
数据科学

PEP-8 教程:Python 代码规范

通过本初学者教程,开始探索 PEP-8(Python 风格指南),从而正确格式化您的代码,最大化可读性!
Chitrank Dixit's photo
Chitrank Dixit
已更新 · 15分钟 阅读

使用 AI 探索

ChatGPTClaudePerplexity

创建您的免费账户

实践并学习路径你的进步。

使用 Google 继续
继续操作即表示您接受我们的《使用条款》和《隐私政策》,并同意您的数据存储在美国。

PEP-8(Python 增强提案)提出了一些关键要点,帮助您让代码更有条理、更易读。正如 Python 之父 Guido Van Rossum 所说:

代码被阅读的次数远多于它被编写的次数。

在本文中,您将通过一些代码示例开始探索 PEP-8!您将涵盖以下主题:

  • 首先介绍 PEP-8,它是什么、为何需要它;
  • 接着是缩进——程序员之间的热门话题。该用 Tab 还是空格?本节将揭晓答案;
  • 也许出乎意料,PEP-8 对单行的最大长度也有指南;
  • 另外,对空行的使用也有建议做法;
  • 随后,表达式与语句中的空白符处理也是初学者容易上手的内容;
  • 什么是编码,为什么在使用 Python 时需要它?Python 3 的默认编码是什么?源文件编码一节将解释这些问题。
  • 在编程时您很可能经常使用 import。本节将讨论 import 的顺序、绝对与相对导入、通配符导入等;
  • 文档对于记录应用的各个方面至关重要,也能提升最终产品的整体质量。注释在这里举足轻重!
  • 您了解模块级双下划线(dunder)命名吗?它们在文档字符串中尤其有用!
  • 最后,您还将进一步了解命名约定:如何为函数命名、通常采用何种命名风格,等等;
  • “您的代码符合 PEP-8 吗?”——这是您一定要问自己的问题。因此最后一节将介绍一些工具,帮助您检查代码是否遵循本文所述以及未覆盖的更多指南。

PEP-8 简介

Python 编程语言已经发展为许多人偏爱的语言之一。它相对容易学习,支持多范式,拥有大量提升实用性的开源模块,并且是数据科学与 Web 开发社区的常用工具。

然而,只有当您知道如何用代码更好地表达想法时,才能真正发挥 Python 的优势。Python 诞生时有一些设计目标,当您输入 import this 时便可一窥端倪。

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

以上 20 条即 Python 的编程哲学,称为“Python 之禅”。输出中也提到了“Readability Counts(可读性至上)”,这应当是您写代码时的首要关切:其他程序员或数据科学家应能理解并参与到您的代码中,从而共同解决手头的任务。

接下来的各节将进一步说明如何达成上述目标!

缩进

在 Python 中编程,缩进是必不可少的。但您要谨慎使用,因为不当缩进会导致语法错误。建议使用四个空格进行缩进。例如,下面的语句使用了四个空格:

if True:
    print("If works")

下面这个带有 print 语句的 for 循环同样使用了四个空格缩进:

for element in range(0, 5):
    print(element)

当您编写较长的表达式时,最好让表达式垂直对齐。这会形成所谓的“悬挂缩进(hanging indent)”。

以下是一些在长表达式中使用悬挂缩进的示例,展示了不同的写法:

  1. value = square_of_numbers(num1, num2,
                           num3, num4)
    
  2. def square_of_number(
         num1, num2, num3, 
         num4):
     return num1**2, num2**2, num3**2, num4**2
    
  3. value = square_of_numbers(
                  num1, num2,
                  num3, num4)
    
  4. list_of_people = [
     "Rama",
     "John",
     "Shiva"
    ]
    
  5. dict_of_people_ages = {
     "ram": 25,
     "john": 29,
     "shiva": 26
    }
    

几乎每位使用 Python 或其他语言的开发者都会在某个时刻思考:缩进应该用 Tab 还是空格?关于 Tab 与空格的争论在社区从未停歇。例如可参见这篇 Stackoverflow 文章。

通常,空格是首选的缩进方式;但如果您接手的脚本已经在使用 Tab,您应当继续用 Tab 保持一致。否则,建议将脚本中的所有缩进改为使用空格。

注意:Python 3 不允许在缩进中混用 Tab 与空格。因此请选择其一并始终如一!

最大行宽

一般来说,Python 代码中的单行长度建议不超过 79 个字符。

遵循这一目标有很多好处,其中包括:

  • 可以并排打开文件进行对比;
  • 无需水平滚动即可查看完整表达式,从而提升代码的可读性与理解度。

注释的行宽建议为 72 个字符。您将在本教程后面学习更多关于注释的常见约定!

最终,如果您在小团队中工作,采用何种编码约定与风格取决于团队共识,偏离最大行宽指南通常也能被接受。但如果您在创建或参与开源项目,则很可能希望或需要遵循 PEP-8 规定的最大行宽规则。

在使用 + 运算符时,最好合理断行,以提升代码的可读性:

推荐写法 不推荐写法

total = (A +
B +
C)

total = (A
+ B
+ C)

或者,您也可以这样写:

total = A
        + B
        + C

简言之,您可以在二元运算符之前或之后断行,只要保持一致即可。如果在编写新代码,建议采用前面展示的最后一种做法:在二元运算符之前断行。

空行

在 Python 脚本中,顶层函数与类之间应以两个空行分隔。类中方法定义之间应以一个空行分隔。下面的示例展示得很清楚:

import unittest

class SwapTestSuite(unittest.TestCase):
    """
        Swap Operation Test Case
    """
    def setUp(self):
        self.a = 1
        self.b = 2

    def test_swap_operations(self):
        instance = Swap(self.a,self.b)
        value1, value2 =instance.get_swap_values()
        self.assertEqual(self.a, value2)
        self.assertEqual(self.b, value1)


class OddOrEvenTestSuite(unittest.TestCase):
    """
        This is the Odd or Even Test case Suite
    """
    def setUp(self):
        self.value1 = 1
        self.value2 = 2

    def test_odd_even_operations(self):
        instance1 = OddOrEven(self.value1)
        instance2 = OddOrEven(self.value2)
        message1 = instance1.get_odd_or_even()
        message2 = instance2.get_odd_or_even()
        self.assertEqual(message1, 'Odd')
        self.assertEqual(message2, 'Even')

SwapTestSuite 与 OddOrEvenTestSuite 两个类之间用两个空行分隔,而类中方法定义(如 .setUp() 和 .test_swap_operations())之间仅用一个空行分隔。上述代码可以成功执行,但不会生成任何输出,因为缺少运行单元测试所需的代码(这里只是展示最佳实践)。

表达式与语句中的空白

当您的代码出现如下写法时,应尽量避免多余的空白:

推荐写法 不推荐写法

func(data, {pivot: 4})

func( data, { pivot: 4 } )

indexes = (0,)

indexes = (0, )

if x == 4: print x, y; x, y = y, x

if x == 4 : print x , y ; x , y = y , x

spam(1)

spam (1)

dct['key'] = lst[index]

dct ['key'] = lst [index]

x = 1
y = 2
long_variable = 3

x = 1
y = 2
long_variable = 3

ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[lower:upper], ham[lower:upper:], ham[lower::step]
ham[lower+offset : upper+offset]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset]

ham[lower + offset:upper + offset]
ham[1: 9], ham[1 :9], ham[1:9 :3]
ham[lower : : upper]
ham[ : upper]

i = i + 1
submitted += 1
x = x2 - 1
hypot2 = x
x + yy
c = (a+b)
(a-b)

i=i+1
submitted +=1
x = x 2 - 1
hypot2 = x
x + y y
c = (a + b)
(a - b)

def complex(real, imag=0.0):
return magic(r=real, i=imag)

def complex(real, imag = 0.0):
return magic(r = real, i = imag)

def munge(input: AnyStr): ...
def munge() -> AnyStr: ...

def munge(input:AnyStr): ...
def munge()->PosInt: ...

def munge(sep: AnyStr = None): ...
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...

def munge(input: AnyStr=None): ...
def munge(input: AnyStr, limit = 1000): ...

if foo == 'blah':
do_blah_thing()
do_one()
do_two()
do_three()

if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()


或


if foo == 'blah': do_blah_thing()
for x in lst: total += x
while t < 10: t = delay()


或


if foo == 'blah': do_blah_thing()
else: do_non_blah_thing()

try: something()
finally: cleanup()

do_one(); do_two(); do_three(long, argument,list, like, this)

if foo == 'blah': one(); two(); three()

FILES = ('setup.cfg',)

FILES = 'setup.cfg',

FILES = [
'setup.cfg',
'tox.ini',
]
initialize(FILES,
error=True,
)

FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)

以上示例摘自 PEP-8。

源文件编码

计算机无法直接存储“字母”“数字”“图片”等任何对象;它只能存储并处理比特(bit),且比特只有二进制取值:yes 或 no、true 或 false、1 或 0 等。如您所知,计算机以电信号工作;这意味着“真实”的比特是电信号的有无。通常以 1 和 0 来表示这种有/无。

要用比特来表示比特之外的任何事物,就需要一套规则。您需要通过一种编码方案(encoding)将比特序列转换为字母、数字或图片等。常见编码方案有 ASCII、UTF-8 等:

  • American Standard Code for Information Interchange(ASCII,美式信息交换标准代码)是计算机与互联网中文本文件最常见的格式。在这类文件中,每个字母、数字或特殊字符由一个 7 位二进制数表示(由七个 0 或 1 组成的字符串)。
  • Unicode(万国码)是一个用于“交换、处理与显示现代世界各语言书写文本”的系统。简言之,Unicode 旨在容纳世界上已知的所有书写系统。目前 Unicode 使用三种不同的编码来表示字符集:UTF-8、UTF-16 与 UTF-32。
    • UTF-16 是变长编码:码位用一个或两个 16 位编码单元表示。
    • UTF-8 是另一种变长编码,使用一到四个 8 位字节。
    • UTF-32 是定长编码,每个 Unicode 码位固定使用 32 位。

提示:如果您想进一步了解编码,请参阅这篇文章。

那么,这为什么重要?

字符串是 Python 中最常用的数据类型之一。可以预见,您总会处理包含或完全由非标准 ASCII 字符组成的字符串。毕竟,您可能需要处理包含重音字符的文本,例如 á、ž、ç 等。

在 Python 3 中,UTF-8 是默认源文件编码。但对于使用 Python 2 的读者,您可能已经知道其默认编码是 ASCII。

那么,如果字符串中包含非 ASCII 字符,例如 "Flügel",会怎样?

在 Python 2 中引用该字符串时,您会得到如下结果:

>>> s 
'Fl\xfcgel'

这看起来不像您的字符串!那么打印它会怎样?

>>> print(s)
Flügel

打印输出为您赋给变量的值。这个非 ASCII 字符 ÃŒ 被编码了。因此当您直接引用该字符串时,才会得到 \xfc。为处理此类情况,您可以使用字符串方法 .encode() 与 .decode()。前者以指定编码返回该 Unicode 字符串的 8 位字符串版本,后者则以给定编码解释字符串。

导入(Imports)

在用 Python 进行数据科学时,导入库和/或模块是家常便饭。正如您可能已经知道的,库应始终在脚本开头导入。

注意:如果需要导入很多内容,请确保每个 import 独占一行。

请看下表以更好地理解:

推荐写法 不推荐写法

from config import settings


或


import os
import sys

import os, sys

另外,导入库时需要遵循一定的顺序。通常可按如下顺序:

  1. 标准库导入;
  2. 相关的第三方库导入;
  3. 本地应用/库的特定导入。

绝对导入与相对导入

接下来,了解绝对与相对导入的区别很有必要。总体而言,Python 更倾向于绝对导入,因为它提升了可读性。不过,随着应用变得更复杂,也可以使用相对导入。隐式相对导入不应使用,并已在 Python 3 中移除。

那么,什么是绝对导入与相对导入?

  • 绝对导入使用函数或类的绝对路径,并以 . 分隔。例如:

      import sklearn.linear_model.LogisticRegression
    
  • 相对导入则相对于当前 Python 文件所在位置。随着项目结构扩展,这种导入有助于提升可读性。假设您的项目结构如下:

      .
      ├── __init__.py
      ├── __init__.pyc
      ├── __pycache__
      │   ├── __init__.cpython-35.pyc
      │   ├── bubble_sort.cpython-35.pyc
      │   ├── selection_sort.cpython-35.pyc
      ├── bubble_sort.py
      ├── heap_sort.py
      ├── insertion_sort.py
      ├── insertion_sort.pyc
      ├── merge_sort.py
      ├── merge_sort.pyc
      ├── quick_sort.py
      ├── radix_sort.py
      ├── selection_sort.py
      ├── selection_sort.pyc
      ├── shell_sort.py
      ├── tests
      │   ├── test1.py
    

您可以在 test1 中使用相对导入来导入存放于 bubble_sort.py 的冒泡排序算法 BubbleSort。写法如下:

from ..bubble_sort import BubbleSort

若想进一步了解绝对与相对导入,请参阅 PEP 328。

通配符导入

最后,应尽量避免通配符导入,因为它不利于可读性;例如,您无法明确看到自己从模块中使用了哪些类、方法或变量:

from scikit import *

注释

注释用于 Python 代码内的文档说明,有助于理解代码。您可以使用许多工具为自有模块生成文档,例如注释与文档字符串(docstring)。注释应尽量详尽,这样他人在阅读代码时,能够正确理解代码及其与其他代码片段的配合方式。

注释以 # 符号开头。井号后的内容不会被解释器执行。例如,下面的代码块只会输出 "This is a Python comment"。

# This is a Python single line comment
print("This is a Python comment")

请记住:上一节提到,注释的行宽建议为 72 个字符!

注释主要有三种类型:

  • 当需要解释较复杂或他人不熟悉的代码时,使用块注释。这类注释通常篇幅更长,适用于其后的一部分或全部代码。块注释与代码保持相同缩进级别。块注释的每一行都以 # 和一个空格开头。如果需要分段,则用只包含一个 # 的空行分隔。

看一段来自scikit-learn 库的示例,以了解这类注释的样式:

   if Gram is None or Gram is False:
        Gram = None
        if copy_X:
            # force copy. setting the array to be fortran-ordered
            # speeds up the calculation of the (partial) Gram matrix
            # and allows to easily swap columns
            X = X.copy('F')
  • 应谨慎使用行内注释,但在需要解释部分代码时它们很有效。它们也有助于您记住某行代码的含义,或在与不熟悉代码细节的同事协作时派上用场。行内注释与语句写在同一行,位于代码之后,同样以 # 和一个空格开头。

例如:

counter = 0  # initialize the counter
  • 文档字符串(docstring)应写在公共模块、文件、类与方法的开头。此类注释以 """ 开始并以 """ 结束:
    """
        This module is intended to provide functions for scientific computing 
    """

模块级双下划线命名

了解了文档字符串之后,您还应知道模块级双下划线(dunder)命名——即前后各带两个下划线的名称——在 Python 中非常常见且有效。这些是 Python 预定义的特殊名称,避免与用户自定义的函数或名称冲突。更多信息可参阅这篇文章。

模块级 dunder(如 __all__、__author__、__version__)应置于模块的主文档字符串之后、所有 import 语句之前。from __future__ 导入应在除文档字符串外的任何代码之前定义:

"""
    Algos module consists of all the basic algorithms and their implementation
"""

from __future__ import print

__all__ = ['searching', 'sorting']
__version__ = '0.0.1'
__author__ = 'Chitrank Dixit'

import os
import sys

提示:请查看这些编写文档字符串的约定。

命名约定

在使用 Python 编程时,您几乎一定会遵循某种命名约定——一组为标识符(变量、类型、函数以及源代码与文档中的其他实体)选择字符序列的规则。

如果您不确定有哪些命名风格,请参考以下几种:

  • b 或单个小写字母;
  • B 或单个大写字母;
  • lowercase
  • UPPERCASE
  • lower_case_with_underscores
  • UPPER_CASE_WITH_UNDERSCORES
  • CapitalizedWords,也称为 CapWords、CamelCase 或 StudlyCaps;
  • mixedCase
  • Capitalized_Words_With_Underscores
  • _single_leading_underscore:弱“内部使用”标识。例如,from M import * 不会导入以下划线开头的对象。
  • single_trailing_underscore_:按约定用于避免与 Python 关键字冲突,例如 Tkinter.Toplevel(master, class_='ClassName')
  • __double_leading_underscore:用于类属性命名时会触发名称改写(在类 FooBar 内,__boo 会变为 _FooBar__boo)。
  • __double_leading_and_trailing_underscore__:位于用户可控命名空间中的“魔术”对象或属性。例如 __init__、__import__ 或 __file__。此类名称不应自行发明,只按文档说明使用。

通用命名约定

下表给出了命名标识符的一些通用指南:

标识符 约定
模块 lowercase
类 CapWords
函数 lowercase
方法 lowercase
类型变量 CapWords
常量 UPPERCASE
包 lowercase
  • 不要使用单个变量名 'l'、'O' 或 'I':在某些字体中它们与数字 0 与 1 过于相似。
  • 一般而言,能用短名称时就用短名称。在某些情况下,可以通过下划线提升可读性。

若想了解通用命名约定的例外情况,请参阅这篇文章。

您的代码符合 PEP-8 吗?

在进一步了解 PEP-8 之后,您可能会想如何检查自己的代码是否遵循了这些指南(以及本教程未覆盖的更多内容)。

除了自行阅读 PEP-8 并深入学习外,强烈建议您了解实用的 pep8 模块、coala 软件包,以及后续章节介绍的其他替代方案!

Python pep8 软件包

pep8 包可用于检查您的 Python 代码中不符合 PEP-8 的部分,并给出修改建议以遵循 PEP-8。您可通过 pip 运行以下命令安装 pep8 模块:

pip install pep8

为演示 pep8 的工作方式,新建一个名为 example.py 的 Python 文件,并写入以下代码:

def my_function(a, b):
    print("The sum of a and b is: ", a + b)

a = 1
b = 2

my_function(a,b)

保存文件,然后在终端或命令提示符中对其运行 pep8 命令:

pep8 example.py

这将输出 pep8 在代码中检测到的不符合项,类似如下:

example.py:1:1: E302 expected 2 blank lines, found 0
example.py:3:5: E225 missing whitespace around operator
example.py:5:5: E225 missing whitespace around operator

第一行表示第 1 行的函数定义之前应有两个空行。接下来的两行表示 print 语句与赋值语句中的加号运算符周围应有空格。

要修复这些问题,可将代码修改为:

def my_function(a, b):
    print("The sum of a and b is:", a + b)

a = 1
b = 2

my_function(a, b)

现在再次运行 pep8 example.py,若无任何输出,表示代码已符合 PEP-8。

您还可以使用 --show-source 参数查看出现不符合项的源代码:

$ pep8 --show-source --show-pep8 testsuite/E40.py
testsuite/E40.py:2:10: E401 multiple imports on one line
import os, sys
         ^
    Imports should usually be on separate lines.

    Okay: import os\nimport sys
    E401: import sys, os

或者通过添加 --statistics 查看各类错误的出现频次:

$ pep8 --statistics -qq Python-2.5/Lib
232     E201 whitespace after '['
599     E202 whitespace before ')'
631     E203 whitespace before ','
842     E211 whitespace before '('
2531    E221 multiple spaces before operator
4473    E301 expected 1 blank line, found 0
4006    E302 expected 2 blank lines, found 1
165     E303 too many blank lines (4)
325     E401 multiple imports on one line
3615    E501 line too long (82 characters)
612     W601 .has_key() is deprecated, use 'in'
1188    W602 deprecated form of raising exception

提示:也别忘了看看其他模块,如 flake8、autopep8 或 pylint!

使用 coala 分析代码

coala 为多种语言提供 lint 与修复功能;在这里我们更关注 Python。您可以使用 pip 安装 coala:

$ pip3 install coala-bears

上面的代码块中,您实际上安装的是 coala-bears:bears 是插件或简单模块,用于扩展 coala 的能力,并随语言而异。本例中,您将使用 pep8bear,它可以定位不符合 PEP-8 的代码并就地修复。强烈建议您使用它来检查 Python 代码。

$ coala -S python.bears=PEP8Bear python.files=\*\*/\*.py \
python.default_actions=PEP8Bear:ApplyPatchAction --save
# other output ...
Executing section python...
[INFO][11:03:37] Applied 'ApplyPatchAction' for 'PEP8Bear'.
[INFO][11:03:37] Applied 'ApplyPatchAction' for 'PEP8Bear'.

pep8online:在线检查您的 Python 代码

除了实用的 pep8 模块与 coala 包,您还可以访问 pep8online 检查代码是否符合 PEP-8。该网站提供在线编辑器,您只需粘贴代码并点击“Check code”按钮!随后即可获得需要改进之处的反馈。简便又实用!

结语

在使用 Python 时,您有时可能为了尽快发布功能而忽略代码质量。然而,本教程中介绍的实践——以及更多未在此覆盖的内容——都应成为您开发—预发—测试—部署周期的一部分。这样做有利于项目相关人员理解代码,且多数情况下无需深挖或依赖调试器即可进行修改。如果您在参与开源项目,贡献者们会因 PEP-8 而受益,这是一条通用标准,每位 Python 开发者都在遵循,它也将帮助他们更好地理解您的代码。

现在您已经通读了本教程,非常建议亲自查看PEP-8!还有很多内容值得探索。

如果您对如何遵循 PEP-8 还有更多建议,或认为本文遗漏了重要内容,请在 @DataCamp 告诉我们。

主题
Python
数据科学

Python 课程

课程

Python 入门

4 小时
7.1M
只需 4 小时就能掌握 Python 数据分析基础知识。 本在线课程将介绍 Python 接口并探索常用包。
查看详情开始课程

创建您的免费账户

实践并学习路径你的进步。

使用 Google 继续
继续操作即表示您接受我们的《使用条款》和《隐私政策》,并同意您的数据存储在美国。
查看更多