Python进阶自检清单:来自《Effective Python》的建议(一)

本系列主要介绍《Effective Python》所推荐的最佳实践,经验丰富的程序员应该对这些实践都比较熟悉了。文中有些补充信息是我自己的理解,请大家多指教!具体分析还请参考原书内容。


一:Pythonic(13条建议)

1. 确认自己的Python版本;

Python2 和 Python3 有较大差异,如果还有人纠结学2还是3,不要犹豫,选3。

2. 遵循PEP8风格指南;

3. 了解bytes、str和unicode的区别;

Python3 中,bytes 是8位二进制序列,str 是 unicode 字符串序列;Python2 中的 str和 unicode 对应 Python3 中的 bytes 和 str ,很奇怪是不是?有谁没被 Python2 的编码问题坑过的,请举手!

4. 如果表达式很复杂,请使用辅助函数;

表达式的演化:

# 最简单的情况
a = a_dict.get('key', )

# 加入or判断,已经比较费解了
a = int(a_dict.get('key', [])[0] or 0)

# 用两行代码和if/else表达式,相对清晰,但如果频繁使用,应该创建辅助函数
a = a_dict.get('key', [])
a = int(a[0]) if a[0] else 0

5. 了解序列切片方法;

  • start 为 0 或 end 为序列长度时,应该省略;
  • 切片没有越界问题,可以利用这个特性;
  • 对 list 赋值时,如果使用切片操作,则不会考虑长度是否一致而会整个替换。

6. 不要在单次切片操作时,同时使用 start、end、stride 参数;

a[-2:2:-2]  # 费解的代码

7. 用列表推导式来取代mapfilter

8. 列表推导式不要嵌套两个以上;

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

# 嵌套两个已经是可读性的极限了
b = [i for sublist in a if len(sublist) > 2 for i in sublist if i % 2 == 0]

9. 如果数据量比较大,用生成器表达式,不要用列表推导式;

10. 用enumerate取代range(len(list))

11. 用zip函数同时遍历多个迭代器;

zip 会以最短的迭代器为基准,如果要以最长的为基准,可以用 itertools 的 zip_longest 函数。

12. 不要使用for和while循环后的else代码块;

这个else块只有在循环正常执行,没有触发 break 语句时才会执行,和if/else概念相反,容易引起误解。

当然,很多人并不知道 for/while 后面居然是可以有 else 语句的,也正说明这个语句没什么用,反而容易导致错误。

13. 合理利用try/except/else/finally结构中的每个代码块;

try:
    # 可能会抛出异常的代码
except:
    # 异常处理
else:
    # 如果没有异常则执行的代码,应该尽量减少try语句下的代码,后续逻辑放else这边来
finally:
    # 无论如何都会执行的代码

二:函数(8条建议)

1. 尽量用异常表示特殊情况,不要返回None;

抛出异常的意思是,调用者必须处理这种异常情况,返回None则有时会被误用,因为None、0之类的会被条件表达式判断为False。

2. 了解如何在闭包中使用外围作用域中的变量;

Python3中使用nonlocal关键字,Python2中可以使用可变对象。

如果闭包函数比较复杂,建议改写为辅助类,把需要处理的变量作为类的属性,因为nonlocal语句离赋值语句远的话,代码不好理解,出错不好追踪。

3. 考虑用生成器改写直接返回列表的函数;

4. 在参数上迭代时要小心;

这条建议的意思是,容器比如列表,可以进行多次迭代,不影响列表本身,但迭代器是有状态的,数据会不断地弹出,最后抛出 StopIteration 异常。如果传入的参数是一个迭代器而不是容器,则可能因为在函数中进行迭代而影响迭代器的状态,导致一些预期外的行为。

判断一个对象是迭代器还是容器,可以采用内置函数 iter():

if iter(arg) is iter(arg): pass

因为针对容器,iter 函数每次会返回新的迭代器对象,而针对迭代器,则直接返回传入的迭代器本身。

当然,这条建议可以进一步扩展,哪怕调用者传入的参数是一个容器,也需要小心处理,因为列表、字典等容器是可变对象,如果我们在函数中不小心修改了传入的对象,调用者可能并不清楚,从而导致预期外的行为,而且这个问题排查起来很麻烦。

def test(a_list):
    return a_list.pop()

a = [1, 2, 3]
b = test(a)  # 我们的目的是获取a的最后一个值,但却影响到了a的值

5. 使用数量可变的位置参数,即 *args;

主要目的是避免太多参数让我们看上去不舒服,但是实际中使用场景不多。因为如果修改函数,比如说,在前面增加一个指定参数,调用者可能不知道,而函数可能还能正常运行,造成一些不好排查的 Bug。

def test(*args): pass

test(1, 2, 3, 4)  # 这是正常的

def test2(condition, *args):
    if condition:
        pass
    else:
        pass

test2(1, 2, 3, 4)  # 不会报错,但其实1被当做condition,可能导致预期外行为

6. 使用关键字参数表示可选行为;

比用 *args 好,意义明确,可以指定默认值,扩展时不容易出问题。

7. 如果参数默认值是可变对象,应使用None替代;

这个建议涉及对模块加载过程的理解,函数的定义是在模块加载时完成的,也就是说,参数的默认值在这时就指向了一个对象,之后并不会修改。

我们出错有两种可能,一种是以为它会变动,实际上它没有变动;另一种是以为它不会变动,实际上会发生变动。

举例来说:

# 第一种情况
 def when(time=datetime.now()):
 """这个函数打印的时间永远是函数定义,即模块加载的时间,并不会在调用时变化"""
     print(time)

# 第二种情况
 def return_sum_with_five(nums=[1, 2, 3, 4]):
     nums.append(5)
     return sum(nums)

 # 第一次调用是正常的 15,但默认值其实已经被修改了
 return_sum_with_five()
 return_sum_with_five()  # 奇怪的结果 20

解决的方案是参数默认值使用None,函数内部逻辑判断是否有传参,没有则赋予默认值。

说来惭愧,这个错我也犯过,新手不知道这个问题是可以理解的,但如果使用了编码规范检查插件,应该会看到警告信息。

这也告诉我们,一定要重视警告信息,哪怕忽略,也应该明白它具体是在说什么,设置这条规则的原因是什么。

8. 使用必须以关键字形式指定的参数;

Python3 中可以这样定义一个函数:

def test(arg1, arg2, *, arg3): pass

这时,调用者必须通过关键字参数形式传入 arg3 的值。 Python2 可以通过 **kwargs 实现,函数从 kwargs 读取值。


发表评论

评论列表,共 0 条评论