Python Cookbook

Table of content:

About

这篇文章会是在 Python 学习和使用过程中的一些思考和认识, 还在持续完善中。

todos:

  • Cython 解释器和 PyPy 解释器
  • 垃圾回收
  • pyc 生命周期

数据结构

String

原理和思考

虚拟机

GIL (Global Interpreter Lock)

对于 Python,充分利用多核性能的阻碍主要是 Python 的全局解释锁 GIL。GIL 限制一次只允许使用一个线程执行 Python 字节码,因此一个 Python 进程通常不能使用多个 CPU 核心。

标准库中,所有执行阻塞型 I/O 操作的函数,在等待操作系统返回时都会释放 GIL,允许其他线程运行, time.sleep() 也会释放 GIL。所以,尽管有 GIL, Python 线程还是能在 I/O 密集型的应用中发挥作用。

不过这些问题可以通过 mutiprocessing 多进程, Cython,分布式计算模型来解决。

GIL 解决的是什么问题

Python uses reference counting for memory management. It means that objects created in Python have a reference count variable that keeps track of the number of references that point to the object. When this count reaches zero, the memory occupied by the object is released.

The problem was that this reference count variable needed protection from race conditions where two threads increase or decrease its value simultaneously. If this happens, it can cause either leaked memory that is never released or, even worse, incorrectly release the memory while a reference to that object still exists.

This reference count variable can be kept safe by adding locks to all data structures that are shared across threads so that they are not modified inconsistently.

总结起来是说 Python 使用引用数来做垃圾回收,在多线程竞态的条件下,需要加锁来保证一次只有一个线程来修改这个数量。

Reference:

GIL 释放逻辑

在 Python2.x 里,GIL 的释放逻辑是当前线程遇见 IO 操作或者 ticks 计数达到 100(ticks 可以看作是 Python 自身的一个计数器,专门做用于 GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。

而每次释放 GIL 锁,线程进行锁竞争、切换线程,会消耗资源。并且由于 GIL 锁存在,Python 里一个进程永远只能同时执行一个线程 (拿到 GIL 的线程才能执行),这就是为什么在多核 CPU 上,python 的多线程效率并不高。

是不是 Python 的多线程就完全没用了

1、CPU 密集型代码 (各种循环处理、计数等等),在这种情况下,ticks 计数很快就会达到阈值,然后触发 GIL 的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以 Python 下的多线程对 CPU 密集型代码并不友好。
2、IO 密集型代码 (文件处理、网络爬虫等),多线程能够有效提升效率 (单线程下有 IO 操作会进行 IO 等待,造成不必要的时间浪费,而开启多线程能在线程 A 等待时,自动切换到线程 B,可以不浪费 CPU 的资源,从而能提升程序执行效率)。所以 Python 的多线程对 IO 密集型代码比较友好。

Reference:

垃圾回收

Cython

Ref:

PyPy

http://pypy.org/

在「流畅的 Python」里面提到如果使用 Python 做一些 CPU 密集的工作,应该试试 PyPy

Python 容器

使用场景

文件操作

常见问题

如何对各种写法做性能对比

有一些办法,比如,

  1. 实现一个上下文管理器
1
2
3
4
5
6
7
8
9
10
11
12
class Timer(object):
def __init__(self, verbose=False):
self.verbose = verbose
def __enter__(self):
self.start = clock()
return self
def __exit__(self, *args):
self.end = clock()
self.secs = self.end - self.start
self.msecs = self.secs * 1000 # millisecs
if self.verbose:
print 'elapsed time: %f ms' % self.msecs
  1. 使用一些可视化工具在, 如 https://github.com/nvdv/vprof

  2. 如果要知道函数里面每一行代码的执行效率,可以用 line_profiler, https://github.com/rkern/line_profiler

Reference:

可迭代对象,迭代器生成器

  1. 迭代器协议
  • 迭代器协议是指:对象需要提供 next 方法,它要么返回迭代中的下一项,要么就引起一个 StopIteration 异常,以终止迭代
  • 可迭代对象就是:实现了迭代器协议的对象
  • 协议是一种约定,可迭代对象实现迭代器协议,Python 的内置工具 (如 for 循环,sum,min,max 函数等) 使用迭代器协议访问对象。
  1. 迭代器
    比如在 Python 中,for 循环可以遍历数组也可以遍历文件。文件对象实现了迭代协议,for 循环并不知道它遍历的是一个文件对象,它只管使用迭代器协议访问对象即可。

迭代器就是用于迭代操作(for 循环)的对象,它像列表一样可以迭代获取其中的每一个元素,任何实现了 __next__ 方法 (python2 是 next)的对象都可以称为迭代器。

  1. 生成器
    Python 是应用生成器对延迟操作进行了支持。所谓延迟操作,就是在需要的时候才生产结果,而不是立即产生结果。

普通函数用 return 返回一个值,然而在 Python 中还有一种函数,用关键字 yield 来返回值,这种函数叫生成器函数,函数被调用时会返回一个生成器对象,生成器本质上还是一个迭代器。

Python 有两种不同的方式提供生成器:

  • 生成器函数:常规函数定义,但是,使用 yield 语句而不是 return 语句返回结果。yield 语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def fib(n):
    prev, curr = 0, 1
    while n > 0:
    n -= 1
    yield curr
    prev, curr = curr, curr + prev

    print([i for i in fib(10)])
    #[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
  • 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表

1
2
3
4
5
6
>>> g = (x*2 for x in range(10))
>>> type(g)
<type 'generator'>
>>> l = [x*2 for x in range(10)]
>>> type(l)
<type 'list'>

生成器的唯一注意事项就是:生成器只能遍历一次。

Reference:

pyc 的生成策略和生命周期

Python 解释器在执行任何一个 Python 程序文件时,首先进行的动作是对文件中的 Python 源代码进行编译,产生一组 Python byte code 字节码,然后将编译结果交给 Python 虚拟机,由虚拟机按照顺序一条条执行字节码

对 Python 编译器来说活,PyCodeObject 对象才是真正的编译结果,而 pyc 文件只是这个对象在硬盘上的表现形式,是 Python 对源文件进行编译结果的两种不同的存在方式。
在程序运行期间,编译结果存在内存的 PyCodeObject 对象中,而运行结束之后,编译结果被保存到了 pyc 文件中,下次运行相同的程序时, Python 会根据 pyc 文件中记录的编译结果直接建立内存中的 PyCodeObject 对象,而不用对源文件进行编译了。

Things:

  • A program doesn’t run any faster when it is read from a ‘.pyc’ or ‘.pyo’ file than when it is read from a ‘.py’ file; the only thing that’s faster about ‘.pyc’ or ‘.pyo’files is the speed with which they are loaded. Python 这样保存字节码是作为一种启动速度的优化。下一次运行程序时,如果你在上次保存字节码之后没有修改过源代码的话,Python 将会加载. pyc 文件并跳过编译这个步骤。当 Python 必须重编译时,它会自动检查源文件和字节码文件的时间戳:如果你又保存了源代码,下次程序运行时,字节码将自动重新创建。
  • When a script is run by giving its name on the command line, the bytecode for the script is never written to a ‘.pyc’ or ‘.pyo’ file. Thus, the startup time of a script may be reduced by moving most of its code to a module and having a small bootstrap script that imports that module. It is also possible to name a ‘.pyc’ or ‘.pyo’file directly on the command line.

Reference:

  • 书籍「Python 源码剖析」

Python 2 和 3 之间的差别

  • print, 在 Python 2 中,print 是一条语句,而 Python3 中作为函数存在
  • 默认编码, Python2 的默认编码是 asscii,这也是导致 Python2 中经常遇到编码问题的原因之一。Python 3 默认采用了 UTF-8 作为默认编码,因此你不再需要在文件顶部写 # coding=utf-8 了
    1
    2
    3
    4
    5
    6
    7
    # py2
    >>> sys.getdefaultencoding()
    'ascii'

    # py3
    >>> sys.getdefaultencoding()
    'utf-8'
  • 字符串是最大的变化之一,这个变化使得编码问题降到了最低可能。在 Python2 中,字符串有两个类型,一个是 unicode,一个是 str,前者表示文本字符串,后者表示字节序列,不过两者并没有明显的界限,开发者也感觉很混乱,不明白编码错误的原因,不过在 Python3 中两者做了严格区分,分别用 str 表示字符串,byte 表示字节序列,任何需要写入文本或者网络传输的数据都只接收字节序列,这就从源头上阻止了编码错误的问题。
  • nonlocal, Python2 中可以在函数里面可以用关键字 global 声明某个变量为全局变量,但是在嵌套函数中,想要给一个变量声明为非局部变量是没法实现的,在 Pyhon3,新增了关键字 nonlcoal,使得非局部变量成为可能。
1
2
3
4
5
6
7
def func():
c = 1
def foo():
c = 12
foo()
print(c)
func() #1
1
2
3
4
5
6
7
8
9
def func():
c = 1
def foo():
nonlocal c
c = 12
foo()
print(c)
func() #12

Ref:

encode and decode

These are the five unavoidable Facts of Life:

  1. All input and output of your program is bytes.
  2. The world needs more than 256 symbols to communicate text.
  3. Your program has to deal with both bytes and Unicode.
  4. A stream of bytes can’t tell you its encoding.
  5. Encoding specifications can be wrong.

Pro Tips to keep in mind as you build your software to keep your code Unicode-clean:

  1. Unicode sandwich: keep all text in your program as Unicode, and convert as close to the edges as possible.
  2. Know what your strings are: you should be able to explain which of your strings are Unicode, which are bytes, and for your byte strings, what encoding they use.
  3. Test your Unicode support. Use exotic strings throughout your test suites to be sure you’re covering all the cases.

Reference:

Python 如何找包

Metaclass

协程

monkey.patch_all() 在做的是什么事情

热加载

todo

内存管理

todo

描述符

concurrent.futures

在 CPU 密集型的任务中使用 concurrent.futures 来绕过 GIL。使用 ProcessPoolExecutor 把工作分配给多个 Python 进程处理。这样充分利用多核。

Ref:

asyncio

Ref:

staticmethod, classmethod

Tricks (fun coding)

run module as script

json.tools
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ curl -sL http://j.mp/1IuxaLD
[{"x":1,"y":2},{"x":3,"y":4},{"x":5,"y":6}]
$ curl -sL http://j.mp/1IuxaLD | Python -m json.tool
[
{
"x": 1,
"y": 2
},
{
"x": 3,
"y": 4
},
{
"x": 5,
"y": 6
}
]

Referece:

推荐阅读

  • Armin Ronacher 的博客 有很多代码的经验和技巧分享, 他写的一些库如 flask, werkzeug 可读性都很
  • kennethreitz 写的一系列 Python lib for human, 如 requests, tablib 等
  • mitsuhiko。flask、Jinja2、werkzeug 和 flask-sqlalchemy 作者
  • gunicorn 的作者 benoitc 写的 Python 代码基本都比较 pythonic https://github.com/benoitc
  • 不超过 500 行代码的各种项目 (以 Python 为主,不全是 Python) GitHub - aosabook/500lines: 500 Lines or Less

Reference

关于头图

拍摄自费城动物园

香港 RYA Competent Crew 课程
邓小平时代