Python -- Effective Python:编写高质量Python代码的59个有效方法

时间:2024-09-22 19:07:32

第 1 章 用 Pythonic 方式来思考

第 1 条:确认自己所用的 Python 版本

  1. python --version

  2. import sys print(sys.version_info) print(sys.version)

第 2 条:遵循 PEP8 风格标准指南

《 Python Enhancement Proposal #8》(8 号 Python 增强提案)又叫 PEP 8

第 3 条:了解 bytes、str 与 unicode 的区别

  1. Unicode 字符转换成二进制数据 : encode 方法
  2. 二进制数据转换成Unicode 字符 :decode 方法

第 4 条:用辅助函数代替复杂的函数表达式

如果出现复杂的表达式,可以抽调成一个函数方法,如果需要反复使用相同逻辑,更应该把复杂的表达式放进辅助函数中。

第 5 条:了解切割序列的方法

1.不要写多余的代码:当 start 索引为 0,或 end 索引为序列长度时,应该将其省略。

  1. 切片操作不会计较 start 与 end 索引是否越界(如 a[:20] 或 a[-20:])。

3.对 list 赋值的时候,如果使用切片操作,就会把原列表中处在相关范围内的值替

换成新值,即便它们的长度不同也依然可以替换

第 6 条:在单次切片操作内,不要同时指定 start 、end 和 stride

somelist[开始索引:结束索引:步进值]

不要同时使用 start 、 end 和 stride ,理解很困难;可以拆作范围切割和步进切割两条赋值表达,或考虑使用内置 itertools 模块中的 islice

尽量不使用负值的 stride 。

第 7 条:用列表推导来取代 map 和 filter

a = [1, 2 ,3 ,4 ,5 ,6 , 7, 8, 9, 10]

  1. squares = [x x**2 for x in a if x % 2 == 0]

  2. squares = map(lambda x: x**2, filter(lambda x: x % 2 == 0), a)

结果均为 [4, 16, 36, 64, 100] 。第1个中的采用列表推导来做,那么只需在循环后面添加条件表达式即可;第2个把内置的 f ilter 函数与 map 结合起来,也能达成同样的效果,但是代码会写得非常

难懂。

字典与集也支持推导表达式。

第 8 条:不要使用两个以上的表达式的列表推导

列表推导支持多级循环,每一级循环也支持多项条件。超过两个表达式的列表推导是很难理解的,应该尽量避免。

可以使用两个条件、两个循环或

一个条件搭配一个循环。如果要写的代码比这还复杂,那就应该使用普通的 if 和 for 语句,并编写辅助函数。

第 9 条:用生成器表达式来改写数据量较大的列表推导

当输入的数据量较大时,列表推导可能会因为占用太多内存而出问题。为了解决这个问题,Python 提供了生成器表达式(generator expression),它是对列表

推导和生成器的一种泛化(generalization)。

把实现列表推导所用的那种写法放在一对圆括号中,就构成了生成器表达式。此时立刻返回一个迭代器,逐次调用内置的 next 函数,以这个迭代器为参数,输出一个值,做循环输出即可。

第 10 条:尽量用 enumerate 取代 range

name_list = ['aa', 'bbb', 'cccc', 'ddddd', 'eeeeee']

for i, name in enumerate(name_list):
print('%d : %s' % (i + 1, name))

结果:

1 : aa
2 : bbb
3 : cccc
4 : ddddd
5 : eeeeee

还可以直接指定 enumerate 函数开始计数时所用的值(默认为0,本例从 1 开始计数),结果不变,这样能

把代码写得更短。

name_list = ['aa', 'bbb', 'cccc', 'ddddd', 'eeeeee']

for i, name in enumerate(name_list, 1):
print('%d : %s' % (i , name))

3.11

第 11 条:用 zip 函数同时遍历两个迭代器

  1. 内置的 zip 函数里可以平行的同时遍历两个迭代器,如果长度不一样则会以较短的迭代器为准而结束循环。

  2. python 3 中的 zip 函数相当于生成器,可以逐次产生元组。

  3. python 2 则时一次性生成返回整份列表;如果用 zip 函数遍历的数据比较多,则会导致程序崩溃,需要用 itertools 内置模块的 izip 函数

第 12 条:不要在 for 和 while 循环后面写 else 模块

# 判断两个数是否互质
a = 4
b = 9
for i in range(2, min(a, b) + 1):
print('Testing', i)
if a % i == 0 and b % i ==0:
print('Not coprime')
break
else:
print('Coprime')

结果:

Testing 2
Testing 3
Testing 4
Coprime

只有当 for 或 while 整个循环主体没有遇到 break 语句时,循环后面的 else 模块才会运行。不要在循环后面使用 else 块,难以理解,用函数返回的形式代替。

第 13 条:合理利用 try/except/else/finally 结构中的每个代码块

else 块:try 块没有发生异常时才会运行。

例子:要从文件中读取某项事务的描述信息,处理该事务,然后就地更新该文件。

解题思路:为了实现此功能,我们可以用 try 块来读取文件并处理其内容,用 except 块来应对 try 块中可能发生的相关异常,用 else 块实时地更新文件并把更新中可能出现的异
常回报给上级代码,然后用 finally 块来清理文件句柄。
UNDEFINED = object()

def divide_json(path):
handle = open(path, 'r+') # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
op = json.loads(data) # May raise ValueError
value = (
op['numerator'] /
op['denominator']) # May raise ZeroDivisionError
except ZeroDivisionError as e:
return UNDEFINED
else:
op['result'] = value
result = json.dumps(op)
handle.seek(0)
handle.write(result) # May raise IOError
return value
finally:
handle.close() # Always runs

第 2 章 函数

第 14 条:尽量用异常来表示特殊情况,而不要返回 None

# 处理分母为 0 的方法(异常)

def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e x, y = 5, 2
try:
result = divide(x, y)
except ValueError:
print('Invalid inputs')
else:
print('Result is %.1f' % result) # 结果:Result is 2.5

第 15 条:了解如何在闭包里使用外围作用域中的变量

python支持闭包。

获取闭包内的数据:

1. nonlocal :nonlocal 语句清楚地表明(python 2 不支持):如果在闭包内给该变量赋值,那么修改的其实是闭包外那个作用域中的变量。这与 global 语句互为补充,global 用来表示对该变量的赋值操作,将会直接修改模块作用域里的那个变量。尽量不要用。

2. 使用可变量(例如,包含单个元素的列表,字典,元组)

如果使用 nonlocal 的那些代码,已经写得越来越复杂,那就应该将相关的状态封装成辅助类(helper class)。

第 16 条:考虑用生成器来改写直接返回列表的函数

如果返回的列表量比较大,那么程序可能耗尽内存并崩溃,用生成器改写则不会出现这种情况。生成器是使用 yield 表达式的函数。调用生成器函数时候,它并不会真的运行,而是会返回迭代器。每次在这个迭代器上面调用内置的 next 函数是,迭代器会把生成器推到下一个 yield 表达式那里。生成器传给 yield 的每一个值,都会有迭代器返回给调用者。

# 计空格位置索引
def index_words_iter(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index + 1

第 17 条:在参数上面迭代时,要多加小心

迭代器只能欸遍历一次,如果多次遍历则会出现意想不到的错误。

(列表可以被多次迭代)

  1. 为解决迭代器不可多次遍历,可以使用迭代器制作一份列表,缺点在于列表的数据量大的话,会造成程序崩溃;

  2. ① 可以使用 lambda 表达式代替,该表达式在调用生成器的时候,可以每次产生新的迭代器;(略显生硬)

    ② 新编一种实现迭代器协议的容器类。(建议使用)

# 可以迭代的容器类
class ReadVisits(object):
def __init__(self, data_path):
self.data_path = data_path def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line) # 可以判断是否传入的是容器类的求取百分比的函数
def normalize_defensive(numbers):
if iter(numbers) is iter(numbers): # An iterator -- bad !
raise TypeError('Must supply a container !')
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result # visits = TreadVisits(path)
visits = [15, 35, 80]
per = normalize_defensive(visits) # No error
print(per)
# it = iter(visits)
# normalize_defensive(visits) # Error

第 18 条:用数量可变的位置参数减少视觉杂讯

*args :(习惯写成这样 starargs)

  1. 做函数参数时可以接收国定形参以外的任意数量的位置参数;

  2. 调用函数时,使用 * 操作符,把序列中的元素当成位置参数传给函数;

  3. 在已经接受 *args 参数的函数上面继续添加位置参数,可能会产生难以排查的

    bug。

第 19 条:用关键字参数来表达可选的行为

  1. 函数参数可以按位置或关键字来指定。

  2. 只使用位置参数来调用函数,可能会导致这些参数值的含义不够明确,而关键

    字参数则能够阐明每个参数的意图。

  3. 给函数添加新的行为时,可以使用带默认值的关键字参数,以便与原有的函数

    调用代码保持兼容。

  4. 可选的关键字参数,总是应该以关键字形式来指定,而不应该以位置参数的形

    式来指定。

第 20 条:用 None 和文档字符串来描述具有动态默认值的参数

如果参数的实际默认值是可变类型(mutable),那就一定要记得用 None 作为形式

上的默认值。(即形参值设为 None)

def log(message, when=None):
"""Log a message with a timestamp. Args:
message: Message to print.
when: datetime of when the message occurred.
Defaults to the present time.
"""
when = datetime.now() if when is None else when
print('%s: %s' % (when, message)) # 两个时间戳就会不一样,如果形参 when=datetime.now() ,则输出相同时间戳
log('Hi there!')
sleep(0.1)
log('Hi again!')
  1. 参数的默认值,只会在程序加载模块并读到本函数的定义时评估一次。对于 {}

    或 [] 等动态的值,这可能会导致奇怪的行为。

  2. 对于以动态值作为实际默认值的关键字参数来说,应该把形式上的默认值写为

    None,并在函数的文档字符串里面描述该默认值所对应的实际行为。

第 21 条:用只能以关键词形式指定的参数来确保代码的明晰

  1. 关键字参数能够使函数调用的意图更加明确。

  2. **kwargs 参数:可以接受任意数量的关键字参数。

第 22 条:尽量用辅助类来维护程序的状态,而不要用字典和元组

  1. 我们很容易就能用 Python 内置的字典与元组类型构建出分层的数据结构,从而保存程序的内部状态。

    但是,当前套多于一层的时候,就应该避免使用这种做法了(例如,不要使用包含字典的字典,不要使用过长的元组)

  2. 如果容器中包含简单而又不可变的数据,可以使用 namedtuple 来表示

  3. 保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆解为多个辅助类。(建议使用)

第 23 条:简单的接口应该接收函数,而不是类的实例

简单的接口应该接收函数,类的实例化可以在函数中完成。

通过 call 特殊方法,可以使类的实例能够像普通的 Python 函数那样得到调用

第 24 条:以 @classmethod 形式的多态去通用地构建对象

  1. python 中每个类只有一个构造器: init

  2. 通过 @classmethod 机制,用一种与构造器相仿的方式构造类的对象

  3. 通过类方法多态机制,可以以更加通用的方式创建并拼接具体的子类。

第 25 条:用 super 初始化父类

  1. 直接在子类中调用超类的 init 方法,可能会产生无法预知的行为,问题之一就是一个类继承多个类,全部调用超类的 init 方法,实际调用顺序并不固定。

  2. 钻石行继承体系:如果子类继承自两个单独的超类,而那两个超类有继承自同一个公共基类,那么就构成了钻石行继承体系。

3.总是应该使用内置的 super 函数来初始化父类。