针对每条注意点的细节,可以给本篇博客评论,博主本人会针对评论给出示例或相应讲解。 外文文献可参考:Effective Python——59 Specific Ways to Write Better Python, author by Brett Slatkin.

# 用 Pythonic 方式来思考

# 1. 了解 bytes、str 和 unicode 的区别
  • python 3 中,bytes 是一种包含8位值得序列,str 是一种包含Unicode字符的序列,开发者不能以>或+等操作符来混同操作 bytes 和 str 示例。
  • python 2 中,str 是一种包含8位值的序列,unicode 是一种包含 Unicode 字符的序列。如果 str 只含有 7 位 ASCII 字符,那么可以通过相关的操作符来同时使用 str 和 unicode。
  • 在对输入的数据进行操作之前,使用辅助函数来保证字符序列的类型与开发者的期望相符。
  • 从文件中读取二进制数据,或向其中写入二进制数据时,总应该以 ‘rb’ 或 ‘wb’ 等二进制模式来开启文件。
# 2. 用辅助函数来取代复杂的表达式
  • 开发者很容易过度运用Python的语法特性,从而写出那种特别复杂并且南艺理解的单行表达式
  • 请把复杂表达式一如辅助函数中,如果要反复使用相同的逻辑,那就更应该这么做。
  • 使用 if/else 表达式,要比用 or 或者 and 这样的 Boolean 操作符写成的表达式更加清晰。
# 3. 了解切割序列的方法
  • 不要写多月的代码:当 start 索引为0,或 end 索引为序列长度时,应该将其省略。
  • 切片操作不会计较 start 与 end 索引是否越界,这使得我们很容易就能从序列的前端或后端开始,对其进行范围固定的切片操作(如 a[:20] 或 a[-20:])
  • 对 list 赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替换成新值,即便它们的长度不同也依然可以替换。
# 4. 在单次切片操作内,不要同时指定 start、end 和 stride
  • 既有 start 和 end,又有 stride 的切割操作,可能会另人费解。
  • 尽量使用 stride 为正数,且不带 start 和 end 索引的切割操作。尽量避免负数作为 stride 。
  • 在同一个切片操作内,不要同时使用 start 、end 和 stride。如果确实需要执行这种操作,那就考虑将其拆解为两条赋值语句,其中一条做范围切割,另一条做步进切割,或考虑使用内置 itertools 模块中的 islice。
# 5. 用列表推导取代 map 和 filter
  • 列表推导要比内置的 map 和 filter 函数清晰,因为它无需额外编写 lambda 表达式。
  • 列表推导可以跳过输入列表中某些元素,如果改用 map 来做,那就必须以 filter 方能实现。
  • 字典与集合也支持推导表达式。
# 6. 不要使用含两个以上表达式的列表推导
  • 列表提到支持多级循环,每一级循环也支持多项条件。
  • 超过两个表达式的列表推导是很难理解的,应该尽量避免。
# 7. 用生成器表达式来改写数据量较大的列表推导
  • 当输入的数据量较大时,列表推导可能会因为占用太多内存而出问题。
  • 由生成器表达式所返回的迭代器,可以逐次产生输出值,从而避免了内存用量问题。
  • 把某个生成器表达式所返回的迭代器,放在另一个生成器表达式的 for 子表达式中,即可将二者组合起来。
  • 串在一起的生成器表达式执行速度很快。
# 8. 尽量用 enumerate 取代 range
  • enumerate 函数提供了一种精简写法,可以在遍历迭代器时获知每个元素的索引。
  • 尽量用 enumerate 来改写那种将 range 与下标访问相结合的序列遍历代码。
  • 可以给 enumerate 提供第二个参数,以指定开始计数时所用的值(默认为0)。
# 9. 用 zip 函数同时遍历两个迭代器
  • 内置的 zip 函数可以平行地遍历多个迭代器。
  • Python 3 中的 zip 相当于生成器,会在遍历过程中逐次产生元组,而 Python 2 中的 zip 则是直接把这些元组完全生成好,并一次性地返回整份列表。
  • 如果提供的迭代器长度不等,那么 zip 就会自动提前终止。
  • itertools 内置模块中的 zip_longest 函数可以平行地遍历多个迭代器,而不用在乎它们的长度是否相等。
# 10. 不要在 for 和 while 循环后写 else 块
  • python 有特殊语法,可在 for 及 while 循环内部语句块之后紧跟一个 else 块。
  • 只有当整个循环体都没遇到 break 语句时,循环后面的 else 块才会执行。
  • 不要在循环后面使用 else 块,因为这种写法不直观,且又容易引人误解。
# 11. 合理利用 try/except/else/finally 结构中的每个代码块
  • 无论 try 块是否发生异常,都可利用 try/finally 复合语句中的 finally 块来执行清理工作。
  • else 块可以用来缩减 try 块中的代码量,并把没有发生异常时所要执行的语句与 try/except 代码块隔开。
  • 顺利运行 try 块后,若想使某些操作能在 finally 块的清理代码之前执行,则可将这些操作写到 else 块中。(这种写法必须要有 except 块)

# 函数

# 1. 尽量用异常来表示特殊情况,而不要返回 None
  • 用 None 这个返回值来表示特殊意义的函数,很容易使调用者犯错,因为 None 和 0 及空字符串之类的值,在条件表达式里都会评估为 False。
  • 函数在遇到特殊情况时,应该抛出异常,而不要返回 None 。调用者看到该函数的文档中所描述的异常之后,应该就会编写相应的代码来处理它们了。
# 2. 了解如何在闭包里使用外围作用域中的变量
  • python 解释器遍历作用域顺序:
  1. 当前函数作用域。
  2. 任何外围作用域(例如,包含当前函数的其他函数)。
  3. 包含当前代码的那个模块的作用域(也叫全局作用域,global scope)。
  4. 内置作用域(也就是包含 len 及 str 等函数的那个作用域)。 如果上述域中没有定义变量,则会抛出 NameError 异常。
  • 获取闭包内的数据:可以通过 nonlocal [变量名] 来获取,但是限制在于不能延伸到模块级别,为了防止污染全局作用域。
  • 优化 nonlocal 代码,使用名叫 _call_ 的特殊方法,该方法暗示了其所在类的用途,并且其所在类是一个带有状态的闭包,程序员自定义实现方法功能。
  • 对于定义在某作用域内的闭包来说,它可以引用这些作用域中的变量。
  • 使用默认方法对闭包内的变量赋值,不会影响外围作用域中的同名变量。
  • 在 Python 3 中,程序可以在闭包内用 nonlocal 语句来修饰某个名称,是该闭包能够修改外围作用域中的同名变量。
  • 在 Python 2 中,程序可以使用可变值(例如,包含单个元素的列表)来实现与 nonlocal 语句相仿的机制。
  • 除了那种比较简单的函数,尽量不要用 nonlocal 语句。
# 3. 考虑用生成器来改写直接返回列表的函数
  • 使用生成器比把收集到的结果放入列表里返回给调用者更加清晰。
  • 由生成器函数所返回的那个迭代器,可以把生成器函数体中,传给 yield 表达式的那些值,逐次产生出来。
  • 无论输入量有多大,生成器都能产生一系列输出,因为这些输入量和输出量,都不会影响它在执行时所占的内存。
# 4. 在使用上一点中迭代时的注意点
  • 函数在输入的参数上面多次迭代时要注意:如果参数是迭代器,那么可能会导致奇怪的行为并错失某些值。
  • Python 的迭代器协议,描述了容器和迭代器应该如何与 iter 和 next 内置函数、for 循环及相关表达式相互配合。
  • 把 _iter_ 方法实现为生成器,可以拿该值为参数,两次调用 iter 函数,若结果相同,则是迭代器,调用内置的 next 函数,即可令该迭代器前进一步。
# 5. 用数量可变的位置参数减少视觉赞讯(及减少杂乱,强调重要内容)
  • 在 def 语句中使用 *args,即可领函数接受数量可变的位置参数。
  • 调用函数时,可以采用 * 操作符,把序列中的元素当成位置参数,传给该函数。
  • 对生成器使用 * 操作符,可能导致程序耗尽内存并崩溃。
  • 在已经接受 *args 参数的函数上面继续添加位置参数,可能会产生难以排查的 bug。
# 6. 用关键字参数来表达可选的行为
  • 函数参数可以按位置或关键字来指定。
  • Python 函数中的所有位置参数,都可以按关键字传递。
  • 只使用位置参数来调用函数,可能会导致这些参数值的含义不够明确,而关键字参数则能阐明每个参数的意图。
  • 给函数添加新的行为时,可以使用带默认值的关键字参数,以便与原有的函数调用代码保持兼容。
  • 可选的关键字参数,总是应该以关键字形式来指定,而不应该以位置参数的形式来指定。
# 7. 用 None 和文档字符串来描述具有动态默认值的参数
  • 参数的默认值,只会在程序加载模块并读到本函数的定义时评估一次。对于 {} 或 [] 等动态的值,这可能会导致奇怪的行为。
  • 对于以动态值作为实际默认值的关键字来说,应该把形式上的默认值写为 None,并在函数的文档字符串里描述该默认值所对应的的实际行为。
# 8. 用只能以关键字形式指定的参数来确保代码清晰
  • 关键字参数能够使函数调用的意图更加明确。
  • 对于各参数之间很容易混淆的函数,可以声明只能以关键字形式指定的参数,以确保调用者必须通过关键字来指定它们。对于接受多个 Boolean 标志的函数,更应该这样做。
  • 在编写函数时,Python 3 有明确的语法来定义这种只能以关键字形式指定的参数。
  • Python 2 的函数可以接受 **kwargs 参数,并手工抛出 TypeError 异常,以便模拟只能以关键字形式来指定的参数。
  • 声明只能以关键字指定参数方式:def xx_function(args1, args2, *, xxx1=True, xxx2=False)。

# 类与继承

# 1. 尽量用辅助类来维护程序的状态,而不要用字典和元组
  • namedtuple的局限:
  1. namedtuple 类无法指定各参数的默认值。
  2. namedtuple 实例的各项属性,依然可以通过下标及迭代来访问。这可能导致其他人以不符合设计者意图的方式使用这些元组。(尤其是公布给外界使用的 API 来说)
  • 不要使用包含其他字典的字典,也不要使用过长的元组。
  • 如果容器中包含简单而又不可变的数据,那么可以优先使用 namedtuple 来表示,待稍后有需要时,再修改为完整的类。
  • 保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆解为多个辅助类。
# 2. 简单的接口应该接受函数,而不是类的实例
  • 对于连接各种 Python 组件的简单接口来说,通常应该给其直接传入函数,而不是先定义某个类,然后再传入该类的实例。
  • Python 中的函数和方法都可以像一级类那样引用,因此,它们与其他类型的对象一样看,也能够放在表达式里面。
  • 通过名为 _call_ 的特殊方法,可以使类的实例能够像普通的 Python 函数那样得到调用。
  • 如果要用函数来保存状态,那就应该定义新的类,并令其实现 _call_ 方法,而不要定义带状态的闭包。
# 4. 以 @classmethod 形式的多态去通用地构建对象
  • 在 Python 程序中,每个类只能有一个构造器,也就是 _init_ 方法。
  • 通过 @classmethod 机制,可以用一种与构造器相仿的方式来构造类的对象。
  • 通过类方法多态机制,我们能够以更加通用的方式来构建并拼接具体类的子类。
# 5. 用 super 初始化父类
  • Python 采用标准的方法解析来解决超类初始化次序及钻石继承问题。
  • 总是应该使用内置的 super 函数来初始化父类。
# 6. 只在使用 Minx-in 组件制作工具类时进行多重继承
  • 能用 mix-in 组件实现的效果,就不要用多重继承来做。
  • 将各功能实现为可插拔的 mix-in 组件,然后令相关的类继承自己需要的那些组件,即可定制该类实例所应具备的行为。
  • 把简单的行为封装到 mix-in 组件里,然后就可以用多个 mix-in 组合出复杂的行为了。
# 7. 多用 public 属性,少用 private 属性
  • Python 编译器无法严格保证 private 字段的私密性。
  • 不要盲目地将属性设为 private,而是应该从一开始就做好规划,并允许子类更多地访问超类内部的 API。
  • 应该多用 protected 属性,并在文档中把这些字段合理用法告诉子类的开发者,而不要试图用 private 属性来限制子类访问这些字段。
  • 只有当子类不受自己控制时,才可以考虑用 private 属性来避免名称冲突。
  • 双下划线开头,且结尾至多有一个下划线的属性表示 private;单个下划线开头属性表示 protected。
  • Python 编译器在处理子类访问父类私有属性时,会把子类实际属性变成 _MyChildObject__xx_ 来访问,实际上父类私有属性时 _MyParentObject__xx_ ,子类之所以无法访问父类私有属性,其实是访问名不相符而已。
# 8. 继承 collections.abc 以实现自定义的容器类型
  • 如果要定制的子类比较简单,那就可以直接从 Python 的容器类型(如 list 或 dict)中继承。
  • 想正确实现自定义的容器类型,可能需要编写大量的特殊方法。
  • 编写自制的容器类型时,可以从 collections.abc 模块的抽象基类中继承,那些基类能够确保我们的子类具备适当的接口及行为。

# 元类及属性

# 1. 用纯属性取代 get 和 set 方法
  • 编写新类时,应该用简单的 public 属性来定义接口,而不要用手工实现 set 和 get 方法。
  • 如果访问对象的某个属性时,需要表现出特殊的行为,那就用 @property 来定义这种行为。
  • @property 方法应该遵循最小惊讶原则,而不应该产生奇怪的副作用。
  • @property 方法需要执行得迅速一些,缓慢或复杂的工作,应该放在普通的方法里面。
# 2. 用描述符来改写需要复用的 @property 方法
  • 如果想复用 @property 方法及其验证机制,那么可以自己定义描述符类。
  • WeakKeyDictionary 可以保证描述符类不会泄露内存。
  • 通过描述符协议来实现属性的获取和设置操作时,不要纠结于 _getattribute_ 的方法具体运作的细节。
# 3.用_getattr_、_getattribute_和_setattr_实现按需生成的属性
  • 通过_getattr_和_setattr_,我们可以用惰性的方式来加载并保存对象的属性。
  • 要理解_getattr_和_getattribute_的区别:前者只会在待访问的属性缺失是触发,而后者则会在每次访问属性时触发。
  • 如果要在_getattribute_和_setattr_方法中访问实例属性,那么应该直接通过super()(也就是object类的同名方法)来做,以避免无限递归。
# 4.用元类来验证子类
  • 通过元类,我们可以在生成子类对象之前,先验证子类的定义是否合乎规范。
  • python2 和 python3 指定元类的语法略有不同。
  • python 系统把子类的整个 class 语句体处理完毕之后,就回调用其元类的 _new_ 方法。
Last Updated: 5/24/2021, 10:37:17 AM