Python 编程语言要掌握的技能之一:使用数字与字符串的技巧

最佳实践

1. 少写数字字面量

“数字字面量(integer literal)” 是指那些直接出现在代码里的数字。它们分布在代码里的各个角落,比如代码 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(IntEum):

    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 就完全没有问题,因为所有人都知道它们的意思。

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

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

所有人都写过这样的代码。有时候我们需要拼接一大段发给用户的告警信息,有时我们需要构造一大段发送给数据库的 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!= None:

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

    **if** gender!= 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: 目标/源字符串是结构化的,遵循某种格式吗?

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

o SQL:SQLAlchemy

o XML:lxml

o JSON、YAML …

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

o Jinja2

o Mako

o Mustache

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

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

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_CONST 0 (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)

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

# 类似的三元表达式:"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

  

常见误区

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

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

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

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

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

def incr(value):

    value+= 1

# 使用 dis 模块查看字节码

import dis

dis.dis(incr)

      0 LOAD_FAST                0 (value)

      2 LOAD_CONST 1 (1)

      4 INPLACE_ADD

      6 STORE_FAST 0 (value)

      8 LOAD_CONST 0 (None)

 10 RETURN_VALUE

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

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

2. 字符串拼接并不慢

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

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

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

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

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

结语

以上就是『Python 编程语言要掌握的技能之一:』系列文章的第三篇,内容比较零碎。希望大家能够喜欢,有问题欢迎大家在评论区留言

更多Python技术文章请关注2019,Python技术持续更新(附教程)

原文地址:https://www.cnblogs.com/chuyang2017/p/10541596.html

时间: 2024-11-09 23:15:23

Python 编程语言要掌握的技能之一:使用数字与字符串的技巧的相关文章

Python 编程语言要掌握的技能之一:善用变量来改善代码质量

如何为变量起名 在计算机科学领域,有一句著名的格言(俏皮话): There are only two hard things in Computer Science: cache invalidation and naming things. 在计算机科学领域只有两件难事:缓存过期 和 给东西起名字 — Phil Karlton 第一个『缓存过期问题』的难度不用多说,任何用过缓存的人都会懂.至于第二个『给东西起名字』这事的难度,我也是深有体会.在我的职业生涯里,度过的作为黑暗的下午之一,就是坐在

Python 编程语言要掌握的技能之一:编写条件分支代码的技巧

Python 里的分支代码 Python 支持最为常见的 if/else 条件分支语句,不过它缺少在其他编程语言中常见的 switch/case 语句. 除此之外,Python 还为 for/while 循环以及 try/except 语句提供了 else 分支,在一些特殊的场景下,它们可以大显身手. 下面我会从 最佳实践.常见技巧.常见陷阱 三个方面讲一下如果编写优秀的条件分支代码. 最佳实践 1. 避免多层分支嵌套 如果这篇文章只能删减成一句话就结束,那么那句话一定是“要竭尽所能的避免分支嵌

Python - 基本数据类型及其常用的方法之数字与字符串

数字(int): 1.int()(将字符串换为数字) a = "123" print(type(a), a) b = int(a) print(type(b), b) num = "a" # 使用 int 方法时默认转换为十进制数 # 通过base来指定转换后的类型 v = int(num, base=16) print(v) 输出: <class 'str'> 123 <class 'int'> 123 10 2.bit_length()

Python工程师具备了这些技能,高薪offer不是问题!

对于新人而言,无论学习什么技术,都要以鼓励的姿态出现.如果只是一味地用薪水和个人所看到的局限性现状去衡量一门技术,那绝对是欠缺眼光的.作为一名Python工程师,究竟具备哪些技能才能提升求职机率?今天我就和大家简单来说说: Python编程语言基础 Python语言基础是入门Python的第一步,Python对于新手来说十分友好,入门相对容易同时又可以干很多事,例如网站开发.运维.数据.爬虫等,在诸多方面,Python都是一门非常方便顺手的工具语言.目前国内很多比较知名的网站,例如豆瓣.知乎.果

Python编程语言历史及特性01

一.Python语言 Python(英语发音:/paθn/), 是一种面向对象.解释型计算机程序设计语言,由Guido van Rossum于1989年底发明,第一个公开发行版发行于1991年.Python语法简洁而清晰,具有丰富和强大的类库.它常被昵称为胶水语言,它能够很轻松的把用其他语言制作的各种模块(尤其是C/C++)轻松地联结在一起.常见的一种应用情形是,使用Python快速生成程序的原型(有时甚至是程序的最终界面),然后对其中有特别要求的部分,用更合适的语言改写,比如3D游戏中的图形渲

【美妙的Python之四】标准变量类型-数字与字符串

美妙的Python之Python标准变量类型 简而言之: Python 是能你无限惊喜的语言,与众不同.         1.数字:        Python支持四种数字变量类型: int(有符号整形) long(长整形) float(浮点型) complex(复数)        数字变量用于存储数值,数字对象是不可改变对象,因此改变数字变量的值会分配一个新的对象.       如下示意图:          变量相当于便签,a=3则相当于将a便签贴到值为3的数字对象上,这样便可以通过便签a

Python装饰器、迭代器&amp;生成器、re正则表达式、字符串格式化

Python装饰器.迭代器&生成器.re正则表达式.字符串格式化 本章内容: 装饰器 迭代器 & 生成器 re 正则表达式 字符串格式化 装饰器 装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理等.装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用.概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能. 先定义一个基本的装饰器: ########## 基本装饰器 ########

python基础巩固(变量类型:字典、集合、元组、列表、数字、字符串)

Python 3 教程 菜鸟教程 https://www.runoob.com/python3/python3-tutorial.html 在控制台DOS运行.py文件 先切换到.py文件所在目录 再输入python hello.py (输入python3 hello.py执行会报错,这里与菜鸟教程不同) 在Unix & Linux 平台安装 Python3 先下载Unix/Linux的源码压缩包 https://www.python.org/downloads/source/ 解压压缩包,并安

笔记(用Python做些事情)--变量(数字、字符串)

Python是动态类型,可以用type()查看类型: Python一切皆对象(对象是类的实例化,三个主要概念封装,继承,多态): //:整除符号: decimal类型:高精度的数字类型,但效率不高: 数字类型常用的函数:import math math.pi:π math.sqrt(80):开平方根 math.log10(2**1000):log以10为底 math.pow(x,y):x的y次方 math.factoria(x):阶乘 数字类型常用的函数:import random random