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

本文介绍了在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,欢迎关注博主,不迷路。

<think>我们正在讨论如何从字符串中提取数字。参考引用提供了几种方法: 引用[2]引用[4]展示了使用正则表达式提取数字(包括整数小数)的方法。 引用[1]展示了使用re.split分割字符串,但分割后会产生空字符串,需要进一步处理。 引用[3]展示了过滤数字的另一种方法(但它是针对一个整数列表,而不是从字符串中提取)。 根据用户需求,我们需要总结在Python中提取字符串数字的几种常用方法。 主要方法: 1. 使用正则表达式:这是最常用灵活的方法。 2. 使用字符串处理函数(如split,filter等)结合数字判断,但这种方法可能更复杂,且对于小数负数处理较为困难。 我们重点介绍正则表达式方法,因为它能够处理各种情况(整数、小数、负数等)。 步骤: 1. 导入re模块 2. 使用re.findall()方法匹配数字模式 常见的数字模式: - 整数:\d+ - 小数:\d+\.\d+ 或 \d*\.\d+(允许小数点前没有数字,如.5,但通常我们要求至少有一个数字部分) 但我们通常希望同时匹配整数小数,可以使用:`r'\d+\.?\d*'` 或 `r'\d*\.?\d+'`,这两种模式略有不同。 注意:模式`r'\d+\.?\d*'`匹配: - 一个或多个数字(整数部分) - 可选的小数点(\.?) - 零个或多个数字(小数部分) 所以它可以匹配:123、123.45、123.(注意123.后面没有小数部分,它也会匹配),但不会匹配.45(因为整数部分至少一个数字) 而模式`r'\d*\.?\d+'`可以匹配.45(因为它允许整数部分为0个,然后小数点,然后至少一个数字),但不会匹配123.(因为小数部分要求至少一个数字)。 因此,为了同时匹配整数、小数(包括.45123.)的情况,我们可以使用更复杂的模式。但通常,我们只需要匹配常见的数字形式(整数标准小数,即整数部分小数部分至少有一个数字)。 如果我们想要匹配包括负数的情况,可以在模式前面加上可选的负号:`r'-?\d+\.?\d*'` 或 `r'-?\d*\.?\d+'`,但要注意负号可能出现的位置。 但是,用户的问题并没有提到负数,我们可以先不考虑负数。 根据引用[4]中的例子:`string = "A1.45,b5,6.45,8.82"`,模式`r"\d+\.?\d*"`可以提取出:['1.45','5','6.45','8.82'] 另外,引用[2]中使用了`r"\d+\.?\d*"`。 因此,我们首先推荐使用正则表达式re.findall方法。 除了正则表达式,我们也可以使用其他方法,但正则表达式是最简洁的。 我们还可以考虑使用re.split分割非数字字符,然后过滤出数字字符串,但这通常不如re.findall直接。 步骤总结: 方法1:使用re.findall匹配数字(推荐) 代码示例: import re text = "这里有3个苹果2.5公斤香蕉" numbers = re.findall(r'\d+\.?\d*', text) # 匹配整数小数(正数) # 结果:['3', '2.5'] 方法2:如果需要匹配负数,可以扩展模式 numbers = re.findall(r'-?\d+\.?\d*', text) # 匹配整数小数(包括负数) 方法3:如果需要匹配科学计数法,需要更复杂的模式,但用户没有提到,暂不讨论。 方法4:使用splitfilter(不推荐,复杂且易错) import re text = "A1.45,b5,6.45,8.82" # 使用非数字字符分割,但注意小数点不能分割 parts = re.split(r'[^\d.]+', text) # 使用非数字非小数点字符分割 # 然后过滤掉空字符串单独的小数点(如果有) numbers = [x for x in parts if x != '' and x != '.'] # 但这样得到的结果可能包含非数字字符串(如果有多个小数点),所以不如正则匹配准确。 因此,我们主要推荐正则表达式方法。 另外,如果我们只需要整数,可以使用`r'\d+'`。 根据引用[2]中的例子,他们使用了`r"\d+"`来提取所有整数。 总结代码: 示例1:提取所有整数 示例2:提取所有数值(包括小数) 注意:我们提取出来的是字符串列表,如果需要转换成数值类型,可以使用map(float, numbers)或者列表推导式。 代码示例: ```python import re # 示例字符串 text = "订单号123,价格56.78元,重量0.5kg,折扣-10%" # 提取整数(只考虑正整数) integers = re.findall(r'\d+', text) print(integers) # 输出: ['123', '56', '78', '0', '5', '10'] 注意:56.78被拆分成5678,0.5被拆分成05,负数符号被忽略 # 提取数值(包括小数,但不包括负数) numbers = re.findall(r'\d+\.?\d*', text) print(numbers) # 输出: ['123', '56.78', '0.5', '10'] 注意:负号没有被匹配,所以-10变成了10 # 提取数值(包括负数,但注意负号数字之间不能有空格) # 使用模式:-?\d+\.?\d* numbers_with_negative = re.findall(r'-?\d+\.?\d*', text) print(numbers_with_negative) # 输出: ['123', '56.78', '0.5', '-10'] 这里假设负号紧挨着数字 # 如果需要更精确地匹配负数(考虑负号前可能有空格),模式可以调整为:-?\s*\d+\.?\d*,但这样可能匹配到空格,通常我们要求负号紧挨数字。 # 转换类型 numbers_float = [float(num) for num in numbers_with_negative] print(numbers_float) # [123.0, 56.78, 0.5, -10.0] ``` 注意事项: 1. 正则表达式无法处理千分位分隔符(如1,000),如果字符串中有逗号分隔,需要先去除逗号或者调整正则表达式(例如:`r'-?\d{1,3}(?:,\d{3})*\.?\d*'`),但这样会复杂很多。 2. 如果字符串中的数字有特殊格式(如科学计数法),需要另外的模式。 因此,我们给出一个通用的方法,并说明其局限性。 最后,根据引用[1],他们使用re.split来分割,然后去掉空字符串。但这种方法不如re.findall直接。 综上所述,我们推荐使用正则表达式re.findall方法。 生成相关问题: 1. 如何提取字符串中的负数? 2. 如何处理带有千分位分隔符(逗号)的数字字符串? 3. 如何提取科学计数法表示的数字(如1.23e-4)? 4. 如何提取字符串中所有的数值(包括整数、小数、负数、分数等)? 5. 提取数字后如何转换成数值类型(整数或浮点数)? </think>### Python 提取字符串数字的完整方法 在 Python 中提取字符串中的数字有以下几种常用方法,根据需求选择最适合的方案: #### 方法 1:使用正则表达式提取整数小数(推荐) ```python import re text = "订单号123,价格56.78元,重量0.5kg" # 提取所有数值(包含小数) numbers = re.findall(r'\d+\.?\d*', text) # 匹配整数小数[^2][^4] print(numbers) # 输出: ['123', '56.78', '0.5'] # 仅提取整数 integers = re.findall(r'\d+', text) print(integers) # 输出: ['123', '56', '78', '0', '5'] ``` - **特点**: - `\d+\.?\d*` 匹配整数小数(如 123, 45.67) - `\d+` 仅匹配整数部分 - 返回字符串列表,需用 `float()` 或 `int()` 转换类型[^2] #### 方法 2:使用 split() 分割后过滤数字 ```python text = "产品A:100, 产品B:200.5" # 分割后过滤非数字部分 parts = re.split(r'\D+', text) # 按非数字字符分割[^1] numbers = [x for x in parts if x != ''] # 移除空字符串 print(numbers) # 输出: ['100', '200', '5'] ``` - **适用场景**: - 当数字之间有固定分隔符时 - 需手动处理小数点负数符号 #### 方法 3:使用 filter() isdigit()(仅整数) ```python text = "第1排第2座票价¥300" # 提取纯整数 numbers = list(filter(str.isdigit, text)) print(numbers) # 输出: ['1', '2', '3', '0', '0'] # 作为连续数字提取 continuous_nums = re.findall(r'\d+', text) print(continuous_nums) # 输出: ['1', '2', '300'] ``` - **限制**: - `isdigit()` 会将 "123" 拆分为 ['1','2','3'] - 无法处理小数点负数[^3] #### 方法 4:处理特殊格式(负数/科学计数法) ```python text = "温度:-12.5℃, 浓度:2.5e-3mol/L" # 匹配负数小数 numbers = re.findall(r'-?\d+\.?\d*', text) print(numbers) # 输出: ['-12.5', '2.5', '-3'] # 匹配科学计数法 sci_nums = re.findall(r'-?\d+\.?\d*[eE]-?\d+', text) print(sci_nums) # 输出: ['2.5e-3'] ``` ### 方法对比表 | 方法 | 适用场景 | 处理小数 | 处理负数 | 复杂度 | |--------------------|--------------------------|----------|----------|--------| | `re.findall('\d+\.?\d*')` | 常规数值提取 | ✓ | ✗ | 低 | | `re.split()` + 过滤 | 带分隔符的数值 | △ | ✗ | 中 | | `filter(isdigit)` | 纯整数提取 | ✗ | ✗ | 低 | | 自定义正则 | 特殊格式(科学计数法等) | ✓ | ✓ | 高 | > **最佳实践**:优先使用正则表达式 `re.findall(r'\d+\.?\d*', text)`,它能处理大多数场景[^2][^4]。需要转换数值类型时: > ```python > # 转换为浮点数列表 > float_nums = [float(num) for num in re.findall(r'-?\d+\.?\d*', "数据: 1.5, -2, 3.14")] > ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值