我的 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. 如何给数字添加千分符号

format(12345, ',') # => 12,345

3. 字符串

3.1. 字符串对齐

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

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

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

3.2. 字符串连接技巧

如下两个变量,hello 和 world:

hello = 'hello '
world = 'world'

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

hello + world

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

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

这样做的好处:

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

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

3.3. 生成等宽数字

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

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

或者:

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

3.4. 字符串转 raw_string

Python 里没有现成的函数可以把字符串转成 raw string。

需要转变的原始字符串如下:

s = "\\xe4\\xbd\\xa0\\xe5\\xa5\\xbd"

我想到的是用 eval 函数来处理,Python2 实现如下:

print(eval("'%s'" % s)) # => 你好

Python3 处理时依赖 bytes 类型,因此提供给 eval 参数的字符串就显著标识未 bytes 类型:

eval("b'%s'" % s).decode("utf-8")  # => 你好

使用 eval 一定要注意安全。

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}

用 Counter 一定要注意,value 小于等于 0 的项会被忽略掉,如果要包含值为 0 的项,只有手动实现两个字典相加。

5. 列表

5.1. 清空列表

假设有如下列表:

l = range(10)

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

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

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

5.2. 复制列表

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

tmp = urls

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

tmp = urls[:]

5.3. 遍历列表时带上下标

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

5.4. 平坦(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.5. 统计列表各项重复次数

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})

5.6. 遍历列表时一次取 n 个元素

假设列表 file_ids 保存了 n 个待下载的文件 id,但 API 限制每次最多只能下载 3 个 id 对应的文件,因此需要遍历列表,每次取 3 个元素:

file_ids = ["001", "002", "003", "004", "005", "006", "007"]

step = 3
for i in range(0, len(file_ids), step):
    print(file_ids[i: i + step])

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. Bytes

7.1. 使用 memoryview 减少内存占用

在处理较大的字节流时,通常是将它读入到内存中,如果只需将数据其中一部分分配给新的变量,Python 内部就会创建新的对象,并分配空间,这样往往会占用很大的内存空间,Python 不像 C 语言那样对内存有很自由的掌控,但 Python 提供了 memoryview 这个内置函数来直接访问原目标的内存空间,而不是拷贝后创建新对象。

with open('/dev/random', 'rb') as f:
    data = f.read(1024000)
    some_data = data[1024:]

上方代码中,some_data 是新对象,Python 会从 1024 位置开始拷贝数据并创建新对象,因此会占用较大的内存空间,如果改用 memoryview:

some_data = memoryview(data)[1024:]

这是 some_data 和 data 是在同一片内存空间做操作,因此不会新拷贝数据,并且可以直接通过下标引用数据:

some_data[0:10].hex()

8. 循环

8.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

8.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

9. 生成器

9.1. 生成器表达式

it = (i for i in range(10) if i%2 == 0)
list(it)               # => [0, 2, 4, 6, 8]

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

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

sum(i for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] if i%2 == 0)  # => 20

9.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

可以这样调用:

it = counter(10)
it.next()                       # => 0
it.next()                       # => 1
it.next()                       # => 2
it.next()                       # => 3
it.send(8)                      # => 8
it.next()                       # => 9

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

9.3. 无限迭代

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

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

10. 序列解构

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)

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

10.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)

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

11. 内置变量

11.1. __debug__

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

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

12. 操作符

12.1. 对象之间比较

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

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

13.

13.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

14. 数据对象可修改问题

一定要注意哪些情况下修改传递的对象会影响到原始数据,否则很容易导致程序 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']

15. I/O

15.1. 缓存

如下代码:

import time

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

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

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

15.2. 逐字节读取文件

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

15.3. 如何获得文件大小

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

15.4. 读取多个文件的内容

from fileinput import FileInput

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

15.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

16. 系统

16.1. 获得当前文件所在目录(而不是当前目录)

from os.path import dirname, abspath

print(dirname(abspath(__file__)))

17. 调试

17.1. 跟踪函数调用栈

类似执行 bash -x。

详细追踪:

python -m trace --trace script.py

显示调用了哪些函数:

python -m trace --trackcalls script.py

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

import traceback

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

def call_b():
    b()

call_b()

18. 工具

18.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

19. Python2 和 Python3

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

19.1. Python2 转 Python3

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

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

19.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

19.3. 判断Python版本

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

19.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='')