一.is 和 == 的误用
• is 比较对象 ID(内存地址,通过 id() 函数获取),
• == 比较对象值
注意:小整数和短字符串可能被驻留优化,导致 is 误判。
如下所示的代码中,变量 a 和 b 其实是两个完全不同的对象,但是通过 is 判断时,结果都是 True,显然不是程序开发者预期的结果。
def main():
a = 256
b = 256
print(id(a), id(b))
print(a is b) # True
a = 1_000_000
b = 1_000_000
print(id(a), id(b))
print(a is b) # True
s1 = "hello!"
s2 = "hello!"
print(id(a), id(b))
print(s1 is s2) # True
最佳实践:始终用 == 比较值,is 仅用于 None、True、False 三种类型比较。
二.字符串结尾反斜杠
字符串不能以单个 反斜杠 结尾,否则会导致语法错误。
原因: 因为最后一个反斜杠,转义了结尾的字符串引号。
def main():
# 下面两行都会 SyntaxError
path1 = r"C:\new_folder\"
path2 = r"\"
修正方案当然也很简单,直接再对末尾的反斜杠进行二次转义即可。
def main():
# 下面两行都会 SyntaxError
path1 = r"C:\new_folder\\"
path2 = r"\\"
三.zap 截取到最短匹配
zip() 函数在参数中,最短的迭代器结束时停止,不会为其他剩余元素配对。
# 堆代码 duidaima.com
def main():
names = ["Alice", "Bob", "Carol"]
scores = [90, 85]
print(list(zip(names, scores))) # [('Alice', 90), ('Bob', 85)]
如果需要填充缺失的值,可以使用 itertools.zip_longest(...) 或者指定参数 strict=True 强制进行剩余元素匹配,但是这样当迭代器元素数量不同时,就会出现报错
zip() argument 2 is shorter than argument 1 ...
四.列表的浅拷贝初始化
下面的二维数组初始化之后,结果并未像预期一样,只有第一个元素 grid[0][0] 的值是 1。
def main():
grid = [[0]*3]*3
grid[0][0] = 1
print(grid) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
问题的原因在于:* 操作符对于列表只复制引用,因为上面的代码中是一个二维数组,每一行都指向一个子数组/列表,所以最终每个子数组的第一个元素都是 1 (因为 3 个子数组/列表指向的是同一个引用)。解决方案也很简单,就是为每个子数组/列表创建一个全新的值。
def main():
grid = [[0]*3 for _ in range(3)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
五.可变类型作为函数参数
Python 函数的参数默认值,如果是可变数据类型 (列表/字典/集合),那么默认参数只在函数定义时创建一次,后续的函数调用拿到的默认值都是同一个对象。也就是说,函数每次调用完成后,默认值都会发生变化 (相当于变成全局变量参数的了)。
def my_append(val, vals = []):
vals.append(val)
return vals
def main():
print(my_append(100)) # [100]
# 第二次调用的时候,参数的默认值变成了第一次调用后的结果
print(my_append(100)) # [100, 100]
# 如果希望每次调用都使用 新的空列表 作为默认参数
# 必须手动指定
print(my_append(100, [])) # [100]
针对上面的问题,更加约定俗成的解决方法是采用 None 作为默认值,这样默认参数在每次调用时都会创建一个新的值。
def my_append(val, vals = None):
if vals is None:
vals = []
vals.append(val)
return vals
def main():
print(my_append(100)) # [100]
print(my_append(100)) # [100]
print(my_append(100)) # [100]
print(my_append(100)) # [100]
六.额外的思考
但是反过来说,我们又可以通过 Python 提供的这个默认值可变机制,来实现一些自定义数据结构,并对外开放非常简单友好的 API。下面是一个模拟向数据库/数据表插入数据的示例代码 (仅限示例,请勿在生产环境中使用)。
def insert_record(item, items = []):
items.append(item)
return items
def main():
# 连续插入 3 条数据
insert_record(100)
insert_record(200)
rows = insert_record(300)
print(rows) # [100, 200, 300]
七.闭包和变量的延迟绑定
在 Python 中,闭包捕获的是变量本身而非调用闭包函数当时的值,导致在循环中创建的多个函数,最终都引用同一个循环变量的最后值,这就是所谓的 延迟绑定 问题。
def make_multipliers():
return [lambda x: i * x for i in range(5)]
def main():
funcs = make_multipliers()
print([f(2) for f in funcs]) # [8, 8, 8, 8, 8] ← 全部是 i=4 的结果
解决方案: 利用默认参数在定义时绑定当前值。
def make_multipliers():
return [lambda x, i=i: i * x for i in range(5)]
def main():
funcs = make_multipliers()
print([f(2) for f in funcs]) # [0, 2, 4, 6, 8]
八.生成器的遍历规则
不同于列表/字典/集合等基础数据类型,生成器 (数据类型) 有一个非常重要的特点:一次性消耗。也就是说,生成器的所有元素只会被遍历一次,当所有的元素被遍历完成后,该生成器就会抛出异常 StopIteration。即使捕获并处理了异常,再次遍历时也无法再次访问到任何元素。
def main():
primes = [2, 3, 5, 7, 11]
# 将每个质数乘以 2 作为结果,存入一个生成器 (迭代器)
numbers = (x * 2for x in primes)
print(type(numbers)) # <class 'generator'>
# 查询元素是否存在时,同样会消耗生成器元素
print(4in numbers)
print(next(numbers))
# 当元素不存在时,说明此时已经遍历到了生成器的末尾
# 也就是说,生成器已经被消耗完了
print(100in numbers)
# 再次访问,就会报错
# print(next(numbers)) # StopIteration
最简单直接的方案是将生成器的结果,一次性地放入到一个列表中,但是这样有一个潜在的问题:当生成器的元素很多时,可能出现性能问题甚至内存溢出 (OOM)。为了解决潜在的内存性能问题,可以通过将生成器本身封装到一个专门的工厂函数中,这样每次调用时,都会返回一个新的生成器函数,避免对同一生成器返回调用时出现的问题。
def make_gen(n):
for i inrange(n):
yield i
def main():
# 每次都是新的生成器
for x in make_gen(5):
print(x)
# 每次都是新的生成器
for x in make_gen(5):
print(x)
如果我们每次访问的都是同一个生成器的元素,并且可以在生成器 一次性消耗 之后,可以从零开始接着访问,然后无限循环这个访问过程,类似于数据结构中的环形队列。这时就可以通过创建专属的自定义生成器类,然后重写迭代相关的魔术方法,最终实现 “循环利用” 生成器。
class Regenerator:
def __init__(self, factory, *args, **kwargs):
# 生成器的工厂函数通过初始化方法注入
self.factory = factory
self.args = args
self.kwargs = kwargs
self._gen = factory(*args, **kwargs)
def __iter__(self):
returnself
def __next__(self):
try:
returnnext(self._gen)
except StopIteration:
# 生成器消耗完之后,重新创建生成器,循环利用
self._gen = self.factory(*self.args, **self.kwargs)
returnnext(self._gen)
# 生成器的工厂函数
def make_gen(n):
for i inrange(n):
yield i
def main():
reg = Regenerator(make_gen, 5)
# 输出如下
# 0 1 2 3 4 0 1 2 3 4
for _ inrange(10):
print(next(reg))
九.生成器中忽略资源释放
生成器为惰性求值和节省内存提供了强大支持,最经典的应用场景中的大文件读取、惰性计算求值,但是稍不留神,就可能引发资源泄漏的问题。下面的代码中,即使读取文件的生成器提前退出,已经打开的文件句柄也不会立即自动关闭,造成文件句柄资源闲置浪费,直到最终被 GC 回收。
def read_file(path):
f = open(path)
for line in f:
iflen(line) == 0:
# 此时退出文件句柄未被关闭
return
yield line.rstrip('\n')
def main():
for row in read_file('/var/log/nginx/access.log'):
# 逐行读取和处理
...
解决方案也很简单,直接使用上下文管理器来负责文件句柄的生命周期,修改后的代码如下所示。
def read_file(path):
with open(path, 'r') as f:
for line in f:
if len(line) == 0:
# 此时退出文件句柄可以正常被关闭
return
yield line.rstrip('\n')
十.cache 装饰器解决递归问题
如果使用 @cache 或者 @lru_cache 缓存装饰器,来解决递归过程中的重复子任务计算问题,那么这个设计本身就存在缺陷。递归需要考虑到 Python 语言本身对于递归深度的执行限制。比如当需要计算的数字很大时,递归函数在执行时会形成一个非常深的嵌套调用栈,当深度超过一定限制后,就会抛出 RecursionError 异常。