快捷搜索:  as

Python编程使用数字与字符串的技巧

image

数字是险些所有编程说话里最基础的数据类型,它是我们经由过程代码连接现实天下的根基。在 Python 里有三种数值类型:整型(int)、浮点型(float)和复数(complex)。绝大年夜多半环境下,我们只必要和前两种打交道。

整型在 Python 中对照让人省心,由于它不区分有无符号并且永不溢出。但浮点型仍和绝大年夜多半其他编程说话一样,依然有着精度问题,常常让很多刚进入编程天下大年夜门的新人们认为利诱:

Why are floating point numbers inaccurate? ​ stackoverflow.com 比拟数字,Python 里的字符串要繁杂的多。要掌握它,你得先弄清楚 bytes 和 str 的差别。假如更不巧,你照样位 Python2 用户的话,光 unicode 和字符编码问题就够你喝上好几壶了(从速迁移到 Python3 吧,就在本日!)。

不过,上面提到的这些都不是这篇文章的主题,假如感兴趣,你可以在网上找到成堆的相关资料。在这篇文章里,我们将评论争论一些 更细微、更不常见 的编程实践。来赞助你写出更好的 Python 代码。

最佳实践

少写数字字面量

“数字字面量(integer literal)” 是指那些直接呈现在代码里的数字。它们散播在代码里的各个角落,比如代码 del users[0] 里的 0 便是一个数字字面量。它们简单、实用,每小我天天都在写。 然则,当你的代码里赓续重复呈现一些特定字面量时,你的“代码质量告警灯”就应该亮起黄灯 :traffic_light: 了。

举个例子,要是你刚加入一家心仪已久的新公司,同事转交给你的项目里有这么一个函数:

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

将重复呈现的数字字面量定义成罗列类型,不但可以改良代码的可读性,代码呈现 Bug 的几率也会低落。

试想一下,假如你在某个分支判断时将 11 错打成了 111 会怎么样?我们时常会犯这种错,而这类差错在早期分外难被发明。将这些数字字面量整个放入罗列类型中可以对照好的规避这类问题。类似的,将字符串字面量改写成罗列也可以得到同样的好处。

应用罗列类型代替字面量的好处:

提升代码可读性 :所有人都不必要影象某个神奇的数字代表什么

提升代码精确性 :削减打错数字或字母孕育发生 bug 的可能性

当然,你完全没有需要把代码里的所有字面量都改成罗列类型。 代码里呈现的字面量,只要在它所处的高低文里面目面貌易理解,就可以应用它。 比如那些常常作为数字下标呈现的 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: 排序字段,默觉得按 created "用户创建日期"

:returns: 列表:[(User ID, User Name), ...]

"""

# 一种古老的 SQL 拼接技术,应用 "WHERE 1=1" 来简化字符串拼接操作

# 区分查询 params 来避免 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 语句 - 是由于这样做简单、直接,相符直觉。然则这样做最大年夜的问题在于: 跟着函数逻辑变得更繁杂,这段拼接代码会变得轻易掉足、难以扩展。事实上,上面这段 Demo 代码也只是仅仅做到看上去 没有显着的 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 注入问题。以是,当你的代码中呈现繁杂的裸字符串处置惩罚逻辑时,请试着用下面的要领替代它:

Q: 目标/源字符串是布局化的,遵照某种款式吗?

是:找找是否已经有开源的工具化模块操作它们,或是自己写一个

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 “机能”,谜底必然会是“机能” 。我们都知道 Python 是一门(速率欠佳的)解释型说话,以是预先谋略出 950400 恰是由于我们不想让每次对函数 f1 的调用都带上这部分的谋略开销。不过事实是: 纵然我们把代码改成 if delta_seconds

Python 代码在履行时会被说冥器编译成字节码,而本相就藏在字节码里。让我们用 dis 模块看看:

def f1(delta_seconds):

if delta_seconds >12 LOAD_CONST0 (None)

14 RETURN_VALUE

望见上面的 2 LOAD_CONST 1 (950400) 了吗?这表示 Python 说冥器在将源码编译成成字节码时,管帐算 11 * 24 * 3600 这段整表达式,并用 950400 调换它。

以是, 当我们的代码中必要呈现繁杂谋略的字面量时,请保留全部算式吧。它对机能没有任何影响,而且会增添代码的可读性。

Hint:Python 说冥器除了会预谋略数值字面量表达式以外,还会对字符串、列表做类似的操作。统统都是为了机能。谁让你们老吐槽 Python 慢呢?

实用技术

1、布尔值着实也是“数字”

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'

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") # 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_FAST0 (value)

2 LOAD_CONST1 (1)

4 INPLACE_ADD

6 STORE_FAST0 (value)

8 LOAD_CONST0 (None)

10 RETURN_VALUE

在上面输出结果中,可以看到这个简单的累加语句,会被编译成包括取值和保存在内的好几个不合步骤,而在多线程情况下,随意率性一个其他线程都有可能在此中某个步骤切入进来,阻碍你得到精确的结果。

是以,请不要凭借自己的直觉来判断某个行径是否“线程安然”,不然等法度榜样在高并发情况下呈现稀罕的 bug 时,你将为自己的直觉付出惨痛的价值。

2、字符串拼接并不慢

我刚打仗 Python 不久时,在某个网站看到这样一个说法: “Python 里的字符串是弗成变的,以是每一次对字符串进行拼接都邑天生一个新工具,导致新的内存分配,效率异常低”。 我对此笃信不疑。

以是,不停以来,我只管即便都在避免应用 += 的要领去拼接字符串,而是用 "".join(str_list)之类的要领来替代。

然则,在某个偶尔的时机下,我对 Python 的字符串拼接做了一次简单的机能测试后发明: Python 的字符串拼接根本就不慢! 在查阅了一些资料后,终极发清楚明了本相。

Python 的字符串拼接在 2.2 以及之前的版本确凿很慢,和我最早看到的说法行径同等。然则由于这个操作太常用了,以是之后的版本里专门针对它做了机能优化。大年夜大年夜提升了履行效率。

如今应用 += 的要领来拼接字符串,效率已经异常靠近 "".join(str_list) 了。以是,该拼接时就拼接吧,不必担心任何机能问题。

Hint: 假如你想懂得更具体的相关内容,可以读一下这篇文章:

Python - Efficient String Concatenation in Python (2016 edition) ​ blog.mclemon.io

image

结语

以上便是『Python 工匠』系列文章的第三篇,内容对照零散。因为篇幅缘故原由,一些常用的操作比如字符串款式化等,文章里并没有涵盖到。今后有时机再写吧。

让我们着末再总结一下要点:

编写代码时,请斟酌涉猎者的感想熏染,不要呈现太多神奇的字面量

当操作布局化字符串时,应用工具化模块比直接处置惩罚更有上风

dis 模块异常有用,请多多应用它验证你的预测

多线程情况下的编码异常繁杂,要足够审慎,不要信托自己的直觉

Python 说话的更新异常快,不要被别人的履历所阁下

假如你对Python编程感兴趣,那么记得来小编的Python进修扣群:1017759557,这里有资本共享,技巧解答,大年夜家可以在一路交流Python编程履历,还有小编收拾的一份Python进修教程,盼望能赞助大年夜家更好的进修python。

您可能还会对下面的文章感兴趣: