Python编程高级技巧| 选择好的名称

本文摘自《Python高级编程(第2版)》第4章

在[异步图书]微信公众号,参与活动:转发本文到朋友圈截图给“异步图书后台”,并在文末留言说出你对本文的感想,12.15日我们将选出2名读者赠送本书。
https://mp.weixin.qq.com/s?__biz=MzA3NTIzMzIxNQ==&mid=2652796235&idx=1&sn=870cd7e14b40321f67aa05d6ca6571e8&chksm=8499779bb3eefe8d7473e9bdd0f3673bad7dd4d7ac957cc9f9a216786797a2a0a6eb042c530c#rd

大部分标准库在构建时都要考虑可用性。例如,内置类型的使用是很自然的,其设计非常易于使用。在这种情况下,Python可以与你开发程序时所思考的伪代码进行比较。大部分代码都可以大声朗读出来。例如,任何人都可以理解下面这个代码片段:

   my_list = []

   if 'd' not in my_list:
       my_list.append('d')

这就是编写Python比编写其他语言更加简单的原因之一。在编写程序时,你的思路可以快速转换成代码。

本章重点介绍编写易于理解和使用的代码的最佳实践,包括:

  • 使用PEP 8描述的命名约定。
  • 一组命名最佳实践。
  • 常用工具的简要介绍,这些工具可以让你检查是否遵守风格指南。

4.1 PEP 8与命名最佳实践

PEP 8为编写Python代码提供了一个风格指南。除了空格缩进、每行最大长度以及其他与代码布局有关的细节等基本规则之外,PEP 8还介绍了大部分代码库所遵循的命名约定。

本节给出了这一PEP的简要总结,并进一步给出了每种元素的命名最佳实践指南。但你仍然必须阅读PEP 8文档。

4.1.1 为何要遵守PEP 8以及何时遵守PEP 8

如果你正在创建一个打算开源的新软件包,那么答案很简单:始终遵守。PEP 8实际上是大多数Python开源软件的标准代码风格。如果你想接受来自其他程序员的任何协作,即使你对最佳代码风格指南有不同的看法,那么也应该坚持遵守PEP 8。这样做的好处是,其他程序员可以更容易地直接上手你的项目。对于新人来说,代码更容易阅读,因为它的风格与大多数其他Python开源包一致。

此外,开始时完全遵守PEP 8,可以让你在未来省时省事。如果你想向公众发布你的代码,最终其他程序员也会建议你切换到PEP 8。关于对某一特定项目是否真有必要这么做的争吵,可能会变成一场永无止境并且永远没有赢家的口水战(flame war)。这是令人悲伤的事实,但为了不失去贡献者,你最终可能还是会被迫与这种风格保持一致。

而且,如果整个项目的代码库处于成熟的开发状态,那么对其重新调整风格(restyling)可能需要做大量的工作。在某些情况下,重新调整风格可能需要修改几乎每行代码。虽然大多数修改可以自动化完成(缩进、换行和行尾空格),但这种大规模的代码检查通常会给所有基于分支的版本控制工作流程引入许多冲突。同时审查这么多修改也很困难。基于上述原因,许多开源项目都有一条规则:风格修改应该始终包含在单独的拉取/合并(pull/merge)请求或补丁中,而不影响任何功能或bug。

4.1.2 超越PEP 8——团队的风格指南

尽管PEP 8提供了一套全面的风格指南,但仍为开发者留有一些自由,特别是在嵌套数据字面量与需要很长的参数列表的多行函数调用方面。有些团队可能会认为他们需要额外的风格规则,最好的做法就是正式发布某种文件供所有团队成员使用。

此外,在某些情况下,对于没有定义风格指南的一些老项目,严格遵守PEP 8可能在经济上不可行。这样的项目仍然可以从正式发布的编码约定中受益,即使这些约定中没有体现PEP 8的官方规则。要记住,比遵守PEP 8更重要的是项目内的一致性。如果有正式发布的规则供每名程序员参考,那么在项目内和组织内保持一致性就简单多了。

4.2 命名风格

Python中使用的不同命名风格包括以下几种。

  • 驼峰式命名法(CamelCase)。
  • 混合式命名法(mixedCase)。
  • 大写(UPPERCASE)或大写加下划线(UPPER_CASE_WITH_UNDERSCORES)。
  • 前缀(leading)和后缀(trailing)下划线,有时是双下划线(doubled)。

小写元素和大写元素通常是一个单词,有时是几个单词连在一起。使用下划线的通常是缩写短语。使用一个单词要更好一些。前缀和后缀下划线用于标记私有元素和特殊元素。

这些风格被应用到以下几种情形。

  • 变量。
  • 函数和方法。
  • property
  • 类。
  • 模块。
  • 包。

变量

Python中有两种变量:

  • 常量。
  • 公有和私有变量。

1.常量

对于常量全局变量,使用大写加下划线。它告诉开发人员,指定的变量表示一个常数值。

 

Python中没有像C++中那样真正的常量——在C++中可以使用const。你可以修改任何变量的值。这就是Python使用命名约定将一个变量标记为常量的原因。

举个例子,doctest模块提供了一系列选项标记和指令(https://docs.python.org/3.5/ library/doctest.html#option-flags),它们都是短小的句子,清晰地定义了每个选项的用途:

   from doctest import IGNORE_EXCEPTION_DETAIL
   from doctest import REPORT_ONLY_FIRST_FAILURE

这些变量名称看起相当长,但清晰地描述它们也很重要。它们主要在初始化代码中使用,而不在代码主体中使用,所以这种冗长的名称并不会令人厌烦。

 

大部分情况下,缩写名称都会使代码含义变得模糊。如果缩写不够清晰,不要害怕使用完整的单词。

有些常量的名称也是由底层技术驱动的。例如,os模块使用C中定义的一些常量,例如EX_XXX系列定义了Unix退出代码编号。例如,同样的名称代码可以在系统的C头文件sysexits.h中找到,如下所示:

   import os
   import sys

   sys.exit(os.EX_SOFTWARE)

使用常量的另一个好的做法是,将它们集中放在使用它们的模块顶部,如果它们要用于下列操作,那么就将其组合在新的变量中:

   import doctest
   TEST_OPTIONS = (doctest.ELLIPSIS |
                   doctest.NORMALIZE_WHITESPACE |
                   doctest.REPORT_ONLY_FIRST_FAILURE)

2.命名和使用

常量用来定义程序所依赖的一组值,例如默认配置文件名。

好的做法是将所有常量集中放在包中的一个文件内。举个例子,Django采用的就是这种方法。一个名为settings.py的模块提供所有常量,如下所示:

   # config.py
   SQL_USER = 'tarek'
   SQL_PASSWORD = 'secret'
   SQL_URI = 'postgres://%s:%s@localhost/db' % (
       SQL_USER, SQL_PASSWORD
   )
   MAX_THREADS = 4

另一种方法是使用可以被ConfigParser模块或类似ZConfig(Zope中用于描述其配置文件的解析器)之类的高级工具解析的配置文件。但有些人认为,对于Python这种文件能够像文本文件一样轻松编辑和修改的语言来说,使用另一种文件格式可能是过分之举。

对于表现得像标记的选项,通常的做法是将它们和布尔运算结合起来,就像doctestre模块所做的那样。doctest中的模式很简单,如下所示:

   OPTIONS = {}

   def register_option(name):
       return OPTIONS.setdefault(name, 1 << len(OPTIONS))

   def has_option(options, name):
       return bool(options & name)

   # 现在定义选项
   BLUE = register_option('BLUE')
   RED = register_option('RED')
   WHITE = register_option('WHITE')

你将会得到下列结果:

>>> # 我们来尝试一下
>>> SET = BLUE | RED
>>> has_option(SET, BLUE)
True
>>> has_option(SET, WHITE)
False

在创建这样一组新的常量时,应避免对它们使用共同的前缀,除非模块中有多组常量。模块名称本身就是一个共同的前缀。另一种解决方法是使用内置enum模块的Enum类,并且依赖于set集合类型而不是二进制运算符。不幸的是,Enum类在面向旧Python版本的代码中应用有限,因为enum模块由Python 3.4版提供。

 

在Python中,使用二进制按位运算来合并选项是很常见的。使用OR(|)运算符可以将多个选项合并到一个整数中,而使用AND(&)运算符则可以检查该选项是否在整数中(参见has_option函数)。

3.公有和私有变量

对于可变的且可以通过导入自由访问的全局变量,如果它们需要被保护,那么应该使用带一个下划线的小写字母。但这种变量不经常使用,因为如果它们需要被保护,模块通常会提供getter和setter来处理。在这种情况下,一个前缀下划线可以将变量标记为包的私有元素,如下所示:

   _observers = []

   def add_observer(observer):
       _observers.append(observer)

   def get_observers():
       ""确保_observers不能被修改。"""
       return tuple(_observers)

位于函数和方法中的变量遵循相同的规则,并且永远不会被标记为私有,因为它们对上下文来说是局部变量。

对于类或实例变量而言,只在将变量作为公有签名的一部分不会带来任何有用信息或者冗余的情况下,才必须使用私有标记符(前缀下划线)。

换句话说,如果变量在方法内部使用,用来提供公有功能,并且只具有这个功能,那么最好将其设为私有。

例如,支持property的属性是很好的私有成员,如下所示:

   class Citizen(object):
       def __init__(self):
           self._message = 'Rosebud...'

       def _get_message(self):
           return self._message

       kane = property(_get_message)

另一个例子是用来记录内部状态的变量。这个值对其他代码没有用处,但却参与了类的行为,如下所示:

   class UnforgivingElephant(object):
       def __init__(self, name):
           self.name = name
           self._people_to_stomp_on = []

       def get_slapped_by(self, name):
           self._people_to_stomp_on.append(name)
           print('Ouch!')

       def revenge(self):
           print('10 years later...')
           for person in self._people_to_stomp_on:
               print('%s stomps on %s' % (self.name, person))

下面是在交互式会话中的运行结果:

>>> joe = UnforgivingElephant('Joe')
>>> joe.get_slapped_by('Tarek')
Ouch!
>>> joe.get_slapped_by('Bill')
Ouch!
>>> joe.revenge()
10 years later...
Joe stomps on Tarek
Joe stomps on Bill

4.函数和方法

函数和方法的名称应该使用小写加下划线。但在旧的标准库模块中并不总是这样。Python 3对标准库做了大量重组,所以大多数函数和方法都有一致的大小写。不过对于某些模块(例如threading)而言,你可以访问使用混合大小写(mixedCase)的旧的函数名称(例如currentThread)。留着它们是为了更容易向后兼容,但如果你不需要在旧版Python中运行代码,那么应该避免使用这些旧的名称。

这种方法的写法在小写范式成为标准之前很常见,一些框架(例如Zope和Twisted)对方法也使用混合大小写。使用它的开发者社区仍然相当多。因此,选择混合大小写还是小写加下划线,这取决于你所使用的库。

作为Zope的开发人员,保持一致性并不容易,因为构建一个混合纯Python模块和导入了Zope代码的模块的应用程序很困难。在Zope中,有一些类混用了两种约定,因为代码库仍在发展,而Zope开发人员想要采用多数人都接受的常用约定。

在这种类型的程序库环境中,得体的做法是只对暴露到框架中的元素使用混合大小写,而其他代码保持遵守PEP 8风格。

还值得注意的是,Twisted项目的开发人员采用一种完全不同的方法来解决这个问题。与Zope一样,Twisted项目早于PEP 8文档。项目启动时没有任何代码风格的官方指南,所以它有自己的风格指南。关于缩进、文档字符串、每行长度等风格规则可以很容易被采用。另一方面,修改所有代码以匹配PEP 8的命名约定,可能会完全破坏向后兼容。对于像Twisted这样的大型项目而言,这么做并不可行。因此Twisted尽可能遵守PEP 8,并将其余内容(例如变量、函数和方面的混合大小写)作为它自己的编码标准的一部分。这与PEP 8的建议完全兼容,因为它特别强调,在项目内的一致性比遵守PEP 8风格指南要更加重要。

5.关于私有元素的争论

对于私有方法和函数,惯例是添加一个前缀下划线。考虑到Python中的名称修饰(name-mangling)特性,这条规则是相当有争议的。如果一个方法有两个前缀下划线,它会在运行时被解释器重命名,以避免与任何子类中的方法产生命名冲突。

因此,有些人倾向于对私有属性使用双前缀下划线,以避免子类中的命名冲突:

   class Base(object):
       def __secret(self):
           print("don't tell")

       def public(self):
           self.__secret()

   class Derived(Base):
       def __secret(self):
           print("never ever")

你将会看到以下内容:

>>> Base.__secret
Traceback (most recent call last):
  File "", line 1, in 
AttributeError: type object 'Base' has no attribute '__secret'
>>> dir(Base)
['_Base__secret', ..., 'public']
>>> Derived().public()
don't tell

Python中名称修饰的最初目的不是提供类似C++的私有花招(gimmick),而是用来确保某些基类隐式地避免子类中的冲突,特别是在多重继承的上下文中。但将其用于每个属性则会使私有代码含义变得模糊,这一点也不Pythonic。

因此,有些人认为应该始终使用显式的名称修饰:

   class Base:
       def _Base_secret(self):  # 不要这么做!!!
           print("you told it ?")

这样会在所有代码中重复类名,所以应该首选__

但正如BDFL(Guido,the Benevolent Dictator For Life,参见http://en.wikipedia.org/wiki/ BDFL)所说,最佳做法是在编写子类中的方法之前查看该类的__mro__(方法解析顺序)值,从而避免使用名称修饰。修改基类的私有方法一定要小心。

关于这个主题的更多信息,许多年前在Python-Dev邮件列表中出现过一个有趣的讨论,人们争论名称修饰的实用性以及它在这门语言中的命运。你可以访问地网址:http://mail.python.org/ pipermail/python-dev/2005-December/058555.html查看。

6.特殊方法

特殊方法(https://docs.python.org/3/reference/datamodel.html#special-method-names)以双下划线开始和结束,常规的方法不应该使用这种约定。有些开发者曾经将其称为dunder方法,作为双下划线(double-underscore)的合成词。它们可用于运算符重载、容器定义等方面。为了保证可读性,它们应该集中放在类定义的开头:

   class WeirdInt(int):
       def __add__(self, other):
           return int.__add__(self, other) + 1

       def __repr__(self):
           return '' % self

       # 公共API
       def do_this(self):
           print('this')

       def do_that(self):
           print('that')

对于常规方法而言,你永远不应该使用这种名称。所以不要为方法创建这样的名称:

   class BadHabits:
       def __my_method__(self):
           print('ok')

7.参数

参数名称使用小写,如果需要的话可以加下划线。它们遵循与变量相同的命名规则。

8.property

property的名称使用小写或小写加下划线。大部分时候,它们表示一个对象的状态,可以是名词或形容词,如果需要的话也可以是如下简短的短语:

   class Connection:
       _connected = []

       def connect(self, user):
           self._connected.append(user)

       @property
       def connected_people(self):
           return ', '.join(self._connected)

在交互式会话中运行的结果如下所示:

>>> connection = Connection()
>>> connection.connect('Tarek')
>>> connection.connect('Shannon')
>>> print(connection.connected_people)
Tarek, Shannon

9.类

类名称始终采用驼峰式命名法,如果它们是模块的私有类,还可能有一个前缀下划线。

类和实例变量通常是名词短语,与用动词短语命名的方法名称构成使用逻辑:

   class Database:
       def open(self):
           pass

   class User:
       pass

下面是在交互式会话中的使用示例:

>>> user = User()
>>> db = Database()
>>> db.open()

10.模块和包

除了特殊模块__init__之外,模块名称都使用小写,不带下划线。

下面是标准库中的一些例子:

  • os
  • sys
  • shutil

如果模块是包的私有模块,则添加一个前缀下划线。编译过的C或C++模块名称通常带有一个下划线,并在纯Python模块中导入。

包名称遵循同样的规则,因为它的表现就像是更加结构化的模块。

4.3 命名指南

一组常用的命名规则可以被应用于变量、方法、函数和property。类和模块的名称也在命名空间的构建中具有重要的作用,从而也影响代码可读性。本迷你指南为挑选名称提供了常见的模式和反模式。

4.3.1 用“has”或“is”前缀命名布尔元素

如果一个元素保存的是布尔值,ishas前缀提供一种自然的方式,使其在命名空间中的可读性更强,代码如下:

   class DB:
       is_connected = False
       has_cache = False

4.3.2 用复数形式命名集合变量

如果一个元素保存的是集合变量,那么使用复数形式是一个好主意。有些映射在暴露为序列时也可以从中受益:

   class DB:
       connected_users = ['Tarek']
       tables = {
           'Customer': ['id', 'first_name', 'last_name']
       }

4.3.3 用显式名称命名字典

如果一个变量保存的是映射,那么你应该尽可能使用显式名称。例如,如果一个字典保存的是个人地址,那么可以将其命名为persons_addresses,代码如下:

   persons_addresses = {'Bill': '6565 Monty Road',
                        'Pamela': '45 Python street'}
   persons_addresses['Pamela']
   '45 Python street'

4.3.4 避免通用名称

如果你的代码不是在构建一种新的抽象数据类型,那么使用类似listdictsequenceelements等专用名词是有害的,即使对于局部变量也一样。它使得代码难以阅读、理解和使用。还应该避免使用内置名称,以避免在当前命名空间中将其屏蔽(shadowing)。还应该避免使用通用的动词,除非它们在该命名空间中有意义。

相反,应该使用领域特定的术语,如下所示:

   def compute(data):  # 太过通用
       for element in data:
           yield element ** 2

   def squares(numbers):  # 更好一些
       for number in numbers:
           yield number ** 2

还有一系列前缀和后缀,虽然在编程中非常常见,但事实上应该避免出现在函数和类名称中:

  • manager;
  • object;
  • do、handle或perform。

这样做的原因是它们的含义模糊、模棱两可,且没有向实际名称中添加任何信息。Jeff Atwood(Discourse 和Stack Overflow的联合创始人)关于这个话题写过一篇非常好的文章,你可以在他的博客上找到这篇文章:http://blog.codinghorror.com/i-shall-call-it-somethingmanager/。

还有许多包的名称应该避免。任何没有对其内容给出任何信息的名称,从长远来看都对项目有很大害处。诸如misctoolsutilscommoncore的名称有很大可能会变成一大堆不相关的、质量非常差的代码片段,其大小呈指数增长。在大多数情况下,这种模块的存在是懒惰或缺乏足够设计经验的迹象。热衷于这种模块名称的人可以预见未来,并将其重命名为trash(垃圾箱)或dumpster(垃圾桶),因为这正是他们的队友最终对这些模块的处理方式。

在大多数情况下,更多的小模块几乎总是更好,即使内容很少,但名称可以很好地反映其内容。说实话,类似utilscommon之类的名称没有本质错误,你可以负责任地使用它们。但现实表明,在许多情况下,它们都会变为危险的结构反模式,并且迅速增长。而且如果你的行动不够快,你可能永远无法摆脱它们。因此,最好的方法就是避免这样有风险的组织模式,如果项目其他人引入的话则将其扼杀在萌芽状态。

4.3.5 避免现有名称

使用上下文中已经存在的名称是不好的做法,因为它会导致阅读代码时——特别是调试时——非常混乱,例如以下代码:

>>> def bad_citizen():
...     os = 1
...     import pdb; pdb.set_trace()
...     return os
...
>>> bad_citizen()
>(4)bad_citizen()
(Pdb) os
1
(Pdb) import os
(Pdb) c

python2.5/os.pyc'>

在这个例子中,os名称被代码屏蔽。内置函数名称和来自标准库的模块名称都应该避免。

尽量使用原创的名称,即使是上下文的局部名称。对于关键字而言,后缀下划线是一种避免冲突的方法:

   def xapian_query(terms, or_=True):
       """if or_ is true, terms are combined with the OR clause"""
       ...

注意,class通常被替换为klasscls

   def factory(klass, *args, **kwargs):
       return klass(*args, **kwargs)

4.4 参数的最佳实践

函数和方法的签名是代码完整性的保证,它们驱动函数和方法的使用并构建其API。除了我们之前看到的命名规则之外,对参数也要特别小心。这可以通过3个简单的规则来实现。

  • 通过迭代设计构建参数。
  • 信任参数和测试。
  • 小心使用魔法参数*args**kwargs

4.4.1 通过迭代设计构建参数

如果每个函数都有一个固定的、定义明确的参数列表,那么代码的鲁棒性会更好。但这在第一个版本中无法完成,所以参数必须通过迭代设计来构建。它们应该反映创建该元素所针对的使用场景,并相应地逐渐发展。

例如,如果添加了一些参数,它们应该尽可能有默认值,以避免任何退化:

   class Service:  # 版本1
       def _query(self, query, type):
           print('done')

       def execute(self, query):
           self._query(query, 'EXECUTE')

   >>> Service().execute('my query')
   done

   import logging

   class Service(object):  # 版本2
       def _query(self, query, type, logger):
           logger('done')

       def execute(self, query, logger=logging.info):
           self._query(query, 'EXECUTE', logger)

   >>> Service().execute('my query')    # 旧式调用
   >>> Service().execute('my query', logging.warning)
   WARNING:root:done

如果一个公共元素的参数必须被修改,那么将使用一个deprecation进程,本节稍后将对此进行说明。

4.4.2 信任参数和测试

考虑到Python的动态类型特性,有些开发人员在函数和方法的顶部使用断言(assertion)来确保参数具有正确的内容,代码如下:

   def division(dividend, divisor):
       assert isinstance(dividend, (int, float))
       assert isinstance(divisor, (int, float))
       return dividend / divisor

   >>> division(2, 4)
   0.5
   >>> division(2, None)
   Traceback (most recent call last):
     File "", line 1, in 
     File "", line 3, in division
   AssertionError

这通常是那些习惯于静态类型、并且感觉Python中缺少点什么的开发者的做法。

这种检查参数的方法是契约式设计Design by Contract,DbC,参见http://en.wikipedia.org/ wiki/Design_By_Contract)编程风格的一部分。在这种设计中,在代码实际运行之间会检查先决条件。

这种方法有两个主要问题。

  • DbC的代码对应该如何使用它进行解释,导致其可读性降低。
  • 这可能使代码速度变慢,因为每次调用都要进行断言。

后者可以通过解释器的"-O"选项来避免。在这种情况下,在创建字节码之前,所有断言都将从代码中删除,这样检查也就会丢失。

在任何情况下,断言都必须小心进行,并且不应该用于使Python变成一种静态类型语言。唯一的使用场景就是保护代码不被无意义地调用。

在大多数情况下,健康的测试驱动开发(TDD)风格可以提供鲁棒性很好的基础代码。在这里,功能测试和单元测试验证了创建代码所针对的所有使用场景。

如果库中的代码被外部元素使用,那么进行断言可能是有用的,因为传入的数据可能会导致程序结束甚至造成破坏。这在处理数据库或文件系统的代码中可能发生。

另一种方法是模糊测试(fuzz testing,参见https://en.wikipedia.org/wiki/Fuzzing),它通过向程序发送随机的数据块来检测其弱点。如果发现了新的缺陷,代码会被修复以解决这一缺陷,并添加一次新的测试。

让我们来关注一个遵循TDD方法的代码库,它向正确的方向发展,每当出现新的缺陷时都会对其进行调整,从而鲁棒性越来越好。当它以正确的方式完成时,测试中的断言列表在某种程度上变得类似于先决条件列表。

4.4.3 小心使用*args**kwargs魔法参数

*args**kwargs参数可能会破坏函数或方法的鲁棒性。它们会使签名变得模糊,而且代码常常在不应该出现的地方构建小型的参数解析器,如下所示:

   def fuzzy_thing(**kwargs):

       if 'do_this' in kwargs:
           print('ok i did')

       if 'do_that' in kwargs:
           print('that is done')

       print('errr... ok')

   >>> fuzzy_thing(do_this=1)
   ok i did
   errr... ok
   >>> fuzzy_thing(do_that=1)
   that is done
   errr... ok
   >>> fuzzy_thing(hahaha=1)
   errr... ok

如果参数列表变得很长而且很复杂,那么添加魔法参数是很吸引人的。但这更表示它是一个脆弱的函数或方法,应该被分解或重构。

如果*args被用于处理元素序列(在函数中以相同方式处理),那么要求传入唯一的容器参数(例如iterator)会更好些,如下所示:

   def sum(*args):  # 可行
       total = 0
       for arg in args:
           total += arg
       return total

   def sum(sequence):  # 更好!
       total = 0
       for arg in sequence:
           total += arg
       return total

**kwargs适用于同样的规则。最好固定命名参数,使方法签名更有意义,如下所示:

   def make_sentence(**kwargs):
       noun = kwargs.get('noun', 'Bill')
       verb = kwargs.get('verb', 'is')
       adj = kwargs.get('adjective', 'happy')
       return '%s %s %s' % (noun, verb, adj)

   def make_sentence(noun='Bill', verb='is', adjective='happy'):
       return '%s %s %s' % (noun, verb, adjective)

另一种有趣的方法是创建一个容器类,将多个相关参数分组以提供执行上下文。这种结构与*args**kwargs不同,因为它可以提供能够操作数值并且能够独立发展的内部构件(internals)。使用它作为参数的代码将不必处理其内部构件。

例如,传入函数的Web请求通常由一个类实例表示。这个类负责保存Web服务器传入的数据,代码如下:

   def log_request(request):  # 版本1
       print(request.get('HTTP_REFERER', 'No referer'))

   def log_request(request):  # 版本2
       print(request.get('HTTP_REFERER', 'No referer'))
       print(request.get('HTTP_HOST', 'No host'))

魔法参数有时是无法避免的,特别是在元编程中。例如,想要创建能够处理任何类型签名的函数的装饰器,它是不可或缺的。更普遍地说,在处理对函数进行遍历的未知数据时,魔法参数都很好用,代码如下:

   import logging

   def log(**context):
       logging.info('Context is:\n%s\n' % str(context))

4.5 类的名称

类的名称必须简明、精确,并足以使人理解类的作用。常见的做法是使用后缀来表示其类型或特性。例如:

  • SQLEngine;
  • MimeTypes;
  • StringWidget;
  • TestCase。

对于基类或抽象类,可以使用一个BaseAbstract前缀,如下所示:

  • BaseCookie;
  • AbstractFormatter。

最重要的是要和类属性保持一致。例如,尽量避免类及其属性名称之间的冗余:

>>> SMTP.smtp_send()  # 命名空间中存在冗余信息
>>> SMTP.send()       # 可读性更强,也更易于记忆

4.6 模块和包的名称

模块和包的名称应体现其内容的目的。其名称应简短、使用小写字母、并且不带下划线:

  • sqlite
  • postgres
  • sha1

如果它们实现一个协议,那么通常会使用lib后缀,代码如下:

   import smtplib 
   import urllib 
   import telnetlib

它们还需要在命名空间中保持一致,这样使用起来更加简单,代码如下:

   from widgets.stringwidgets import TextWidget  # 不好
   from widgets.strings import TextWidget        # 更好

同样,应该始终避免使用与标准库模块相同的名称。

如果一个模块开始变得复杂,并且包含许多类,那么好的做法是创建一个包并将模块的元素划分到其他模块中。

__init__模块也可以用于将一些API放回顶层,因为它不会影响使用,但有助于将代码重新组织为更小的部分。例如,考虑foo包中的__init__模块,其内容如下所示:

   from .module1 import feature1, feature2
   from .module2 import feature3

这将允许用户直接导入特性,如下列代码所示:

   from foo import feature1, feature2, feature3

但要注意,这可能会增加循环依赖的可能性,并且在__init__模块中添加的代码将被实例化。所以要小心使用。

4.7 有用的工具

前面的约定和实践的一部分可以使用下列工具来控制和处理。

  • Pylint:一个非常灵活的源代码分析器。
  • pep8flake8:它们是小型的代码风格检查器,也是包装器,添加了一些更有用的特性,例如静态分析和复杂度测量。

4.7.1 Pylint

除了一些质量保证方面的度量之外,Pylint还允许你检查给定的源代码是否遵循某种命名约定。它的默认设置对应于PEP 8,Pylint脚本会提供一份shell报告输出。

要安装Pylint,你可以使用pip,代码如下:

   $ pip install pylint

安装完成后,pylint这个命令就可用了,可以在一个模块上运行,也可以利用通配符在多个模块上运行。我们在Buildout的bootstrap.py脚本上试用这个命令,代码如下:

$ wget -O bootstrap.py https://bootstrap.pypa.io/bootstrap-buildout.py -q
$ pylint bootstrap.py
No config file found, using default configuration
************* Module bootstrap
C: 76, 0: Unnecessary parens after 'print' keyword (superfluous-parens)
C: 31, 0: Invalid constant name "tmpeggs" (invalid-name)
C: 33, 0: Invalid constant name "usage" (invalid-name)
C: 45, 0: Invalid constant name "parser" (invalid-name)
C: 74, 0: Invalid constant name "options" (invalid-name)
C: 74, 9: Invalid constant name "args" (invalid-name)
C: 84, 4: Import "from urllib.request import urlopen" should be placed at
the top of the module (wrong-import-position)

...

Global evaluation
-----------------
Your code has been rated at 6.12/10

Pylint的实际输出要更长一些,这里只截取了其中一部分。

注意,Pylint可能会给出不好的评分或抱怨。例如,import语句没有被模块本身的代码使用,这在某些情况下是完全可以的(使其在命名空间中可用)。

如果一个库采用混合大小写为方法命名,那么对其调用可能也会降低评分。无论如何,总体评价并不那么重要。Pylint只是一个工具,指出可能的改进之处。

要想对Pylint进行微调,第一件要做的事就是,使用-generate-rcfile选项在项目目录下创建一个.pylintrc配置文件,如下所示:

$ pylint --generate-rcfile > .pylintrc

这个配置文件是自带说明的(self-documenting,每个选项都用注释说明),应该已经包含所有可用的配置选项。

除了检查是否遵守某种任意的编码标准,Pylint还可以给出有关整体代码质量的额外信息,例如:

  • 代码重复度量。
  • 未使用的变量和导入。
  • 缺失的函数、方法或类的文档字符串。
  • 函数签名过长。

默认启用的可用检查列表非常长。重要的是要知道,有些规则是任意的,不能轻易应用到所有代码库。要记住,一致性永远比遵守某种任意的标准更有价值。幸运的是,Pylint是可调节的,所以如果你的团队使用一些与默认不同的命名和编码约定,你可以轻松配置来检查与这些约定的一致性。

4.7.2 pep8flake8

pep8这个工具只有一个目的:它仅提供对PEP 8代码约定的风格检查。这是它与Pylint的主要区别,后者具有许多额外的功能。对于那些仅对PEP 8标准的自动化代码风格检查感兴趣的程序员来说,这是最佳选择,不需要任何额外的工具配置(像Pylint那样)。

pep8可以用pip安装,代码如下:

$ pip install pep8

在Buildout的bootstrap.py脚本上运行pep8,它会给出不符合代码风格之处的简短列表:

$ wget -O bootstrap.py https://bootstrap.pypa.io/bootstrap-buildout.py -q
$ pep8 bootstrap.py
bootstrap.py:118:1: E402 module level import not at top of file
bootstrap.py:119:1: E402 module level import not at top of file
bootstrap.py:190:1: E402 module level import not at top of file
bootstrap.py:200:1: E402 module level import not at top of file

与Pylint的输出的主要区别在于其长度。pep8只关注风格,所以它不会给出任何其他警告,例如未使用的变量、太长的函数名称或文档字符串缺失。它也不会给出任何评分。它真的很有意义,因为不存在部分一致性。任何对风格指南的违背——即使是最小的违背——也会使代码立刻变得不一致。

pep8的输出比PyLint更简单,也更容易解析,所以如果你想要与一些连续集成解决方案(例如Jenkins)集成,那么选择pep8可能更好。如果你想要一些静态分析的功能,那么可以使用flake8包,它是pep8和其他一些工具的包装器,可以轻松扩展,并提供了更丰富的功能,包括:

  • McCabe复杂度测量。
  • 利用pyflakes做静态分析。
  • 利用注释禁用整个文件或单行代码。

4.8 小结

本章通过Python官方风格指南(PEP 8文档)来介绍广受认可的编码约定。除了官方风格指南,介绍了一些命名建议,可以让你以后的代码更加明确;还介绍了一些有用的工具,在保持代码风格一致方面不可或缺。

所有这些内容都是为本书第一个实用主题做准备——编写并分发Python包。下一章我们将学习如何在公共PyPI仓库中发布我们自己的包,以及在私人组织中如何利用打包生态系统的力量。

Python编程高级技巧| 选择好的名称_第1张图片

在“异步图书”后台回复“关注”,即可免费获得2000门在线视频课程;推荐朋友关注根据提示获取赠书链接,免费得异步图书一本。赶紧来参加哦!

扫一扫上方二维码,回复“关注”参与活动!
点击阅读原文,购买《Python高级编程(第2版)》

你可能感兴趣的:(Python)