数字是几乎所有编程语言中最基本的数据类型,它们构成了通过代码连接现实世界的基础。在Python中,有三种数值类型:int(整数)、float(浮点数)和complex(复数)。在大多数情况下,我们只需要处理前两种。
Python中的整数相对无忧,因为它们不区分有符号和无符号值,并且永远不会溢出。然而,浮点数仍然存在精度问题,就像许多其他编程语言一样,这经常让刚刚进入编程世界的新手感到困惑:“为什么浮点数不准确?”。
与数字相比,Python中的字符串要复杂得多。要掌握它们,首先需要了解bytes和str之间的区别。如果你碰巧是Python 2的用户,那么仅Unicode和字符编码问题就足够让你头疼了(立即迁移到Python 3吧!)。
然而,以上提到的这些都不是本文的主题。如果你感兴趣,你可以在网上找到大量相关信息。在这篇文章中,我们将讨论一些更为微妙和不太常见的编程实践,这些实践可以帮助你编写更好的Python代码。
“整数字面量”指的是直接出现在代码中的数字。它们散布在代码中,比如代码del users[0]
中的0
,它就是一个整数字面量。它们简单实用,每个人每天都会写。然而,当你的代码中某些特定的字面量不断重复出现时,你的“代码质量警告灯”应该亮起黄灯。
例如,假设你刚刚加入了一个期待已久的新公司,项目中有一个你的同事交接给你的函数:
def mark_trip_as_featured(trip):
"""将旅程添加到推荐部分。
"""
if trip.source == 11:
do_some_thing(trip)
elif trip.source == 12:
do_some_other_thing(trip)
... ...
return
这个函数是做什么的呢?你努力理解它的含义,但trip.source == 11
的情况是什么?== 12
呢?这两行代码非常简单,不使用任何魔法功能。但如果你是一个刚刚接触这段代码的新手,可能需要整整一个下午才能理解它们的含义。
**问题出在这些数字字面量上。**最初编写这个函数的人可能是一位有经验的程序员,他在公司成立时加入了。他对这些数字的含义非常清楚。但如果你是一个刚刚接触这段代码的新手,情况就完全不同了。
使用enum
枚举类型改进代码
那么我们如何改进这段代码呢?最直接的方法是为这两个条件分支添加注释。然而,在这里“添加注释”并不是提高代码可读性的最佳方式(实际上在大多数其他情况下也是如此)。我们需要有意义的名称,而不是这些字面量,而enum
类型正好可以胜任这个任务。
enum
是Python 3.4版本引入的内置模块。如果你使用的是较早版本,可以通过pip install enum34
安装。下面是使用enum
的示例代码:
# -*- coding: utf-8 -*-
from enum import IntEnum
class TripSource(IntEnum):
FROM_WEBSITE = 11
FROM_IOS_CLIENT = 12
def mark_trip_as_featured(trip):
if trip.source == TripSource.FROM_WEBSITE:
do_some_thing(trip)
elif trip.source == TripSource.FROM_IOS_CLIENT:
do_some_other_thing(trip)
... ...
return
将重复出现的数字字面量定义为枚举类型,不仅可以提高代码的可读性,还可以减少代码中出现错误的可能性。
想象一下,如果在判断某个分支时,你错误地输入了111
而不是11
会发生什么?我们经常犯这种错误,而且这种错误在早期阶段很难检测到。将所有这些数字字面量放入枚举类型中可以有效地避免这种问题。同样,将字符串字面量重写为枚举也可以实现相同的好处。
使用枚举类型而不是字面量的优势:
当然,并不需要将代码中的所有字面量都改成枚举类型。只要一个字面量在其上下文中容易理解,就可以使用它。例如,像0
和-1
这样经常作为数字下标出现的使用是没有问题的,因为每个人都知道它们的含义。
什么是“裸字符串操作”?在本文中,它指的是仅使用基本的加法、减法、乘法、除法和循环,结合内置函数/方法来操作字符串并获得所需结果。
每个人都写过这样的代码。有时我们需要连接一
大段提醒信息发送给用户,有时我们需要构建一个长的SQL查询语句发送到数据库,就像下面的例子:
def fetch_users(conn, min_level=None, gender=None, has_membership=False, sort_field="created"):
"""获取用户列表。
:param int min_level: 所需的最低用户级别,默认为所有级别。
:param int gender: 过滤用户性别,默认为所有性别。
:param int has_membership: 选择所有成员/非成员用户,默认为非成员。
:param str sort_field: 排序字段,默认为创建日期“用户创建日期”。
:returns: list:[(用户ID, 用户名), ...]
"""
# 一种古老的SQL连接技术,使用“WHERE 1=1”来简化字符串连接操作。
# 区分查询参数以避免SQL注入问题。
statement = "SELECT id, name FROM users WHERE 1=1"
params = []
if min_level is not None:
statement += " AND level >= ?"
params.append(min_level)
if gender is not None:
statement += " AND gender >= ?"
params.append(gender)
if has_membership:
statement += " AND has_membership == true"
else:
statement += " AND has_membership == false"
statement += " ORDER BY ?"
params.append(sort_field)
return list(conn.execute(statement, params))
我们使用这种方法连接所需的字符串——在这种情况下,是一个SQL语句——是因为它简单、直接、直观。然而,这样做的最大问题是,随着函数逻辑变得更加复杂,这个连接代码容易出现错误,难以扩展。上面的演示代码看起来似乎没有bug(谁知道是否有潜在问题)。
实际上,对于结构化和基于规则的字符串(如SQL语句)来说,最好使用面向对象的方法构建和编辑它们。下面的代码使用SQLAlchemy模块完成了相同的功能:
def fetch_users_v2(conn, min_level=None, gender=None, has_membership=False, sort_field="created"):
"""获取用户列表。
"""
query = select([users.c.id, users.c.name])
if min_level is not None:
query = query.where(users.c.level >= min_level)
if gender is not None:
query = query.where(users.c.gender == gender)
query = query.where(users.c.has_membership == has_membership).order_by(users.c[sort_field])
return list(conn.execute(query))
上述fetch_users_v2
函数更短,更易于维护,并且根本不需要担心SQL注入问题。因此,在你的代码中存在复杂的原始字符串处理逻辑时,请尽量用以下方法替换它:
问题:目标/源字符串是否结构化并遵循特定格式?
是:查找现有的开源面向对象模块来操作它们,或者自己写一个。
lxml
否:尝试使用模板引擎而不是复杂的字符串处理逻辑来实现目标。
偶尔,在我们的代码中会有一些复杂的数字,就像下面的例子:
def f1(delta_seconds):
# 如果超过11天,什么都不做。
if delta_seconds > 950400:
return
...
首先,上面的代码没有问题。
首先,我们在笔记本中计算了一下(当然,像我这样聪明的人会使用IPython):11天总共有多少秒
?然后我们把神奇的数字950400
填入我们的代码中,并最终在上面添加了一条注释,告诉大家这个神奇的数字是怎么来的。
我的问题是:“为什么我们不只把代码写成_if delta_seconds < 11 * 24 * 3600:_
呢?”
“答案肯定是‘性能’。”我们都知道Python是一种解释性语言,速度 (差)。因此,预计算950400
是因为我们不想在每次调用函数f1
时都产生计算开销。然而,即使我们将代码修改为‘if delta_seconds < 11 * 24 * 3600:
’,该函数也不会产生任何额外的开销。
Python代码在执行时由解释器编译成字节码,真相就在那个字节码中。让我们使用dis
模块来查看一下:
def f1(delta_seconds):
if delta_seconds < 11 * 24 * 3600:
return
import dis
dis.dis(f1)
# dis执行结果
5 0 LOAD_FAST 0 (delta_seconds)
2 LOAD_CONST 1 (950400)
4 COMPARE_OP 0 (<)
6 POP_JUMP_IF_FALSE 12
6 8 LOAD_CONST 0 (None)
10 RETURN_VALUE
>> 12 LOAD_CONST 0 (None)
14 RETURN_VALUE
你看到上面的“2 LOAD_CONST 1 (950400)
”了吗?这表明当Python解释器将源代码编译成字节码时,它将计算表达式“11 * 24 * 3600
”,并将其替换为“950400”。
因此,当我们的代码需要包含复杂计算的字面表达式时,请保留整个方程。这对性能没有影响,并提高了代码的可读性。
除了对数值字面表达式进行预计算外,Python解释器还对字符串和列表执行类似的操作。一切都是为了性能。谁让你总是抱怨Python速度慢呢?
今天,我们简单的了解了一些python中数字和字符串的简单实践,这样就可以提高你代码的可读性和维护性,主要包括:
好了,今天就讲到这里,下一期带来一些小TIPs,欢迎关注博主,不迷路。