Python中数字和字符串的最佳实践 (上)

数字是几乎所有编程语言中最基本的数据类型,它们构成了通过代码连接现实世界的基础。在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注入问题。因此,在你的代码中存在复杂的原始字符串处理逻辑时,请尽量用以下方法替换它:

问题:目标/源字符串是否结构化并遵循特定格式?

是:查找现有的开源面向对象模块来操作它们,或者自己写一个。

  • SQL:SQLAlchemy
  • XML:lxml
  • JSON,YAML …

否:尝试使用模板引擎而不是复杂的字符串处理逻辑来实现目标。

  • Jinja2
  • Mako
  • Mustache

不需要预先评估字面表达式

偶尔,在我们的代码中会有一些复杂的数字,就像下面的例子:

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中数字和字符串的简单实践,这样就可以提高你代码的可读性和维护性,主要包括:

  1. 使用枚举代替魔鬼数字
  2. 使用库文件进行长字符串的连接
  3. 对于有特殊意义的数字表达式 可以直接使用有意义的数字进行连接操作,比如一天多少秒直接写成: 24 * 60 * 60

好了,今天就讲到这里,下一期带来一些小TIPs,欢迎关注博主,不迷路。

你可能感兴趣的:(Python,python,数据库)