编程某种意义上是一门『手艺』,因为优雅而高效的代码,就如同完美的手工艺品一样让人赏心悦目。
致“匠人”
数字是几乎所有编程语言里最基本的数据类型,它是我们通过代码连接现实世界的基础。
上周我们了讨论一些细微的编程实践,这周我们来讨论一下使用数字与字符串的的实用技巧和常见误区来帮助你写出更好的 Python 代码。
实用技巧
1布尔值其实也是“数字”
Python 里的两个布尔值 True 和 False 在绝大多数情况下都可以直接等价于 1 和 0 两个整数来使用,就像这样:
>>> True + 1
2
>>> 1 / False
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
那么记住这点有什么用呢?首先,它们可以配合 sum 函数在需要计算总数时简化操作:
>>> l = [1, 2, 4, 5, 7]
>>> sum(i % 2 == 0 for i in l)
2
此外,如果将某个布尔值表达式作为列表的下标使用,可以实现类似三元表达式的目的:
# 类似的三元表达式:"Javascript" if 2 > 1 else "Python"
>>> ["Python", "Javascript"][2 > 1]
'Javascript'
2改善超长字符串的可读性
单行代码的长度不宜太长。比如 PEP8 里就建议每行字符数不得超过 79。现实世界里,大部分人遵循的单行最大字符数在 79 到 119 之间。如果只是代码,这样的要求是比较容易达到的,但假设代码里需要出现一段超长的字符串呢?
这时,除了使用斜杠 和加号 + 将长字符串拆分为好几段以外,还有一种更简单的办法:使用括号将长字符串包起来,然后就可以随意折行了:
def main():
logger.info(("There is something really bad happened during the process. "
"Please contact your administrator."))
当多级缩进里出现多行字符串时
日常编码时,还有一种比较麻烦的情况。就是需要在已经有缩进层级的代码里,插入多行字符串字面量。因为多行字符串不能包含当前的缩进空格,所以,我们需要把代码写成这样:
def main():
if user.is_active:
message = """Welcome, today's movie list:
- Jaw (1975)
- The Shining (1980)
- Saw (2004)"""
但是这样写会破坏整段代码的缩进视觉效果,显得非常突兀。要改善它有很多种办法,比如我们可以把这段多行字符串作为变量提取到模块的最外层。不过,如果在你的代码逻辑里更适合用字面量的话,你也可以用标准库 textwrap 来解决这个问题:
from textwrap import dedent
def main():
if user.is_active:
# dedent 将会缩进掉整段文字最左边的空字符串
message = dedent("""
Welcome, today's movie list:
- Jaw (1975)
- The Shining (1980)
- Saw (2004)""")
3“r” 开头的内建字符串函数
Python 的字符串有着非常多实用的内建方法,最常用的有 .strip()、.split() 等。这些内建方法里的大多数,处理起来的顺序都是从左往右。但是其中也包含了部分以 r 打头的从右至左处理的镜像方法。在处理特定逻辑时,使用它们可以让你事半功倍。
假设我们需要解析一些访问日志,日志格式为:"{user_agent}" {content_length}:
>>> log_line = '"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" 47632'
如果使用 .split() 将日志拆分为 (user_agent, content_length),我们需要这么写:
>>> l = log_line.split()
>>> " ".join(l[:-1]), l[-1]
('"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"', '47632')
但是如果使用 .rsplit() 的话,处理逻辑就更直接了:
>>> log_line.rsplit(None, 1)
['"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"', '47632'
4使用“无穷大” float("inf")
如果有人问你:“Python 里什么数字最大/最小?”。你应该怎么回答?有这样的东西存在吗?
答案是:“有的,它们就是:float("inf") 和 float("-inf")”。它们俩分别对应着数学世界里的真负无穷大。当它们和任意数值进行比较时,满足这样的规律:float("-inf") < 任意数值 < float("inf")。
因为它们有着这样的特点,我们可以在某些场景用上它们:
# A. 根据年龄升序排序,没有提供年龄放在最后边
>>> users = {"tom": 19, "jenny": 13, "jack": None, "andrew": 43}
>>> sorted(users.keys(), key=lambda user: users.get(user) or float('inf'))
['jenny', 'tom', 'andrew', 'jack']
# B. 作为循环初始值,简化第一次判断逻辑
>>> max_num = float('-inf')
>>> # 找到列表中最大的数字
>>> for i in [23, 71, 3, 21, 8]:
...: if i > max_num:
...: max_num = i
...:
>>> max_num
71
常见误区
1“value += 1” 并非线程安全
当我们编写多线程程序时,经常需要处理复杂的共享变量和竞态等问题。
“线程安全”,通常被用来形容 某个行为或者某类数据结构,可以在多线程环境下被共享使用并产生预期内的结果。一个典型的满足“线程安全”的模块就是 queue 队列模块。
而我们常做的 value += 1 操作,很容易被想当然的认为是“线程安全”的。因为它看上去就是一个原子操作 (指一个最小的操作单位,执行途中不会插入任何其他操作)。然而真相并非如此,虽然从 Python 代码上来看,value += 1 这个操作像是原子的。但它最终被 Python 解释器执行的时候,早就不再 “原子” 了。
我们可以用前面提到的 dis 模块来验证一下:
def incr(value):
value += 1
# 使用 dis 模块查看字节码
import dis
dis.dis(incr)
0 LOAD_FAST 0 (value)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_FAST 0 (value)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
在上面输出结果中,可以看到这个简单的累加语句,会被编译成包括取值和保存在内的好几个不同步骤,而在多线程环境下,任意一个其他线程都有可能在其中某个步骤切入进来,阻碍你获得正确的结果。
因此,请不要凭借自己的直觉来判断某个行为是否“线程安全”,不然等程序在高并发环境下出现奇怪的 bug 时,你将为自己的直觉付出惨痛的代价。
2字符串拼接并不慢
我刚接触 Python 不久时,在某个网站看到这样一个说法: “Python 里的字符串是不可变的,所以每一次对字符串进行拼接都会生成一个新对象,导致新的内存分配,效率非常低”。 我对此深信不疑。
所以,一直以来,我尽量都在避免使用 += 的方式去拼接字符串,而是用 "".join(str_list) 之类的方式来替代。
但是,在某个偶然的机会下,我对 Python 的字符串拼接做了一次简单的性能测试后发现: Python 的字符串拼接根本就不慢! 在查阅了一些资料后,最终发现了真相。
Python 的字符串拼接在 2.2 以及之前的版本确实很慢,和我最早看到的说法行为一致。但是因为这个操作太常用了,所以之后的版本里专门针对它做了性能优化。大大提升了执行效率。
如今使用 += 的方式来拼接字符串,效率已经非常接近 "".join(str_list) 了。所以,该拼接时就拼接吧,不必担心任何性能问题。
结束语
让我们最后再总结一下要点:
· 编写代码时,请考虑阅读者的感受,不要出现太多神奇的字面量。
· 当操作结构化字符串时,使用对象化模块比直接处理更有优势。
· dis 模块非常有用,请多多使用它验证你的猜测。
· 多线程环境下的编码非常复杂,要足够谨慎,不要相信自己的直觉。
· Python 语言的更新非常快,不要被别人的经验所左右。
原文作者:腾讯高级工程师 朱雷