怎样才能写出Pythonic的代码 #P001#

1 什么是Pythonic

在Python语言里面,有一个专门的词用来形容代码写的好,那就是“Pythonic”。那么,何为Pythonic呢?相信不少Python工程师都知道,Python中有一个彩蛋,回答了什么是Pythonic。这个彩蛋,也就是所谓的Python之禅(The Zen of Python)。

只需要在Python的交互模式下,导入this库,就可以看到Python之禅的详细内容。如下所示:

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
....
Readability counts.
....

为了节省文章的篇幅,这里只给出了一部分内容。读者可以在Python的交互模式下导入this库查看完整的内容。接下来,我们简单翻译几句Python之禅:

优美胜于丑陋
明了胜于晦涩
简介胜于复杂
……
可读性很重要
……

大家有没有发现,所谓的Python之禅,简直就是正确的废话。它只告诉我们什么是好,什么是不好,但是,却没有告诉我们通往成功彼岸的方法。关于Pythonic,除了禅意以外,更重要的是,还需要方法论,教我们如何写出Pythonic的代码。此外,Pythonic这个词也没有一个准确的定义,为了帮助大家理解,我们给Pythonic下一个明确的定义:Pythonic就是以Python的方式写出简洁优美的代码!

2 利用Python语言特性写出简洁优美的代码

由于这是本课程的第一篇内容,课程还没有介绍什么是好多代码,什么是不好的代码。因此,先来看一个例子,用以说明什么是简洁优美的代码。

在Python里面,字符串、列表和元组,都称之为序列。序列支持索引操作和切片操作。切片操作可以指定起点、终点和步长,步长也可以为负数。看一下下面的切片操作:

L = [1, 2, 3, 4, 5, 6, 7, 8, 9]
L[-2::-2]
L[-2:2:-2]
L[2:2:-2]

谁能够快速的回答上面几个切片的答案?没有人!因此,虽然Python拥有非常灵活的语法,但是,也不能够乱用。就这个例子来说:

  • 在同一个切片操作中,不要同时使用start、end和stride
  • 尽量使用stride为正数,且不要带start和end索引的切片操作

如果读者具有其他编程语言的背景,那么,在你已经能够写出简洁优美的代码的前提下,要写出Pythonic的代码,还需要对Python的语言特性有比较好的了解,才能够写出简洁优美的Python代码。例如,下面的例子在Python里面与在C、C++和Java里面有着显著的差别。

  1. 交换两个数字

    在其他语言里面:

     t = a
     a = b
     b = t
    

    在Python语言里面

     a, b = b, a
    
  2. 列表推导

    列表推导是C、C++、Java里面没有的语法,却是Python里面使用非常广泛,是特别推荐的用法。与列表推导对应的,还有集合推导和字典推导。我们来演示一下。

    • 列表:30~40 所有偶数的平方

        [ i*i for i in range(30, 41) if i% 2 == 0 ]
      
    • 集合:1~20所有奇数的平方的集合

        { i*i for i in range(1, 21) if i % 2 != 0 }
      
    • 字典:30~40 所有奇数的平方

        { i:i*i for i in range(30, 40) if i% 2 != 0 }
      

    再看两个更加实用的例子:

    • 当前用户home目录下所有的文件列表

        [ item for item in os.listdir(os.path.expanduser('~')) if os.path.isfile(item) ]
      
    • 当前用户home目录下所有的目录列表

        [ item for item in os.listdir(os.path.expanduser('~')) if os.path.isdir(item) ]
      
    • 当前用户home目录下所有目录的目录名到绝对路径之间的字典

        { item: os.path.realpath(item) for item in os.listdir(os.path.expanduser('~')) if os.path.isdir(item) }
      
  3. 上下文管理器

    在编程中,我们要打开文件进行处理,在处理文件过程中可能会出错。我们需要在处理文件出错的情况下,也能够顺利关闭文件。

    • Java风格/C++风格的Python代码:

        myfile= open(r'C:\misc\data.txt')
        try:
            for line in myfile:
                ...use line here...
        finally:
            myfile.close()
      
    • Pythonic的代码:

        with open(r'C:\misc\data.txt') as myfile:
            for line in myfile:
                ...use line here...
      

    这里要强调的是,上下文管理器是Python里面比较推荐的方式,如果用try...finally而不用with,就会被认为不够Pythonic。此外,上下文管理器还可以应用于锁和其他很多类似必须需要执行清理操作的场景,我们会在接下来的内容中进行详细介绍。

  4. 内置函数

    • enumerate

      enumerate是一个类,但是用起来却跟函数一样方便,为了表述方便,我们后面统称为函数。不使用enumerate可能是Python新手最容易被吐槽的地方了。enumerate其实非常简单,接收一个可迭代对象,返回index和可迭代对象中的元素的组合。

      对于Python初学者,推荐使用IPython交互式地测试各个函数的效果,并且,我们可以在函数后面输入一个问号,然后回车,就能够获得这个函数的帮助文档了。如下所示:

        In [1]: enumerate?
        Type:       type
        String Form:<type 'enumerate'>
        Namespace:  Python builtin
        Docstring:
        enumerate(iterable[, start]) -> iterator for index, value of iterable...
      

      关于enumerate的效果,一起来看一个例子,大家就知道为什么不使用enumerate会被吐槽了。下面的代码是不使用enumerate时,打印列表中的元素和元素在列表中位置的代码:

        from __future__ import print_function
      
        L = [ i*i for i in range(5) ]
      
        index = 0
        for data in L:
            index += 1
            print(index, ':', data)
      

      这是使用enumerate的Python代码:

        from __future__ import print_function
      
        L = [ i*i for i in range(5) ]
      
        for index, data in enumerate(L):
            print(index + 1, ':',  data)
      

      这是正确使用enumerate的姿势:

        from __future__ import print_function
      
        L = [ i*i for i in range(5) ]
      
        for index, data in enumerate(L, 1):
            print(index, ':',  data)
      

      去除import语句和列表的定义,实现同样的功能,不使用enumerate需要4行代码,使用enumerate只需要2行代码。如果想把代码写得简洁优美,那么,大家要时刻记住:在保证代码可读性的前提下,代码越少越好。显然,使用enumerate效果就好很多。

    • any

      在内置函数中,sort、sum、min和max是大家用的比较多的,也比较熟悉的。像any和all这种函数,是大家都知道,并且觉得很简单,但是使用的时候就想不起来的。我们来看一个具体的例子。

      我们现在的需求是判断MySQL中的一张表是否存在主键,有主键的情况,如下所示:

        mysql> show index from t;
        +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
        | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
        +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
        | t     |          0 | PRIMARY  |            1 | id          | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
        +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
        1 row in set (0.00 sec)
      

      我们再来看一个没有主键的例子,如下所示:

        mysql> show index from t;
        +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
        | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
        +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
        | t     |          0 | id       |            1 | id          | A         |           0 |     NULL | NULL   |      | BTREE      |         |               |
        | t     |          1 | idx_age  |            1 | age         | A         |           0 |     NULL | NULL   | YES  | BTREE      |         |               |
        +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
        2 rows in set (0.00 sec)
      

      在这个没有主键的例子中,虽然没有显式的定义主键,但是它有一个非空的唯一索引。在InnoDB中,如果存在非空的唯一约束,那么,这一列将会被当作主键。综合前面两种情况的输出,我们知道,我们要判断一张表是否存在主键,我们不能通过是否存在一个key_name名为PRIMARY的索引来判断,而应该通过Non_unique为0和Null列不为YES来判断。

      说完了需求,我们来看一下具体的实现。使用pymysql连接数据库,数据库中的每一行,将会以元组的形式返回,如下所示:

        (('t', 0, 'PRIMARY', 1, 'id', 'A', 0, None, None, '', 'BTREE', '', ''),)
      

      也就是说,我们现在要遍历一个二维的元组,然后判断是否存在Non_unique为0,Null列不为YES的记录。详细了解了具体实现以后,我们写下了下面的代码:

        def has_primary_key():
            for row in rows:
                if row[1] == 0 and row[9] != 'YES':
                    return True
            return False
      

      非常的简单,但是,如果我们使用any函数的话,代码将会更短。如下所示:

        def has_primary_key():
            return any(row[1] == 0 and row[9] != 'YES' for row in rows):
      

3 利用动态语言特性写出简洁优美的代码

在上一节上, 我们介绍了一些利用Python语言特性来编写简洁优美代码的例子。在这一节中,我们来看一个利用Python语言高级特性的例子。Python作为一门动态类型语言,与C、C++、Java等静态类型语言有着显著的区别。在这一节中,我们介绍其中一个高级特性,这个特性充分的演示了动态类型语言与静态类型语言的差异。更多的Python语言高级特性,我们将在随后的章节中介绍。

在这个例子中,我们会收到很多不同的请求,对于不同的请求,调用不同的请求处理函数,这个需求如此常见,相信大家应该见过这样的代码:

if cmd == 'A':
    process_a()
elif cmd == 'B':
     process_b()
elif cmd == 'C':
     process_c()
elif cmd == 'd':
     process_d()
else:
    raise NotImplementException

对于接收消息并调用不同的处理函数来说,上面的代码是很直观、也很容易想到的。但是,这段代码存在一个比较严重的问题。随着消息增加,if语句不断壮大,这段程序最后将变得无法维护。

很多优秀的开源项目也会有类似的需求,但是,它们会使用其他方法解决这个问题。例如,在redis系统中,会使用字典保存命令到执行函数之间的映射关系。当接收到一条新的命令时,先判断该命令是否存在于字典中,如果不存在,则说明不支持该命令。如果存在,则通过命令名称从字典中获取函数的指针,并通过函数指针的方式去调用相应的处理函数。如下所示:

struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},

我们也可以参照redis的实现方式,使用字典保存命令与命令处理函数之间的映射关系。但是,在Python语言中,还可以做的更好。

Python语言是一门动态类型语言,具有自省(introspection)的能力。所谓自省,就是可以编写程序来处理其他已有的程序。例如,对于这里的需求,可以先根据请求的名称,判断相应的处理模块是否具有对应的处理函数。如果有,则获取相应的处理函数,然后进行调用。这么说或许比较抽象,我们来看一个具体的例子。

假设现在有一个名为Person的类,该类只有一个name属性。此外,该类有一个名为get_first_name方法和一个名为get_last_name的方法。如下所示:

class Person(object):
    def __init__(self, name):
        self.name = name

    def get_first_name(self):
        return self.name.split()[0]

    def get_last_name(self):
        return self.name.split()[-1]

为了进行更好的说明,我们在IPython中进行功能演示。在IPython中导入Person这个类,并创建一个对象。如下所示:

In [1]: from person import Person

In [2]: jason = Person('Jason Statham')

Python是一门动态类型的语言,我们可以在程序中处理已有的程序。例如,我们可以通过Python内置的dir函数,查看jason这个对象的所有属性。如下所示:

In [3]: dir(jason)
Out[3]:
['__class__',
 '__dict__',
 '__doc__',
 'get_first_name',
 'get_last_name',
 'name']

我们已经看到了jason这个对象拥有的属性,并且,Python内置的dir函数会返回这些属性的列表。现在,我们想测试某个属性是否存在,如果对象存在这个属性,则获取这个属性。这个任务可以由Python内置的hasattr和getattr函数来完成。如下所示:

In [4]: hasattr(jason, 'get_first_name')
Out[4]: True

In [5]: action = getattr(jason, 'get_first_name')

In [6]: action()
Out[6]: 'Jason'

In [7]: action = getattr(jason, 'get_last_name')

In [8]: action()
Out[8]: 'Statham'

在这段程序中,我们首先通过hasattr函数判断对象是否具有某个属性,然后,我们通过getattr获取该属性。由于我们获取到的属性是一个方法,因此,我们可以直接进行调用。可以看到,无论我们想要获取get_first_name还是get_last_name,都可以通过getattr获取到属性,然后进行调用。

上面这段程序对我们有什么启发呢?假设我们有一个类,这个类中对每一条请求都有对应的处理函数。为了简单起见,假设请求的名称和处理函数同名。那么,我们可以通过hasattr函数判断客户端是否拥有相应的属性,如果没有,则说明客户端不支持该请求。如果有,我们可以通过getattr获得该请求的处理函数,然后调用该函数进行处理。

在这个例子中,我们只应用了非常简单的Python自省能力。包括通过hasattr方法判断对象是否具有相应的属性,通过getattr获取对象的属性。使用Python的自省,不用像其他编程语言一样,使用一堆的if/else语句来处理不同的消息。也不用像redis一样,专门维护一个字典来保存每个消息及其对应的处理函数。因此,提高了程序的可扩展性。

4 像写报纸一样写代码

关于如何能够写出Pythonic的代码,我的观点是:不管用什么语言,你都应该努力写出简洁优美的代码。如果不能,那我推荐你看看《重构》和《代码整洁之道》。虽然这两本书使用的是Java语言,但是,并不影响作者要传递的思想。此外,我也有一些经验传授给大家,希望能够帮助新同学快速地写出还不错的代码。

* 像写报纸一样写代码
    - 准确无歧义
    - 完整无废话
    - 注意排版以引导读者
    - 注意标点符号以帮助读者
* 保证可读性的前提下,代码尽可能短小

我们来看一个"注意标点符号以帮助读者"的例子。在Python里面,空行是会被忽略的,也就说,有没有空行,有多少空行,对Python解释器来说都是一样的。但是,有没有空行对程序员来说可就不一样了,会显著影响程序的可读性。

下面来看一个例子。随机生成1000个0~999之间的整数,然后求他们的和。这里是没有空行的例子:

import random
def sum_num(num, min_num, max_num):
    i = 0
    data = []
    for i in range(num):
        data.append(random.randint(min_num, max_num))
    total = 0
    for item in data:
        total += item
    return total
if __name__ == '__main__':
    print sum_num(1000, 0, 1000)

这里是有空行的例子:

import random

def sum_num(num, min_num, max_num):
    i = 0
    data = []
    for i in range(num):
        data.append(random.randint(min_num, max_num))

    total = 0
    for item in data:
        total += item

    return total


if __name__ == '__main__':
    print sum_num(1000, 0, 1000)

读者可以用心感受一下这两段代码,代码一模一样,唯一的区别是第二段代码多了几个空行。有空行的代码,明显看起来更加舒适愉悦。在用汉语写作文的时候,老师一直教导我们要合理的使用标点符号,合理的分段。其实,写代码和写文章是一样的。写代码时,大家可以这样想象:换行是逗号,空一行是句号,空两行是分段。至于逗号,由于我们总是在一行中写一条语句,所以,逗号是可以忽略的,如果你在一行中写了多条语句,就好比在写作文的时候没有正确的使用逗号,也让人难以理解。如果你从来不空行,所有代码纠缠在一起,就好比没有句号,让人读起来很累,同理,不分段也不是好习惯。

5 总结

所谓的Pythonic,其实并没有大家想的那么神秘,最终目的都是写出简洁优美的代码。写出简洁优美代码的思想在各个语言中都是一样的。如果你用其他编程语言写不出简洁优美的代码,那么,你也没办法用Python写出简介优美的代码。如果你能用其他语言写出很好的代码,那么,还是需要了解Python这门语言特有的一些语法和语言特性,充分利用Python里面比较好语言特性。这样,就能够写出Pythonic的代码了。

关于Pythonic、关于Python语言的高级特性、关于编程中容易犯的错误,都无法在一篇文章中全部介绍。如果读者在看完这篇文章以后,还有一种意犹未尽的感觉,那么,就跟随我的角度,一起来学习《Python高质量编程》吧!

作者介绍

赖明星,架构师、作家。现就职于腾讯,参与并主导下一代金融级数据库平台研发。有多年的 Python 开发经验和一线互联网实战经验,擅长 C、Python、Java、MySQL、Linux 等主流技术。国内知名的 Python 技术专家和 Python 技术的积极推广者,著有《Python Linux 系统管理与自动化运维》一书。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值