Python中数字和字符串的最佳实践(下)
上一讲中我们学习了python中枚举、魔数的使用,今天咱们来看看还有那些实用小TIP。
实用小贴士
布尔值其实是“数字”
在 Python 中,布尔值 True 和 False 在大多数情况下可以直接等同于两个整数 1 和 0,就像这样:
>>> True + 1
2
>>> 1 / False
Traceback (most recent call last):
File "", line 1, in
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'
提高长字符串的可读性
单行代码的长度不应太长。例如,PEP8 建议每行字符数不超过 79。在现实世界中,大多数人遵循每行字符数在 79 到 119 之间的最大限制。如果只是代码,这些要求相对容易满足,但如果代码中需要非常长的字符串怎么办呢?
在这种情况下,除了使用反斜杠 \
和加号 +
将长字符串拆分成多行外,还有另一种更简单的方法:将长字符串放在括号中允许任意换行:
s = (
"There is something really bad happened during the process. "
"Please contact your administrator."
)
print(s)
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)""")
大数字也可以更易读
对于特别大的数字,您可以通过在中间添加下划线来提高可读性(PEP515,需要 Python 3.6+)。
例如:
>>> 10_000_000.0 # 每三位数划分。
10000000.0
>>> 0xCAFE_F00D # 十六进制数字也有效,将其分组为四位数时更易读。
3405705229
>>> 0b_0011_1111_0100_1110 # 二进制也是有效的。
16206
>>> int('0b_1111_0000', 2) # 在处理字符串时,下划线也会被正确处理。
240
不要忘记以“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'
然而,如果使用 .rsplit()
,处理逻辑会更加直观:
>>> log_line.rsplit(None, 1)
['"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"', '47632']
使用“无穷大” 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
常见误解
“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
在上面的输出中,可以看到这个简单的累加语句会被编译成几个不同的步骤,包括值的检索和保存。在多线程环境中,任何其他线程都可能中断其中的一个步骤,从而妨碍您获得正确的结果。
因此,请不要依赖直觉来判断一个行为是否“线程安全”。否则,当在高并发条件下程序中出现奇怪的错误时,您将为自己的直觉付出痛苦的代价。
字符串拼接不慢
当我刚开始学习 Python 时,我在一个网站上看到一个说法:“Python 中的字符串是不可变的,所以每次你连接字符串时,都会创建一个新对象,导致新的内存分配,效率非常低下。” 我全然相信了这一点。
因此,我一直尽量避免使用 +=
运算符来拼接字符串,而是使用像 “”.join(str_list)
这样的方法来替代。
然而,偶然间,我对 Python 中的字符串拼接 进行了简单的性能测试,发现它一点也不慢!经过查阅一些信息,我终于发现了真相。
在 Python 2.2 版本之前,Python 中的字符串拼接确实很慢,与我最初看到的情况一致。但由于这个操作非常常见,随后的版本对它进行了特定的性能优化。这极大地提高了执行效率。
如今,使用 +=
运算符来拼接字符串的效率已经非常接近使用 “”.join(str_list)
。因此,需要时可以放心拼接,不用担心任何性能问题。
结语
以上是“Python 工匠”系列的第三篇文章,内容颇为零碎。由于篇幅限制,一些常见操作,如字符串格式化,在本文中没有涉及。如果有机会,我将稍后写一些关于它们的内容。
让我们再次总结关键点:
- 在编写代码时,请考虑读者的体验,避免使用过多的神奇字面量。
- 在操作结构化字符串时,使用面向对象的模块比直接处理更具优势。
- dis 模块非常有用,请经常使用它来验证您的猜想。
- 在多线程环境中进行编码非常复杂;谨慎行事,不要相信您的直觉。
- Python 语言更新非常迅速;不要受到他人经验的影响。