
写Python代码的人经常遇到一个情况。同一个功能有好几种写法。选哪种写法,短时间看不出差别。项目变大以后,差别就出来了。
看一个例子。统计一个列表里每个元素出现的次数。
Python
data = ['a', 'b', 'a', 'c', 'b', 'a']
count = {}
for item in data:
if item not in count:
count[item] = 0
count[item] += 1
这段代码能跑。它有一个小问题。每次循环要判断键是否存在。数据量小的时候无所谓。数据量到百万级,判断次数就是百万次。写法可以改。
Python
count = {}
for item in data:
count[item] = count.get(item, 0) + 1
用get方法,省掉了if判断。一次循环只调用一次方法。速度有提升。还有更直接的办法。
Python
from collections import Counter
count = Counter(data)
Counter是标准库里的东西。它底层用C实现。处理百万数据比手写循环快很多。很多人不知道这个类。知道了以后,这类统计任务不用再写循环。
再看字符串拼接。新手常写这样的代码。
Python
result = ''
for s in string_list:
result += s
字符串在Python里不可变。每次加号都会创建新字符串。旧字符串被丢弃。内存里反复分配释放。列表有一万个元素,就创建一万个中间字符串。join方法不一样。
Python
result = ''.join(string_list)
join先计算总长度,一次性分配内存。然后逐个复制。中间不产生临时对象。数据量越大,差距越明显。有人测试过,十万个字符串拼接,join比加号快几十倍。这个技巧十年前就有人讲。今天还有人在写加号循环。原因是没有意识到不可变对象的代价。
变量交换是另一个常见场景。
Python
a = 1
b = 2
temp = a
a = b
b = temp
Python有更短的写法。
Python
a, b = b, a
右边先构造一个元组。左边按顺序解包。整个过程在字节码层面完成。比手动引入临时变量少几步操作。这个写法不光短。它还避免了临时变量名冲突。代码里少一个变量,就少一个出错的地方。
列表推导式用的人多。生成器表达式用的人少。两者长得像。行为差别大。
Python
squares = [x*x for x in range(1000000)]
这行代码立刻创建一个百万元素的列表。内存里实实在在放一百万个整数。换成圆括号。
Python
squares = (x*x for x in range(1000000))
这时候squares是一个生成器。它不预先计算任何值。每次next调用才算一个。内存里只存当前值。处理大数据流的时候,生成器能省下大量内存。有人写循环处理文件行,用列表推导先把所有行读进内存。文件一G以上,机器就卡。改成生成器,内存占用几乎不变。
函数默认参数有一个坑。很多人踩过。
Python
def add_item(item, target=[]):
target.append(item)
return target
调用add_item(1)返回[1]。再调用add_item(2)返回[1, 2]。第二次调用前没有传target。按理说应该用空列表。实际用了第一次调用修改过的列表。原因是默认参数在函数定义时求值。空列表只创建一次。所有调用共享同一个对象。正确写法用None。
Python
def add_item(item, target=None):
if target is None:
target = []
target.append(item)
return target
这样每次调用都新建列表。问题消失。这个坑在面试题里出现频率很高。实际项目里也常见。尤其是写工具函数的时候。参数默认值用可变对象,早晚出事。
异常处理也有技巧。看这段代码。
Python
try:
value = data[key]
except KeyError:
value = default
功能没问题。异常机制有开销。Python里抛异常和捕获异常比普通判断慢。如果key不存在是常见情况,用get更好。
Python
value = data.get(key, default)
get一次调用解决。没有异常开销。如果key不存在是罕见情况,用try更清晰。选择哪种写法,看哪种情况更常发生。这个判断标准很多人不清楚。他们要么全用try,要么全用get。代码性能就差在这些细节上。
文件读取也有讲究。
Python
f = open('data.txt')
content = f.read()
f.close()
手动close有风险。中间代码抛异常,close不会执行。文件句柄泄漏。系统资源有限。泄漏多了程序崩溃。with语句解决这个问题。
Python
with open('data.txt') as f:
content = f.read()
不管有没有异常,文件都会关闭。with后面可以跟多个对象。写日志和读数据同时进行的时候很方便。
Python
with open('input.txt') as fin, open('output.txt', 'w') as fout:
for line in fin:
fout.write(line.upper())
两个文件都自动管理。代码缩进清晰。不用记着关哪个。不用处理关闭时的异常。这种写法省心。
字典合并以前要写循环。Python 3.5以后可以用解包。
Python
d1 = {'a': 1}
d2 = {'b': 2}
merged = {**d1, **d2}
一行完成。键冲突时后面的覆盖前面的。Python 3.9又加了|运算符。
Python
merged = d1 | d2
效果一样。写法更短。有人担心性能。实测两者差距很小。选哪个看个人习惯。知道有这些写法,读别人代码时不至于看不懂。
迭代字典的时候不要改字典。
Python
d = {'a': 1, 'b': 2}
for key in d:
if d[key] == 1:
del d[key]
这段代码会抛RuntimeError。字典大小在迭代过程中变了。Python检测到就报错。正确做法是先收集要删的键。
Python
keys_to_delete = [k for k, v in d.items() if v == 1]
for key in keys_to_delete:
del d[key]
或者直接构造新字典。
Python
d = {k: v for k, v in d.items() if v != 1}
第二种写法更干净。它不改原字典。它创建一个新字典赋给同名变量。旧字典被垃圾回收。代码读起来也顺。过滤逻辑一眼看清。
这些技巧有一个共同点。它们都不复杂。单个看都很小。组合起来用,代码质量不一样。写代码的人每天做很多选择。选对了,程序跑得快,维护省力。选错了,问题慢慢积累。等到出问题再改,成本高得多。
以上就是“写了10年Python才发现这8个写法差距这么大你天天写的代码可能慢了几十倍”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14553/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料