编写高效且优雅的 PYTHON 代码

转载 2016年11月21日 16:58:41

Python 作为一门入门极易并容易上瘾的语音,相信已经成为了很多人 “写着玩” 的标配脚本语言。但很多教材并没有教授 Python 的进阶和优化。本文作为进阶系列的文章,从基础的语法到函数、迭代器、类,还有之后系列的线程 / 进程、第三方库、网络编程等内容,共同学习如何写出更加 Pythonic 的代码部分提炼自书籍:《Effective Python》&《Python3 Cookbook》,但也做出了修改,并加上了我自己的理解和运用中的最佳实践

Pythonic

列表切割

list[start:end:step]

  • 如果从列表开头开始切割,那么忽略 start 位的 0,例如list[:4]
  • 如果一直切到列表尾部,则忽略 end 位的 0,例如list[3:]
  • 切割列表时,即便 start 或者 end 索引跨界也不会有问题
  • 列表切片不会改变原列表。索引都留空时,会生成一份原列表的拷贝

 

 

列表推导式

  • 使用列表推导式来取代mapfilter

 

 

  • 不要使用含有两个以上表达式的列表推导式

 

 

  • 数据多时,列表推导式可能会消耗大量内存,此时建议使用生成器表达式

 

 

迭代

  • 需要获取 index 时使用enumerate
  • enumerate可以接受第二个参数,作为迭代时加在index上的数值

 

 

  • zip同时遍历两个迭代器

 

 

  • zip遍历时返回一个元组

 

 

  • 关于forwhile循环后的else
    • 循环正常结束之后会调用else内的代码
    • 循环里通过break跳出循环,则不会执行else
    • 要遍历的序列为空时,立即执行else

 

 

反向迭代

对于普通的序列(列表),我们可以通过内置的reversed()函数进行反向迭代:

除此以外,还可以通过实现类里的__reversed__方法,将类进行反向迭代:

 

try/except/else/finally

  • 如果try内没有发生异常,则调用else内的代码
  • else会在finally之前运行
  • 最终一定会执行finally,可以在其中进行清理工作

函数

使用装饰器

装饰器用于在不改变原函数代码的情况下修改已存在的函数。常见场景是增加一句调试,或者为已有的函数增加log监控

举个栗子:

除此以外,还可以编写接收参数的装饰器,其实就是在原本的装饰器上的外层又嵌套了一个函数:

但是像上面那样使用装饰器的话有一个问题:

也就是说原函数已经被装饰器里的new_fun函数替代掉了。调用经过装饰的函数,相当于调用一个新函数。查看原函数的参数、注释、甚至函数名的时候,只能看到装饰器的相关信息。为了解决这个问题,我们可以使用 Python 自带的functools.wraps方法。

stackoverflow: What does functools.wraps do?

functools.wraps是个很 hack 的方法,它本事作为一个装饰器,做用在装饰器内部将要返回的函数上。也就是说,它是装饰器的装饰器,并且以原函数为参数,作用是保留原函数的各种信息,使得我们之后查看被装饰了的原函数的信息时,可以保持跟原函数一模一样。

此外,有时候我们的装饰器里可能会干不止一个事情,此时应该把事件作为额外的函数分离出去。但是又因为它可能仅仅和该装饰器有关,所以此时可以构造一个装饰器类。原理很简单,主要就是编写类里的__call__方法,使类能够像函数一样的调用。

 

使用生成器

考虑使用生成器来改写直接返回列表的函数

 

用这种方法有几个小问题:

  • 每次获取到符合条件的结果,都要调用append方法。但实际上我们的关注点根本不在这个方法,它只是我们达成目的的手段,实际上只需要index就好了
  • 返回的result可以继续优化
  • 数据都存在result里面,如果数据量很大的话,会比较占用内存

因此,使用生成器generator会更好。生成器是使用yield表达式的函数,调用生成器时,它不会真的执行,而是返回一个迭代器,每次在迭代器上调用内置的next函数时,迭代器会把生成器推进到下一个yield表达式:

获取到一个生成器以后,可以正常的遍历它:

如果你还是需要一个列表,那么可以将函数的调用结果作为参数,再调用list方法

 

可迭代对象

需要注意的是,普通的迭代器只能迭代一轮,一轮之后重复调用是无效的。解决这种问题的方法是,你可以定义一个可迭代的容器类

这样的话,将类的实例迭代重复多少次都没问题:

但要注意的是,仅仅是实现__iter__方法的迭代器,只能通过for循环来迭代;想要通过next方法迭代的话则需要使用iter方法:

 

使用位置参数

有时候,方法接收的参数数目可能不一定,比如定义一个求和的方法,至少要接收两个参数:

对于这种接收参数数目不一定,而且不在乎参数传入顺序的函数,则应该利用位置参数*args

但要注意的是,不定长度的参数args在传递给函数时,需要先转换成元组tuple。这意味着,如果你将一个生成器作为参数带入到函数中,生成器将会先遍历一遍,转换为元组。这可能会消耗大量内存:

 

使用关键字参数

  • 关键字参数可提高代码可读性
  • 可以通过关键字参数给函数提供默认值
  • 便于扩充函数参数

定义只能使用关键字参数的函数

  • 普通的方式,在调用时不会强制要求使用关键字参数

 

 

  • 使用 Python3 中强制关键字参数的方式

 

 

  • 使用 Python2 中强制关键字参数的方式

 

 

关于参数的默认值

算是老生常谈了:函数的默认值只会在程序加载模块并读取到该函数的定义时设置一次

也就是说,如果给某参数赋予动态的值( 比如[]或者{}),则如果之后在调用函数的时候给参数赋予了其他参数,则以后再调用这个函数的时候,之前定义的默认值将会改变,成为上一次调用时赋予的值:

因此,更推荐使用None作为默认参数,在函数内进行判断之后赋值:

 

__slots__

默认情况下,Python 用一个字典来保存一个对象的实例属性。这使得我们可以在运行的时候动态的给类的实例添加新的属性:

然而这个字典浪费了多余的空间 — 很多时候我们不会创建那么多的属性。因此通过__slots__可以告诉 Python 不要使用字典而是固定集合来分配空间。

 

__call__

通过定义类中的__call__方法,可以使该类的实例能够像普通函数一样调用。