百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

Python 数字和字符串里的学问(python字符串和数字的区别)

off999 2024-11-01 12:51 31 浏览 0 评论

(第一批送书名单公布,见文末)

文 | piglei@piglei公众号

编辑 | EarlGrey

推荐 | 编程派公众号(ID:codingpy)

数字是几乎所有编程语言里最基本的数据类型,它是我们通过代码连接现实世界的基础。在 Python 里有三种数值类型:整型(int)、浮点型(float)和复数(complex)。绝大多数情况下,我们只需要和前两种打交道。

整型在 Python 中比较让人省心,因为它不区分有无符号并且永不溢出。但浮点型仍和绝大多数其他编程语言一样,依然有着精度问题,经常让很多刚进入编程世界大门的新人们感到困惑:"Why Are Floating Point Numbers Inaccurate?"。

相比数字,Python 里的字符串要复杂的多。要掌握它,你得先弄清楚 bytes 和 str 的区别。如果更不巧,你还是位 Python2 用户的话,光 unicode 和字符编码问题就够你喝上好几壶了(赶快迁移到 Python3 吧,就在今天!)。

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

最佳实践

1. 少写数字字面量

“数字字面量(integer literal)” 是指那些直接出现在代码里的数字。它们分布在代码里的各个角落,比如代码 delusers[0]里的0就是一个数字字面量。它们简单、实用,每个人每天都在写。但是,当你的代码里不断重复出现一些特定字面量时,你的“代码质量告警灯”就应该亮起黄灯 了。

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

  1. def mark_trip_as_featured(trip):

  2. """将某个旅程添加到推荐栏目

  3. """

  4. if trip.source == 11:

  5. do_some_thing(trip)

  6. elif trip.source == 12:

  7. do_some_other_thing(trip)

  8. ... ...

  9. return

这个函数做了什么事?你努力想搞懂它的意思,不过 trip.source==11是什么情况?那==12呢?这两行代码很简单,没有用到任何魔法特性。但初次接触代码的你可能需要花费一整个下午,才能弄懂它们的含义。

问题就出在那几个数字字面量上。最初写下这个函数的人,可能是在公司成立之初加入的那位元老程序员。而他对那几个数字的含义非常清楚。但如果你是一位刚接触这段代码的新人,就完全是另外一码事了。

使用 enum 枚举类型改善代码

那么,怎么改善这段代码?最直接的方式,就是为这两个条件分支添加注释。不过在这里,“添加注释”显然不是提升代码可读性的最佳办法(其实在绝大多数其他情况下都不是)。我们需要用有意义的名称来代替这些字面量,而 枚举类型(enum)用在这里最合适不过了。

enum是 Python 自 3.4 版本引入的内置模块,如果你使用的是更早的版本,可以通过pip install enum34来安装它。下面是使用 enum 的样例代码:

  1. # -*- coding: utf-8 -*-

  2. from enum import IntEnum


  3. class TripSource(IntEnum):

  4. FROM_WEBSITE = 11

  5. FROM_IOS_CLIENT = 12



  6. def mark_trip_as_featured(trip):

  7. if trip.source == TripSource.FROM_WEBSITE:

  8. do_some_thing(trip)

  9. elif trip.source == TripSource.FROM_IOS_CLIENT:

  10. do_some_other_thing(trip)

  11. ... ...

  12. return

将重复出现的数字字面量定义成枚举类型,不光可以改善代码的可读性,代码出现 Bug 的几率也会降低。

试想一下,如果你在某个分支判断时将 11错打成了111会怎么样?我们时常会犯这种错,而这类错误在早期特别难被发现。将这些数字字面量全部放入枚举类型中可以比较好的规避这类问题。类似的,将字符串字面量改写成枚举也可以获得同样的好处。

使用枚举类型代替字面量的好处:

  • 提升代码可读性:所有人都不需要记忆某个神奇的数字代表什么

  • 提升代码正确性:减少打错数字或字母产生 bug 的可能性

当然,你完全没有必要把代码里的所有字面量都改成枚举类型。代码里出现的字面量,只要在它所处的上下文里面容易理解,就可以使用它。比如那些经常作为数字下标出现的0-1就完全没有问题,因为所有人都知道它们的意思。

2. 别在裸字符串处理上走太远

什么是“裸字符串处理”?在这篇文章里,它指只使用基本的加减乘除和循环、配合内置函数/方法来操作字符串,获得我们需要的结果。

所有人都写过这样的代码。有时候我们需要拼接一大段发给用户的告警信息,有时我们需要构造一大段发送给数据库的 SQL 查询语句,就像下面这样:

  1. def fetch_users(conn, min_level=None, gender=None, has_membership=False, sort_field="created"):

  2. """获取用户列表


  3. :param int min_level: 要求的最低用户级别,默认为所有级别

  4. :param int gender: 筛选用户性别,默认为所有性别

  5. :param int has_membership: 筛选所有会员/非会员用户,默认非会员

  6. :param str sort_field: 排序字段,默认为按 created "用户创建日期"

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

  8. """

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

  10. # 区分查询 params 来避免 SQL 注入问题

  11. statement = "SELECT id, name FROM users WHERE 1=1"

  12. params =

  13. if min_level is not None:

  14. statement += " AND level >= ?"

  15. params.append(min_level)

  16. if gender is not None:

  17. statement += " AND gender >= ?"

  18. params.append(gender)

  19. if has_membership:

  20. statement += " AND has_membership == true"

  21. else:

  22. statement += " AND has_membership == false"


  23. statement += " ORDER BY ?"

  24. params.append(sort_field)

  25. return list(conn.execute(statement, params))

我们之所以用这种方式拼接出需要的字符串 - 在这里是 SQL 语句 - 是因为这样做简单、直接,符合直觉。但是这样做最大的问题在于:随着函数逻辑变得更复杂,这段拼接代码会变得容易出错、难以扩展。事实上,上面这段 Demo 代码也只是仅仅做到看上去没有明显的 bug 而已 (谁知道有没有其他隐藏问题)。

其实,对于 SQL 语句这种结构化、有规则的字符串,用对象化的方式构建和编辑它才是更好的做法。下面这段代码用 SQLAlchemy 模块完成了同样的功能:

  1. def fetch_users_v2(conn, min_level=None, gender=None, has_membership=False, sort_field="created"):

  2. """获取用户列表

  3. """

  4. query = select([users.c.id, users.c.name])

  5. if min_level is not None:

  6. query = query.where(users.c.level >= min_level)

  7. if gender is not None:

  8. query = query.where(users.c.gender == gender)

  9. query = query.where(users.c.has_membership == has_membership).order_by(users.c[sort_field])

  10. return list(conn.execute(query))

上面的 fetch_users_v2函数更短也更好维护,而且根本不需要担心 SQL 注入问题。所以,当你的代码中出现复杂的裸字符串处理逻辑时,请试着用下面的方式替代它:

Q:目标/源字符串是结构化的,遵循某种格式吗?

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

    • SQL:SQLAlchemy

    • XML:lxml

    • JSON、YAML ...

  • 否:尝试使用模板引擎而不是复杂字符串处理逻辑来达到目的

    • Jinja2

    • Mako

    • Mustache

3. 不必预计算字面量表达式

我们的代码里偶尔会出现一些比较复杂的数字,就像下面这样:

  1. def f1(delta_seconds):

  2. # 如果时间已经过去了超过 11 天,不做任何事

  3. if delta_seconds > 950400:

  4. return

  5. ...

话说在前头,上面的代码没有任何毛病。

首先,我们在小本子(当然,和我一样的聪明人会用 IPython)上算了算:11天一共包含多少秒?。然后再把结果950400这个神奇的数字填进我们的代码里,最后心满意足的在上面补上一行注释:告诉所有人这个神奇的数字是怎么来的。

我想问的是:“为什么我们不直接把代码写成 ifdelta_seconds<11*24*3600:呢?”

“性能”,答案一定会是“性能”。我们都知道 Python 是一门~~(速度欠佳的)~~解释型语言,所以预先计算出950400正是因为我们不想让每次对函数f1的调用都带上这部分的计算开销。不过事实是:即使我们把代码改成ifdelta_seconds<11*24*3600:,函数也不会多出任何额外的开销。

Python 代码在执行时会被解释器编译成字节码,而真相就藏在字节码里。让我们用 dis 模块看看:

  1. def f1(delta_seconds):

  2. if delta_seconds < 11 * 24 * 3600:

  3. return


  4. import dis

  5. dis.dis(f1)


  6. # dis 执行结果

  7. 5 0 LOAD_FAST 0 (delta_seconds)

  8. 2 LOAD_CONST 1 (950400)

  9. 4 COMPARE_OP 0 (<)

  10. 6 POP_JUMP_IF_FALSE 12


  11. 6 8 LOAD_CONST 0 (None)

  12. 10 RETURN_VALUE

  13. >> 12 LOAD_CONST 0 (None)

  14. 14 RETURN_VALUE

看见上面的 2LOAD_CONST1(950400)了吗?这表示 Python 解释器在将源码编译成成字节码时,会计算11*24*3600这段整表达式,并用950400替换它。

所以,当我们的代码中需要出现复杂计算的字面量时,请保留整个算式吧。它对性能没有任何影响,而且会增加代码的可读性。

Hint:Python 解释器除了会预计算数值字面量表达式以外,还会对字符串、列表做类似的操作。一切都是为了性能。谁让你们老吐槽 Python 慢呢?

实用技巧

1. 布尔值其实也是“数字”

Python 里的两个布尔值 TrueFalse在绝大多数情况下都可以直接等价于10两个整数来使用,就像这样:

  1. >>> True + 1

  2. 2

  3. >>> 1 / False

  4. Traceback (most recent call last):

  5. File "<stdin>", line 1, in <module>

  6. ZeroDivisionError: division by zero

那么记住这点有什么用呢?首先,它们可以配合 sum函数在需要计算总数时简化操作:

  1. >>> l = [1, 2, 4, 5, 7]

  2. >>> sum(i % 2 == 0 for i in l)

  3. 2

此外,如果将某个布尔值表达式作为列表的下标使用,可以实现类似三元表达式的目的:

  1. # 类似的三元表达式:"Javascript" if 2 > 1 else "Python"

  2. >>> ["Python", "Javascript"][2 > 1]

  3. 'Javascript'

2. 改善超长字符串的可读性

单行代码的长度不宜太长。比如 PEP8 里就建议每行字符数不得超过 79。现实世界里,大部分人遵循的单行最大字符数在 79 到 119 之间。如果只是代码,这样的要求是比较容易达到的,但假设代码里需要出现一段超长的字符串呢?

这时,除了使用斜杠 \和加号+将长字符串拆分为好几段以外,还有一种更简单的办法:使用括号将长字符串包起来,然后就可以随意折行了

  1. def main:

  2. logger.info(("There is something really bad happened during the process. "

  3. "Please contact your administrator."))

当多级缩进里出现多行字符串时

日常编码时,还有一种比较麻烦的情况。就是需要在已经有缩进层级的代码里,插入多行字符串字面量。因为多行字符串不能包含当前的缩进空格,所以,我们需要把代码写成这样:

  1. def main:

  2. if user.is_active:

  3. message = """Welcome, today's movie list:

  4. - Jaw (1975)

  5. - The Shining (1980)

  6. - Saw (2004)"""

但是这样写会破坏整段代码的缩进视觉效果,显得非常突兀。要改善它有很多种办法,比如我们可以把这段多行字符串作为变量提取到模块的最外层。不过,如果在你的代码逻辑里更适合用字面量的话,你也可以用标准库 textwrap来解决这个问题:

  1. from textwrap import dedent


  2. def main:

  3. if user.is_active:

  4. # dedent 将会缩进掉整段文字最左边的空字符串

  5. message = dedent("""\

  6. Welcome, today's movie list:

  7. - Jaw (1975)

  8. - The Shining (1980)

  9. - Saw (2004)""")

3. 别忘了那些 “r” 开头的内建字符串函数

Python 的字符串有着非常多实用的内建方法,最常用的有 .strip.split等。这些内建方法里的大多数,处理起来的顺序都是从左往右。但是其中也包含了部分以r打头的从右至左处理的镜像方法。在处理特定逻辑时,使用它们可以让你事半功倍。

假设我们需要解析一些访问日志,日志格式为:"{useragent}" {contentlength}:

  1. >>> log_line = '"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" 47632'

如果使用 .split将日志拆分为(user_agent,content_length),我们需要这么写:

  1. >>> l = log_line.split

  2. >>> " ".join(l[:-1]), l[-1]

  3. ('"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"', '47632')

但是如果使用 .rsplit的话,处理逻辑就更直接了:

  1. >>> log_line.rsplit(None, 1)

  2. ['"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")<任意数值<float("inf")

因为它们有着这样的特点,我们可以在某些场景用上它们:

  1. # A. 根据年龄升序排序,没有提供年龄放在最后边

  2. >>> users = {"tom": 19, "jenny": 13, "jack": None, "andrew": 43}

  3. >>> sorted(users.keys, key=lambda user: users.get(user) or float('inf'))

  4. ['jenny', 'tom', 'andrew', 'jack']


  5. # B. 作为循环初始值,简化第一次判断逻辑

  6. >>> max_num = float('-inf')

  7. >>> # 找到列表中最大的数字

  8. >>> for i in [23, 71, 3, 21, 8]:

  9. ...: if i > max_num:

  10. ...: max_num = i

  11. ...:

  12. >>> max_num

  13. 71

常见误区

1. “value += 1” 并非线程安全

当我们编写多线程程序时,经常需要处理复杂的共享变量和竞态等问题。

“线程安全”,通常被用来形容 某个行为或者某类数据结构,可以在多线程环境下被共享使用并产生预期内的结果。一个典型的满足“线程安全”的模块就是 queue 队列模块。

而我们常做的 value+=1操作,很容易被想当然的认为是“线程安全”的。因为它看上去就是一个原子操作 (指一个最小的操作单位,执行途中不会插入任何其他操作)。然而真相并非如此,虽然从 Python 代码上来看,value+=1这个操作像是原子的。但它最终被 Python 解释器执行的时候,早就不再 “原子” 了。

我们可以用前面提到的 dis模块来验证一下:

  1. def incr(value):

  2. value += 1



  3. # 使用 dis 模块查看字节码

  4. import dis


  5. dis.dis(incr)

  6. 0 LOAD_FAST 0 (value)

  7. 2 LOAD_CONST 1 (1)

  8. 4 INPLACE_ADD

  9. 6 STORE_FAST 0 (value)

  10. 8 LOAD_CONST 0 (None)

  11. 10 RETURN_VALUE

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

因此,请不要凭借自己的直觉来判断某个行为是否“线程安全”,不然等程序在高并发环境下出现奇怪的 bug 时,你将为自己的直觉付出惨痛的代价。

2. 字符串拼接并不慢

我刚接触 Python 不久时,在某个网站看到这样一个说法:“Python 里的字符串是不可变的,所以每一次对字符串进行拼接都会生成一个新对象,导致新的内存分配,效率非常低”。我对此深信不疑。

所以,一直以来,我尽量都在避免使用 +=的方式去拼接字符串,而是用"".join(str_list)之类的方式来替代。

但是,在某个偶然的机会下,我对 Python 的字符串拼接做了一次简单的性能测试后发现:Python 的字符串拼接根本就不慢!在查阅了一些资料后,最终发现了真相。

Python 的字符串拼接在 2.2 以及之前的版本确实很慢,和我最早看到的说法行为一致。但是因为这个操作太常用了,所以之后的版本里专门针对它做了性能优化。大大提升了执行效率。

如今使用 +=的方式来拼接字符串,效率已经非常接近"".join(str_list)了。所以,该拼接时就拼接吧,不必担心任何性能问题。

结语

以上就是『Python 工匠』系列文章的第三篇,内容比较零碎。由于篇幅原因,一些常用的操作比如字符串格式化等,文章里并没有涵盖到。以后有机会再写吧。

让我们最后再总结一下要点:

  • 编写代码时,请考虑阅读者的感受,不要出现太多神奇的字面量

  • 当操作结构化字符串时,使用对象化模块比直接处理更有优势

  • dis 模块非常有用,请多多使用它验证你的猜测

  • 多线程环境下的编码非常复杂,要足够谨慎,不要相信自己的直觉

  • Python 语言的更新非常快,不要被别人的经验所左右

# 文末送书活动 #

活动规则大家请一定要查看,点击这篇推文哦:从今天起,每天至少送大家一本书

这篇推文的福利赠书,改为:《Python 编程快速上手》,送给本篇文章点赞最多、且符合活动要求的精选留言者。

本次活动的第一篇推文获奖名单,其实昨天已经揭晓咯!后面希望每天都会有一位幸运读者诞生吧!

我临时决定增加一个名额,给到幸运读者:change、经营简单,请两位添加我的个人微信号:codingpython,提供收货地址,并告知希望赠送的书籍名称。(P.S 我今天有考试,可能没法及时通过好友申请)

相关推荐

安全教育登录入口平台(安全教育登录入口平台官网)

122交通安全教育怎么登录:122交通网的注册方法是首先登录网址http://www.122.cn/,接着打开网页后,点击右上角的“个人登录”;其次进入邮箱注册,然后进入到注册页面,输入相关信息即可完...

大鱼吃小鱼经典版(大鱼吃小鱼经典版(经典版)官方版)

大鱼吃小鱼小鱼吃虾是于谦跟郭麒麟的《我的棒儿呢?》郭德纲说于思洋郭麒麟作诗的相声,最后郭麒麟做了一首,师傅躺在师母身上大鱼吃小鱼小鱼吃虾虾吃水水落石出师傅压师娘师娘压床床压地地动山摇。...

谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
哪个软件可以免费pdf转ppt(免费的pdf转ppt软件哪个好)
哪个软件可以免费pdf转ppt(免费的pdf转ppt软件哪个好)

要想将ppt免费转换为pdf的话,我们建议大家可以下一个那个wps,如果你是会员的话,可以注册为会员,这样的话,在wps里面的话,就可以免费将ppt呢转换为pdfpdf之后呢,我们就可以直接使用,不需要去直接不需要去另外保存,为什么格式转...

2026-02-04 09:03 off999

电信宽带测速官网入口(电信宽带测速官网入口app)

这个网站看看http://www.swok.cn/pcindex.jsp1.登录中国电信网上营业厅,宽带光纤,贴心服务,宽带测速2.下载第三方软件,如360等。进行在线测速进行宽带测速时,尽...

植物大战僵尸95版手机下载(植物大战僵尸95 版下载)

1可以在应用商店或者游戏平台上下载植物大战僵尸95版手机游戏。2下载教程:打开应用商店或者游戏平台,搜索“植物大战僵尸95版”,找到游戏后点击下载按钮,等待下载完成即可安装并开始游戏。3注意:确...

免费下载ppt成品的网站(ppt成品免费下载的网站有哪些)

1、Chuangkit(chuangkit.com)直达地址:chuangkit.com2、Woodo幻灯片(woodo.cn)直达链接:woodo.cn3、OfficePlus(officeplu...

2025世界杯赛程表(2025世界杯在哪个国家)

2022年卡塔尔世界杯赛程公布,全部比赛在卡塔尔境内8座球场举行,2022年,决赛阶段球队全部确定。揭幕战于当地时间11月20日19时进行,由东道主卡塔尔对阵厄瓜多尔,决赛于当地时间12月18日...

下载搜狐视频电视剧(搜狐电视剧下载安装)

搜狐视频APP下载好的视频想要导出到手机相册里方法如下1、打开手机搜狐视频软件,进入搜狐视频后我们点击右上角的“查找”,找到自已喜欢的视频。2、在“浏览器页面搜索”窗口中,输入要下载的视频的名称,然后...

pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
永久免费听歌网站(丫丫音乐网)

可以到《我爱音乐网》《好听音乐网》《一听音乐网》《YYMP3音乐网》还可以到《九天音乐网》永久免费听歌软件有酷狗音乐和天猫精灵,以前要跳舞经常要下载舞曲,我从QQ上找不到舞曲下载就从酷狗音乐上找,大多...

音乐格式转换mp3软件(音乐格式转换器免费版)

有两种方法:方法一在手机上操作:1、进入手机中的文件管理。2、在其中选择“音乐”,将显示出手机中的全部音乐。3、点击“全选”,选中所有音乐文件。4、点击屏幕右下方的省略号图标,在弹出菜单中选择“...

电子书txt下载(免费的最全的小说阅读器)

1.Z-library里面收录了近千万本电子书籍,需求量大。2.苦瓜书盘没有广告,不需要账号注册,使用起来非常简单,直接搜索预览下载即可。3.鸠摩搜书整体风格简洁清晰,书籍资源丰富。4.亚马逊图书书籍...

最好免费观看高清电影(播放免费的最好看的电影)

在目前的网上选择中,IMDb(互联网电影数据库)被认为是最全的电影网站之一。这个网站提供了各种类型的电影和电视节目的海量信息,包括剧情介绍、演员表、评价、评论等。其还提供了有关电影制作背后的详细信息,...

孤单枪手2简体中文版(孤单枪手2简体中文版官方下载)

要将《孤胆枪手2》游戏的征兵秘籍切换为中文,您可以按照以下步骤进行操作:首先,打开游戏设置选项,通常可以在游戏主菜单或游戏内部找到。然后,寻找语言选项或界面选项,点击进入。在语言选项中,选择中文作为游...

取消回复欢迎 发表评论: