我的 Python Cookbook

Table of Contents

1 开发工具

  • IPython

编辑器和IDE推荐:

  • Emacs
  • Vim
  • PyCharm
  • WingIde
  • Visual Studio

这里有份详细的对比:http://stackoverflow.com/questions/81584/what-ide-to-use-for-python

2 字符串

2.1 字符串对齐

让字符串“The end”左右边都将有20个“+”:

n [11]: print('The end'.center(20,'+'))
++++++The end+++++++

用直接输出字符串的方法很麻烦,也影响代码的外观。使用 center 方法比较容易一些。

2.2 字符串连接技巧

如下两个变量,hello 和 world:

hello = 'hello '
world = 'world'

如果将它们连接在一起,可以使用最先考虑到的字符串连接方式:

hello + world

但是,用”+“和的性能比较低下,用%s可以更加方便地处理好字符串的连接:

strs = '%s%s' % (hello, world)

这样做的好处:

1、可以很好控制浮点数的位数;

2、不要再对数字型用 str 转换了,因为 %s 已经自动完成该功能了

2.3 生成等宽数字

比如生成 01 02 03 这样格式的字符串:

for i in xrange(11):
    print('%02d' % i)

或者:

for i in xrange(11):
    print('{0:02d}'.format(i))

2.4 字符串转 raw_string

Python 里没有现成的函数可以把字符串转成 raw string,但我们经常遇到这种问题。可以用两种方法搞定:

1、repr

a = '\\test'
a = repr(a).[1:-1]

2、eval

a = '\\test'
a = eval('"%s"'%a)

eval 的太麻烦,而且有安全隐患,推荐第一种方法。

3 循环

3.1 如何在循环中重新迭代

例,有一个 URL 列表,需要对列表中每个 URL 都进行一次请求,若是请求失败则重试。

方法1,模拟“队列”来实现,每次从队列中取出一个 URL,若是请求失败就将 URL 重新放回队列:

urls = ["http://1.com", "http://2.com", "http://3.com"]

while urls:
    url = urls.pop()

    try:
        do_something(url)
    except Exception:
        urls.append(url)
        continue

方法2,迭代中嵌套一个死循环,反复重试到成功为止:

urls = ["http://1.com", "http://2.com", "http://3.com"]

for url in urls:
    while True:
        try:
            do_something(url)
            break
        except Exception:
            # 出错后反复重试
            continue

3.2 try..except,发生异常后重试

下面调用 login 方法,login 方法内部以发送 HTTP 请求来获得 token:

self.token = self.login(self.username, self.password)

现在希望请求如果遇到超时,可以自动重试。实现方法和上面的类似:

while True:
    try:
        self.token = self.login(self.username, self.password)
    except requests.exceptions.ConnectionError:
        time.sleep(3)
        continue
    # 如果成功后,就跳出循环
    break

4 字典

4.1 key 和 value 逆转

利用字典解析将 {"a": 1, "b": 2, "c":3} 转换成 {1: "a", 2: "b", 3: "c"}:

>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> {k:v for k,v in d.items()}
{'a': 1, 'c': 3, 'b': 2}

Dict 的 items 方法返回一个列表,列表中每个元素都是一个包含 key 和 value 的元组:

>>> d.items()
[('a', 1), ('c', 3), ('b', 2)

4.2 按 key 排序

内置的 sorted 函数可以为 key 参数指定一个 lambda 表达式,告诉函数应该如何去做比较判断,由于 dict 对象的 items 函数可以返回一个列表,每一个元素都是 key 和 value 组成的元组,只用取元组第一个元素(key)做比较即可:

d = {"1": 'a', '2': 'b', '4': 'c', '3': 'd'}
dict(sorted(d.items(), key=lambda i:i[0]))  # => {'1': 'a', '2': 'b', '3': 'd', '4': 'c'}

4.3 defaultdict

根据官方文档记载,defaultdict 继承自 dict,并且和 dict 有一样的方法。defaultdict 是由 C 实现的,具体代码见https://github.com/python/cpython/blob/master/Modules/_collectionsmodule.c

defaultdict 在初始化时提供一个可调用对象,当引用的 key 不存在时,defaultdict 对象会返回调用的默认结果:

from collections import defaultdict

d = defaultdict(list)
d['a']                          # 由于键“a”不存在,新建一个键 a,对应的值是空列表
d['b'].append(1)
d                               # defaultdict(list, {'a': [], 'b': [1]})

初始化时也可以用 lambda 表达式:

d = defaultdict(lambda: "hello")
d['a']                          # => 'hello'
d                               # => defaultdict(<function __main__.<lambda>>, {'a': 'hello'})

4.4 合并字典并累加相同 key 的值

from collections import Counter

a = {"a": 1, "b": 2, "c": 3}
b = {"c": 3, "b": 1, "e": 100}

merged = Counter(a) + Counter(b)

print(dict(merged))             # => {'a': 1, 'b': 3, 'c': 6, 'e': 10}

5 列表

5.1 列表解析

Python 既支持简单的表达式,也有高级的表达式。Python 列表解析可以根据列表中的表达式计算结果来创建一个新的列表。例如,我需要建立一个新的列表,列表元素包含了 1~100 之间的所有偶数,C 语言中我们可以用这样的方法完成查找偶数:

#include <stdio.h>

int main() {
    int num = 100;
    int i = 0;

    for ( ; i <= 100; i++) {
        if (i%2 == 0)
        printf("%d\n", i);
    }
    return 0;
}

同样,以这样的思维在 Python 中也可以这样写:

for i in range(101):
    if i%2 == 0:
        print(i)

显然 Python 中要敲打的代码数量比 C 语言代码少多了,但是我们利用列表解析还可以更加省略,甚至只需要一行代码就完成了计算功能:

>>> l = [x for x in range(101) if x%2 == 0]                                         (1)
>>> l
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100]

注意(1)那行代码,l 是定义的一个新的列表,在方括号中,首先是一个循环,range(101) 会将 1~100 的数连续存储在l列表中,每一次循环,x 都会赋上列表当前的索引值,然后再将这个值与 2 做取模运算,如果结果为 0,则证明是一个偶数,接着将 x 赋值给方括号中开头的 x。整个顺序我通过标号标记在表达式中:

l = [x for x in range(101) if x%2 == 0]

同样,稍加改造,可以计算 1~100 间所有奇数:

l = [x for x in range(101) if x%2 == 1]

5.2 清空列表

假设有如下列表:

l = range(10)

清空l这个列表有三种方法:

  • 第一种方法:del l[:],这种方法最彻底,将从内存中删除。
  • 第二种方法:l = [],这种方法仅仅是把 l 指向到了一个新的、空的队列对象中。
  • 第三种方法:l[:] = [],这种本人觉得应该也是和第二种方法差不多。

为了节约内存的话,用第一种方法其实是不错的。

5.3 复制列表

将列表赋值给变量其实是将列表的引用(即内存地址)赋给了变量,如:

tmp = urls

就是将该列表引用的地址赋值给了 tmp 变量,所以无论如何改变 urls,都会影响到 tmp 的变化。如果要想 tmp 复制一份不会随着 urls 变化而变化的列表,需要重新创建一个列表对象:

tmp = urls[:]

5.4 遍历列表时带上下标

for i, value in enumerate(["a", "b", "c"]):
    print(i, value)

5.5 平坦(flatten)嵌套列表

如,列表:

[[1, 2], [3, 4], [5]]

平坦为:

[1, 2, 3, 4, 5]

实现如下:

def flatten_list(lst):
    """
    >>> flatten_list([[1, 2], [3, 4], [5]])
    [1, 2, 3, 4, 5]
    """
    def do_flatten():
        for item in lst:
            if hasattr(item, "__iter__") and isinstance(item, list):
                for sub_item in item:
                    yield sub_item
            else:
                yield item

    return list(do_flatten())

5.6 统计列表各项重复次数

from collections import Counter

col = Counter(['a','b','c','a','b','b'])  # => Counter({'b': 3, 'a': 2, 'c': 1})
col["xxx"]                                # => 0,如果访问不存在的值,Counter 不会引发 KeyError 异常,而是返回 0

自己实现版:

from collections import defaultdict

count = defaultdict(int)

for i in [1, 2, 3, 4, 5, 5, 3, 1]:
    count[i] += 1

print(count) # => defaultdict(<type 'int'>, {1: 2, 2: 1, 3: 2, 4: 1, 5: 2})

6 元组

通过括号构造只有一个元素的元组:

(1,)

如果在实际编码过程中不小心遗忘最后的逗号(很有可能发生的),那最终的数据类型可就超出预期了:

type((1))                       # => int
type(('a'))                     # => str

比如我在用 Python sqlite3 库时,执行以下错误的代码:

cursor.execute("select * from tasks where task_name=?", (task_name))

报错,提示 SQL 语句只需绑定一个变量,但提供了 3 个,原因是本该给 execute 方法提供的是个元组类型,但由于少打了逗号,导致 task_name 成了一个长度为 3 的字符串:

sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 3 supplied.

7 生成器

7.1 生成器表达式

In [9]: l = range(10)

In [10]: it = (i for i in l if i%2 == 0)

In [11]: list(it)
Out[11]: [0, 2, 4, 6, 8]

生成器表达式(Generator Expressions)与列表解析不同,语法上是括号包围的,产生的结果是一个可迭代对象。

Generator expressions 永远写到括号里面的,还可以:

In [15]: l
Out[15]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [16]: sum(i for i in l if i%2 == 0)
Out[16]: 20

7.2 传递值给生成器

《Functional Programming HOWTO》里“Passing values into a generator”一小节代码如下:

def counter (maximum):
    i = 0
        while i < maximum:
            val = (yield i)
            if val is not None:
                i = val
            else:
                i += 1

可以这样调用:

In [48]: it = counter(10)

In [49]: it.next()
Out[49]: 0

In [50]: it.next()
Out[50]: 1

In [51]: it.next()
Out[51]: 2

In [52]: it.next()
Out[52]: 3

In [53]: it.send(8)
Out[53]: 8

In [54]: it.next()
Out[54]: 9

这里可以把 yield 放到表达式后面,调用者可以调用 send 方法传递一个值进去,如果没有传递的话,val 默认是 None

7.3 无限迭代

itertools 模块里的 repeat 提供了一个迭代对象,每次都返回相同值:

for i in itertools.repeat(1):
    print(i)                    # 无限打印“1”

8 序列解构

>>> l = range(3)
>>> a,b,c = l
>>> a,b,c
(0, 1, 2)
>>> it = iter(l)
>>> a,b,c = it
>>> a,b,c
(0, 1, 2)
>>>

可迭代对象也可以把元素拆给变量。必须保证元素个数和变量数一致才可以。

8.1 什么时候用“_”来忽略值

例,top-1m.csv 是一个记录了 Aleax 排名的文件,内容如下:

1,youtube.com
2,google.com
3,facebook.com
4,baidu.com
5,wikipedia.org
6,reddit.com
7,yahoo.com
8,google.co.in
9,qq.com
10,taobao.com
...

如果我不关心第一列数据,就能用到下划线:

with open("top-1m.csv") as f:
    for line in f:
        _, domain = line.strip().split(",")

        if domain in data:
            print(domain)

这样可以不为第一列分配一个不会使用的变量。

9 内置变量

9.1 __debug__

是一个内置的常量,默认的值是 True,如果为 False,将去除代码中的 assert 语句。

但是不可以直接为它赋值为 False,因为它是常量。只有用 python -O 执行的时候,它才为 False

10 操作符

10.1 对象之间比较

(1,2,3) < (4,5,6)               # True
(1,2,7) < (4,5,6)               # True
(4,2,7) < (4,5,6)               # True

元组比较首先从序列第一个开始找,如果双方相等,就比较下一个元素,直到找到不等元素后,停止比较。也可用于列表等内置类型的比较。

11

11.1 匿名类

见:https://docs.python.org/3/library/functions.html#type

type(name, bases, dict)

当给type函数提供3个参数时,就可以创建匿名类。

c = type("XX", (object, ), {"__init__": lambda: None, "a": 1})
c.a # => 1

12 数据对象可修改问题

一定要注意哪些情况下修改传递的对象会影响到原始数据,否则很容易导致程序 bug。

第一个例子,试图将数据封装到类中保护起来:

class MemberInfo(object):
    def __init__(self, username, blog):
        self.data = {
            "name": username,
            "blog": blog
        }

    def get_raw_data(self):
        return self.data

    def print_info(self):
        print(self.data)


info = MemberInfo("lu4nx", "www.shellcodes.org")
raw_data = info.get_raw_data()
raw_data["name"] = "new"

info.print_info()

但是执行 info.print_info() 后,却输出:

{'name': 'new', 'blog': 'www.shellcodes.org'}

执行结果不复合我们的预期,被保护起来的数据最终被修改了。因为 get_raw_data 函数返回的数据并不是拷贝的新对象,而是指向的原始数据,对它的操作会直接影响到原始数据,所以返回时应该复制一份新的数据:

def get_raw_data(self):
    return self.data.copy()

同样的问题也发生在函数的参数传递中,示例代码:

def test_append(info_list):
    new_list = info_list.append("test")
    return new_list


lst = ["lu4nx", "www.shellcodes.org"]
new_lst = test_append(lst)

print(lst)                      # => ['lu4nx', 'www.shellcodes.org', 'test']

test_append 函数是直接对传递的 list 对象进行 append,最终修改到了原始的 lst 变量,所以 lst 变量最终变为:

['lu4nx', 'www.shellcodes.org', 'test']

13 I/O

13.1 缓存

如下代码:

import time

for i in xrange(5):
    print i,
    time.sleep(4)

你会发现变量 i 不是实时打印到终端的。这是因为 print i, 不会输出换行符,默认 Linux 的标准输出是缓存的,当遇到换行符或者达到一定大小之后,才会打印出来。stderr 是不缓存的,一有内容则实时输出。

为了让代码实时输出,可以用 sys.stdout.flush 函数

13.2 逐字节读取文件

with open('file') as f:
    for c in iter(lambda: f.read(1), ''):
        print(c)

13.3 如何获得文件大小

fstream = open('file','r')
fstream.seek(0,os.SEEK_END)    # 将位置移到文件尾
fstream.tell()        # 返回当前位置,就是文件尾。返回的数字就是文件的大小,单位字节

13.4 读取多个文件的内容

from fileinput import FileInput

with FileInput(files=("/etc/hosts", "/etc/hosts.allow")) as f:
    for line in f:
        print(line)

13.5 实现“每读 n 行就 xx”

例如,每读500行,就将这500行的内容发送出去:

with open("/path/file") as f:
    data = []

    while True:
        try:
            if len(data) == 3:
                send_to_queue(data)
                data.clear()

            line = next(f).strip()
            data.append(line)
        except StopIteration:
            # 发送余下的数据
            send_to_queue(data)
            break

14 调试

14.1 跟踪函数调用栈

类似执行 bash -x。

详细追踪:

python -m trace --trace script.py

显示调用了哪些函数:

python -m trace --trackcalls script.py

14.2 如何获得哪个函数调用了当前函数

import traceback

def b():
    for func in traceback.extract_stack():
        print(func.name)

def call_b():
    b()

call_b()

15 工具

15.1 venv

无论是生产环境还是开发环境,virtualenv 在 Python2.x 中广泛使用。Python3 中则提供了和 virtualenv 等价功能的工具——venv。

比如新建一个名为 self-python 的虚拟环境:

$ pyvenv self-python
$ source self-python/bin/activate # 进入环境
(self-python) $

如果在 Python2.x 和 Python3.x 共存的系统中使用 virtualenv,也可指定版本:

$ virtualenv --python /usr/bin/python2.7 self-python

16 Python2 和 Python3

Python3 改进了许多不合理的地方,比如在 Python2.x 中很多标准库的命名风格不统一,到了 Python3 中已经统一,例如 Python2 中的 Queue 已经改成 queue、ConfigParser 改成了 configparser。

16.1 Python2 转 Python3

可以用官方提供的 2to3 这个小工具将 Python2 的代码转成 Python3。

详细参考官方文档:https://docs.python.org/3/library/2to3.html

16.2 导入库兼容

Python3 中很多内置包名已不和 Python2 中相同,比如实现队列的包,在 Python2 中叫“Queue”,Python3 中已经规范为“queue”,在使用时可以通过异常捕获来兼容:

try:
    from Queue import Empty
    from Queue import Queue
except ImportError:
    from queue import Empty
    from queue import Queue

16.3 判断Python版本

if sys.version_info.major == 2:
    # Python2
elif sys.version_info.major == 3:
    # Python3

16.4 Python2 调用 print 函数

print 在 Python3 里已经成为一个函数了,例如:

>>> list(map(print, [1, 2, 3]))
1
2
3
[None, None, None]

如果要在 Python2.x 中完全把 print 当作函数使用,需要显示引用:

from __future__ import print_function

print(1, end='')
print(2, end='')