为了账号安全,请及时绑定邮箱和手机立即绑定
慕课专栏

目录

索引目录

Python 核心技术精讲

限时优惠 ¥ 58.00

原价 ¥ 78.00

07月23日后恢复原价

限时优惠
立即订阅
04 无规矩不成方圆:走进 PEP 8,掌握 Python 编码规范(下)
更新时间:2020-06-22 22:33:41
只有在那崎岖的小路上不畏艰险奋勇攀登的人,才有希望达到光辉的顶点。

——马克思

这节课,我们继续接上文对 PEP 8 规范进行解读:

注释编写

注释对于代码的阅读、扩展以及维护都非常重要。在 Python 中常用的注释方式有三种,分别为块注释( Block Comments )、行内注释( Inline Comments )、文档字符串( Documentation Strings )。首先我们需要了解的是在注释编写过程中的一些通用原则,这些原则不管针对哪种注释方式,都是值得参考的。

  • 应注重注释的可读性和完整性,这样有助于代码后续的扩展和维护;
  • 应注重注释的实时性,即代码在修改或扩展过程中,及时更新对应的注释;
  • 应优先使用英文编写注释。

以上三点从概括的角度阐述了编写注释时的一些主要思想。而在实际的编写场景中,还有一些通用的建议可以参考,比如:

  • 注释应为完整的句子;
  • 当注释的开头不为以小写字母开始的标识符时,应大写首字母;
  • 在包含多个句子的注释中,在非结尾句的结尾处可以使用两个空格等。

更多的细节可参考 “Strunk and White” 中的写作风格( “Strunk and White” 指的是《The Elements of Style》,为著名的美式英语写作风格指南)。下面我们便开始针对每一种注释方式分别来了解对应的一些注释编写技巧:

块注释( Block Comments )

块注释在代码内部比较常见,其通常由一至多个段落构成,段落之间应使用开头为 # 的空行隔开,每个段落由完整的句子构成,在每行以 # 和一个空格开始。另外,块注释和被注释代码之间应具有同级别的缩进,这样有助于区分注释和代码之间关联关系。

通常我们提到的这些细节和准则在 Python 标准库的编写中往往做的更好,我们以 Python 标准库 collectionsCounter 对象的 fromkeys 方法的注释来进行示例说明,这段注释主要说明了在 Counter 类中没有定义 fromkeys 方法的原因。

@classmethod
def fromkeys(cls, iterable, v=None):
    # There is no equivalent method for counters because the semantics
    # would be ambiguous in cases such as Counter.fromkeys('aaabbc', v=2).
    # Initializing counters to zero values isn't necessary because zero
    # is already the default value for counter lookups.  Initializing
    # to one is easily accomplished with Counter(set(iterable)).  For
    # more exotic cases, create a dictionary first using a dictionary
    # comprehension or dict.fromkeys().
    raise NotImplementedError(
        'Counter.fromkeys() is undefined.  Use Counter(iterable) instead.')

行内注释( Inline Comments )

行内注释是一种形式相对简单的注释,它和表达式或语句位于同一行,之间应通常使用两个空格隔开,注释部分应以 # 和一个空格开始。需要注意的一点是,在编写行内注释时,其描述要尽量明确,对于一些代码所表示的含义或逻辑显而易见时,行内注释没有必要再对其进行复述。

# 不提倡下面的方式
my_list = []  # Make a list

文档字符串( Documentation Strings )

在 Python 中,最重要的注释形式便是文档字符串( Documentation Strings ),我们也常称之为 “docstrings”,一方面用来对模块、类、函数、方法等进行说明;另一方面可以配合一些辅助工具来自动化生成代码文档(关于这部分,我们会在后面的小节中专门说明)。

我们现在或许还不能完全意识到文档字符串的重要性,但如果你了解开源软件的话,你一定会知道一个开源软件文档质量的好坏有时会决定它是否会流行起来。

对于文档字符串的形式来说,它通常是模块、类、方法、函数定义中的首个语句,使用类似 """docstrings""" 这样的形式。若在文档字符串中存在 \,可使用 r"""docstrings""" 形式;若在文档字符串中使用 unicode,可使用 u"""docstrings""" 。对于所有模块、被另外的模块导入的类或函数、类中的公共方法(包括构造函数),均应编写文档字符串。对于包来说,也可在其 __init__.py 中编写文档字符串。

单行文档字符串结构较为简单,在单行文档字符串后不应有空行,避免对显而易见的含义进行复述。

>>> def function_name():
...     """docstrings"""
...     pass
... 

对于多行文档字符串来说,它的结构相对复杂,主要由引号、摘要行、空行、文档描述组成。整个文档字符串的缩进和代码首部缩进级别应相同,摘要行和首部引号可在同一行,也可在首部引号下一行,尾部引号单独成行。

对于普通类,其文档字符串可包含其实例属性、公共方法等的简要说明,在其文档字符串后可增加一个空行;对于继承于父类的子类,除了类的文档字符串中的内容外,可说明子类和父类的差异,比如对于重写父类的方法可使用 override 结合其他内容进行表示,对于调用父类方法并进行内容扩展的类方法,可使用 extend 结合其他内容进行表示;对于函数、方法,其文档字符串可包含参数、返回值说明、有可能抛出的异常以及功能等的简要说明;对于模块的文档字符串,可包含模块中可导入的类、函数等的简要说明;

对于包,其文档字符串可包含可导入的子包、模块等的简要说明(在 PEP 257 详细的描述了文档字符串的更多细节,感兴趣的同学可以查阅)。在这里我们分别以以 Python 标准库 os 中的一段文档字符串和第三方库 Tornado 中的一段文档字符串来进行示例说明:

# os
def makedirs(name, mode=0o777, exist_ok=False):
    """makedirs(name [, mode=0o777][, exist_ok=False])

    Super-mkdir; create a leaf directory and all intermediate ones.  Works like
    mkdir, except that any intermediate path segment (not just the rightmost)
    will be created if it does not exist. If the target directory already
    exists, raise an OSError if exist_ok is False. Otherwise no exception is
    raised.  This is recursive.

    """
    pass

在 Tornado 的文档字符串中,我们可以看到反引号 ``…`` 这样的写法(以及在我们没有列举的一些文档字符串中也有双星号 **…**、 星号 *…* 写法),这种写法和 reStructuredText 标记语言有关,在开源软件的文档编写中非常常用,在后面 sphinx 相关的小节中,我们再来详细说明。

# Tornado
def import_object(name: str) -> Any:
    """Imports an object by name.

    ``import_object('x')`` is equivalent to ``import x``.
    ``import_object('x.y.z')`` is equivalent to ``from x.y import z``.

    >>> import tornado.escape
    >>> import_object('tornado.escape') is tornado.escape
    True
    >>> import_object('tornado.escape.utf8') is tornado.escape.utf8
    True
    >>> import_object('tornado') is tornado
    True
    >>> import_object('tornado.missing_module')
    Traceback (most recent call last):
        ...
    ImportError: No module named missing_module
    """
    if name.count(".") == 0:
        return __import__(name)

    parts = name.split(".")
    obj = __import__(".".join(parts[:-1]), fromlist=[parts[-1]])
    try:
        return getattr(obj, parts[-1])
    except AttributeError:
        raise ImportError("No module named %s" % parts[-1])

在 PyCharm 中我们很方便的编写函数或方法的文档字符串,比如我们编写了这种形式的函数后:

def function_name(var_one, var_two: bool = False):
    """"""
    pass

只需要在引号中间回车,PyCharm 会帮助我们生成一个标准的结构,其包含必要的信息:

def function_name(var_one, var_two: bool = False):
    """
    
    :param var_one: 
    :param var_two: 
    :return: 
    """
    pass

另外,文档字符串可通过对象的 __doc__ 属性查看,同时也可借助 Python 的内置函数 help() 来查看。同时,编写良好的文档字符串还可以结合自动化工具帮助我们生成代码文档,比如 Python 自带的 pydoc 命令,上面提到的第三方工具 sphinx 等。

>>> def function_name():
...     """docstrings"""
...     pass
... 
>>> function_name.__doc__
'docstrings'
>>> help(function_name)
Help on function function_name in module __main__:

function_name()
    docstrings

命名规范

命名规范在代码的设计中是一个相当具有扩展性的话题,我们在这里会了解到其中主要的命名规则和 Python 中针对不同元素的常用命名方式。在这些基本的准则基础上,后续我们仍需要对一些高质量开源代码进行阅读,因为编写这些代码的业内大拿们往往在命名背后都有一套能够支撑其选择该命名的理论,在这个过程中我们会不断积累经验,逐步的理解在代码中如何进行一个好的命名。

常见的命名规则

  • 匈牙利命名法:以一至多个小写字母表示其属性、类型,后接首字母大写的一至多个单词表示其作用描述。比如 m_bCanRun,其中 m_ 表示其为成员变量,b 表示其类型为布尔值,CanRun 表示其代表是否可以检查的含义。
  • 驼峰命名法:以一至多个逻辑单元构成,每个逻辑单元可称为一个识别字,首个识别字首字母小写,其余识别字首字母大写,比如 canRun,由于首字母小写,这种形式也被称为小驼峰命名法。
  • 帕斯卡命名法:和驼峰命名法类似,区别为首字母大写,如 CanRun,也被称为大驼峰命名法。
  • 下划线命名法:和驼峰命名法类似,区别为所有识别字均小写,识别字之间使用 _ 连接,比如 can_run

除此之外,还有其他形式的命名规则,比如大写 CANRUN、大写结合下划线 CAN_RUN、小写 canrun 等。另外在 Python 中也有前缀或者后缀( leading or tailing )下划线、双下划线等命名方式,在 Linux 代码中也存在对于一组相关方法使用相同的前缀的形式命名形式,比如 c_runc_stop 等。

Python 常用的命名方式

通用准则

  • 无论你编写的模块、类、还是函数,其中对外暴露出的公共使用部分的命名应从使用场景出发。比如 os 模块中 makedirsremovedirs 等函数的命名,我们可以清楚的知道这些函数的使用场景,而不用关心其内部实现;
  • 尽量使用完整的、准确的、易于理解的、具有明确目的的单词来命名,或者使用下划线将可以表达完整含义的单词(或缩写,但缩写往往会造成不明确)进行连接(哪怕这样会很长,Explicit is better than implicit );
  • 避免使用容易混淆的字符,比如 l( L 的小写)、O( o 的大写)等;
  • 避免采用内置名称、关键字和已经使用过的名称,避免采用过于通用的名称,前者会造成原有功能的屏蔽,后者会造成含义过于广泛而失去明确性;
  • 与此同时,还有观点表示应避免使用 toolsutilscommon 等名称,以及避免使用以 objectmanager / managementhandler 等作为后缀的名称,这些观点中将其称之为反模式,认为这些名称没有起到实际的意义,并且类似 utils 的命名反而最终会成为劣质代码的聚集地,并且将这种名称认为是缺乏设计的名称,我们在这里将这些观点当作一种扩展阅读即可;

对于变量(和常量)

  • 变量名可小写,也可使用下划线命名;
  • 类型变量名(常用于类型提示)应使用大驼峰命名(关于类型提示,后面的课程中会详细讲解);
  • 常量应使用全大写字母,必要时使用下划线分隔,并且要注意在 Python 中并没有类似其他语言中 const 的概念,常量仅仅是一种约定(常量通常放置在代码顶部、或单独的模块、或特定的配置文件中)。对于一组常量,不应使用同一个前缀(这也适用于在同一个类中的方法或者属性),因为这样会和模块名称造成冗余,若多组常量,则针对每一组常量可以使用同一前缀。
  • 对于容器类变量,常采用复数名词形式;对于映射类变量,常采用 key_value 的形式,其中 keyvalue 为键和值的实际含义。
  • 对于表示布尔值的变量或者常量(但不仅限于这两者),常采用 hasis 作为前缀。

对于类(和异常)

  • 类名应使用大驼峰命名(这里不包括一些 Python 内置的类,比如 intlist ),类和属性常采用名词,方法多采用动词或者包含动词。
  • 基类常采用 Base 作为前缀,抽象类常采用 Abstract 作为前缀。
  • 异常名应使用大驼峰命名法,当此异常表示一个错误时,应添加 Error 后缀(并不是所有异常都代表代码运行错误,比如 KeyboardInterruptSystemExit )。

对于函数、方法

  • 函数名、方法名应使用小写,也可使用下划线命名(但我们仍会在一些代码中看到一些方法或者函数名称采用了驼峰命名法,甚至在 Python 的标准库中也存在这样的现象,比如在 threading 模块中,因为这些代码的出现往往早于 PEP 8 规范的诞生,同时为了向后兼容而保留,但通常这些模块都会提供相同功能的以小写加下划线命名的方法,我们应该尽可能使用这些新的方法)。
  • 实例方法的首个参数名应为 self ,类方法的首个参数应为 clsclass 作为关键字不能被使用,常被 clsklass 替换);

对于模块和包

  • 模块( modules )名应尽可能使用小写,在必要时可以使用下划线命名(除了 __init__ 模块以外),当使用 C/C++ 编写扩展模块时,通常需要添加下划线前缀;
  • 包( packages )名应使用小写,最好不要使用下划线;

特殊格式

  • 单下划线前缀,比如 _name ,常称为 ”伪私有属性“(也可用于私有的方法、类等),这种命名方式在 Python 中是一种表示私有属性的约定(同时注意 from ... import * 时不会导入这种形式的对象,并且这种伪私有变量通常会通过特定的方法来获取或者赋值),私有属性通常没有直接对外的功能,常用于记录内部状态或用于提供一些公共功能的方法内使用;

  • 单下划线后缀,比如 name_ ,常用于避免和 Python 的关键字发生冲突;

  • 双下划线前缀,比如 __name ,常用于基类避免和子类中的命名冲突,Python 会通过转换规则将其转换为类似 _Class__name 的形式,这种方式通常称为命名修饰 name mangling 或 name decoration(这种方式通常用于多继承,应避免将这种形式用在私有属性的命名上);

    >>> class MyClass:
    ...     __name = 1
    ...
    >>> MyClass.__name  # 无法直接访问
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    AttributeError: type object 'MyClass' has no attribute '__name'
    >>> MyClass._MyClass__name
    1
    
  • 前后双下划线,比如 __name__ ,这是我们之前提到过的 Python 内部常用的 dunder 名称,不应自定义这类命名。

代码检查与格式化工具

PyCharm 默认的检查和格式化工具

PyCharm 中已经内置了 pycodestyle.py 进行检查,在 PyCharm 右下角或在 Settings 中搜索 Inspections :

图片描述

当然,你也可以在选择忽略某些检查提示,在 Ignore errors 中添加或者在 IDE 中检查提示浮窗中选择 More actions…,点击 Ignore errors like this。

图片描述

代码检查工具 Pylint

pylint 是一款代码检查与分析工具(类似的可以进行静态检查的工具还有 flake8 等),它不仅可以对代码中违反 PEP 8 的部分进行检查,还可以对代码中常见的错误进行静态分析。同时,pylint 可以很方便的集成进 PyCharm 中。

代码格式化工具 Black

black 是一款相对较新的代码格式化工具(类似的格式化工具还有 autopep8、yapf 等),配置项少是它最大的特点,这通常对于刚开始学习 Python 编码规范的读者来说是一件好事,不必关注太多细节便可以写出相对规范的 Python 代码。

与 PEP 8 结合形成团队规范

在一个软件团队中,编码规范的一致性是非常重要的,一个团队中的所有成员都需要共同遵守该规范。对于这个规范,通常有两个原则,首先是团队内部一致,其次是在条件允许的范围内尽可能和 PEP 8 一致。那我们应该如何根据项目的实际情况来和 PEP 8 结合形成团队的代码基本规范呢?

项目初期

如果在项目初期,完全遵守 PEP 8 或者对其做一些结合团队内部实际情况的小幅度调整(比如适当拓宽行最大长度等)即可,这样不仅有利于提高团队内部编码规范的一致性,有助于团队内部的协作,提高代码本身的可读性,也降低了未来和开源社区接轨的难度。

项目成熟

如果项目已经基本成熟,并且在项目初期没有遵循 PEP 8 指南,一种做法和上面提到的情况相同,完全遵守 PEP 8 的规范或者做小幅度调整,那么这样意味着需要全盘按照 PEP 8 规范进行改造,这种方式所带来的工作量会相当巨大(哪怕是借助一些成熟的工具)。

因为这种调整往往会伴随着其他功能的开发,可能会涉及到大量版本管理相关的分支冲突,尤其是在较复杂的大型项目中。在这种情况下,有两种解决方式:

  • 在每个独立的功能开发或者问题修复中进行规范的调整,可以在一次提交中仅修改一个独立代码功能块的编码风格或者在完成其他必要工作的过程中顺带进行该部分的编码规范调整,这样来将成本和影响降到最低。
  • 另外一种做法是将原有规范和 PEP8 结合,即保留原有规范,剩余部分均采用 PEP8 规范,对所有以前和未来的代码均采用这一种定制化的规范,这种做法的好处便是项目中只存在一种规范,但缺点便是和 PEP8 产生了一定分歧。还有一种比较常见的方式便是在代码核心部分不做修改,依然采取项目之前的规范,在其他部分中遵循 PEP8 规范,那么这也意味着在项目会存在两种规范。

项目发布

如果你的项目已经作为框架或者公共组件发布,除了以上两点之外,你还需要考虑向后兼容,这时可以参考我们上面提到的类似 threading 模块的做法。

总结

本小节中,我们对 Python 编码规范进行了较为详细的讲解,从多个方面入手着重讲解了 PEP 8 的核心内容。同时在编码规范的基础上,我们还对一些流行度较高的工具进行了介绍,它们的安装使用都非常方便,希望大家可以在之后多尝试。作为一个软件工程师,写出一手规范、标准的代码,都是对自己、对协作者最大的负责。希望大家不仅可以理解并掌握 PEP 8,而且还能将其用以指导自己进行编码实践,不断提高编码的规范性。

}
限时优惠 ¥ 58.00 ¥ 78.00

你正在阅读课程试读内容,订阅后解锁课程全部内容

千学不如一看,千看不如一练

手机
阅读

扫一扫 手机阅读

Python 核心技术精讲
限时优惠 ¥ 58.00 ¥ 78.00

举报

0/150
提交
取消