关于python:Python-工匠使用数字与字符串的技巧

48次阅读

共计 8329 个字符,预计需要花费 21 分钟才能阅读完成。

序言

这是“Python 工匠”系列的第 3 篇文章。
数字是简直所有编程语言里最根本的数据类型,它是咱们通过代码连贯事实世界的根底。在 Python 里有三种数值类型:整型(int)、浮点型(float)和复数(complex)。绝大多数状况下,咱们只须要和前两种打交道。

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

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

不过,下面提到的这些都不是这篇文章的主题,如果感兴趣,你能够在网上找到成堆的相干材料。在这篇文章里,咱们将探讨一些 更轻微、更不常见 的编程实际。来帮忙你写出更好的 Python 代码。


内容目录

  • 最佳实际

1 少写数字字面量,应用 enum 枚举类型改善代码
2 别在裸字符串解决上走太远
3 不用预计算字面量表达式

  • 实用技巧

1 当多级缩进里呈现多行字符串时
2 布尔值其实也是“数字”
3 改善超长字符串的可读性
4 别忘了那些“r”结尾的内建字符串函数
5 应用“无穷大”float(“inf”)

  • 常见误区

1“value = 1”并非线程平安
2 字符串拼接并不慢


最佳实际

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(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 就齐全没有问题,因为所有人都晓得它们的意思。

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 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

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

咱们的代码里偶然会呈现一些比较复杂的数字,就像上面这样

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_VALU

看见下面的 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 "<stdin>", line 1, in <module>
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”) < 任意数值 < 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_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) 了。所以,该拼接时就拼接吧,不用放心任何性能问题。

Hint: 如果你想理解更具体的相干内容,能够读一下这篇文章:Python – Efficient String Concatenation in Python (2016 edition) – smcl


结语

以上就是『Python 工匠』系列文章的第三篇,内容比拟系统。因为篇幅起因,一些罕用的操作比方字符串格式化等,文章里并没有涵盖到。当前有机会再写吧。

让咱们最初再总结一下要点:

  • 编写代码时,请思考阅读者的感触,不要呈现太多神奇的字面量
  • 当操作结构化字符串时,应用对象化模块比间接解决更有劣势
  • dis 模块十分有用,请多多应用它验证你的猜想
  • 多线程环境下的编码非常复杂,要足够审慎,不要置信本人的直觉
  • Python 语言的更新十分快,不要被他人的教训所左右

看完文章的你,有没有什么想吐槽的?请留言或者在 我的项目 Github Issues 通知我吧。


往期举荐

Python 工匠:善用变量来改善代码品质
Python 工匠:编写条件分支代码的技巧


蓝鲸智云简介

腾讯蓝鲸智云(简称蓝鲸)软件体系是一套基于 PaaS 的技术解决方案,致力于打造行业当先的一站式自动化运维平台。目前曾经推出社区版、企业版、私有云版,欢送体验。请点击拜访蓝鲸官网:http://bk.tencent.com/

正文完
 0