Python工匠:数字与字符串(上)

2019-05-20 15:10:04 浏览数 (1)

编程某种意义上是一门『手艺』,因为优雅而高效的代码,就如同完美的手工艺品一样让人赏心悦目。

致“匠人”

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

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

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

在这篇文章里,我们将讨论一些 更细微、更不常见 的编程实践。来帮助你写出更好的 Python 代码。

1

少写数字字面量

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

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

代码语言:javascript复制
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 的样例代码:

代码语言:javascript复制
# -*- 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 查询语句,就像下面这样:

代码语言:javascript复制
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 模块完成了同样的功能:

代码语言:javascript复制
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

不必预计算字面量表达式

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

代码语言:javascript复制
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 模块看看:

代码语言:javascript复制
def f1(delta_seconds):    if delta_seconds < 11 * 24 * 3600:        return
import disdis.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 替换它。

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

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

结束语

由于篇幅原因,一些常用的操作比如字符串格式化等,文章里并没有涵盖到。以后有机会再写吧。

原文作者:腾讯高级工程师 朱雷

来源:腾讯内部KM论坛

  -Python好课-  

从零开始,开发Wukong-robot

多方位Python体系,满足不同技能需求

腾讯NEXT学院

求职干货 | 前辈blog  | 前端课程

↓ 戳“阅读原文”享课程详情

var first_sceen__time = ( new Date());if ("" == 1 && document.getElementById('js_content')) { document.getElementById('js_content').addEventListener("selectstart",function(e){ e.preventDefault(); }); } (function(){ if (navigator.userAgent.indexOf("WindowsWechat") != -1){ var link = document.createElement('link'); var head = document.getElementsByTagName('head')[0]; link.rel = 'stylesheet'; link.type = 'text/css'; link.href = "//res.wx.qq.com/mmbizwap/zh_CN/htmledition/style/page/appmsg_new/winwx45ba31.css"; head.appendChild(link); } })();

爱学习的N妹

赞赏

长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

阅读原文

阅读

分享 在看

已同步到看一看

取消 发送

我知道了

朋友会在“发现-看一看”看到你“在看”的内容

确定

已同步到看一看写下你的想法

最多200字,当前共字 发送

已发送

朋友将在看一看看到

确定

写下你的想法...

取消

发布到看一看

确定

最多200字,当前共字

发送中

微信扫一扫 关注该公众号

微信扫一扫 使用小程序

即将打开""小程序

取消 打开

0 人点赞