优雅的python

Table of Contents

循环

遍历一个范围内的数字

for i in [0, 1, 2, 3, 4, 5]:
    print(i ** 2)
0
1
4
9
16
25

更好的代码:

for i in range(6):
    print(i ** 2)
0
1
4
9
16
25

  • range 会返回一个迭代器,用来一次一个值地遍历一个范围

遍历一个集合

colors = [ 'red',  'green',  'blue',  'yellow' ]

for i in range(len(colors)):
    print(colors[i])
red
green
blue
yellow

更好的写法:

for color in colors:
    print(color)
red
green
blue
yellow

反向遍历一个集合

colors = [ 'red',  'green',  'blue',  'yellow' ]

for i in range(len(colors) - 1,  -1,  -1):
        print (colors[i]) 
yellow
blue
green
red

更好的写法:

for  color in reversed(colors):
    print (color) 
yellow
blue
green
red

这种写法效率高,优雅,而且省去亲自创建和自增下标。当你发现在操作集合的下标时,你很有可能在做错事

遍历两个集合

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']

n = min(len(names), len(colors))

for i in range(n):
    print (names[i], '->', colors[i])
raymond -> red
rachel -> green
matthew -> blue

更好的写法:

for name, color in zip(names, colors):
    print (name, '->', color)
raymond -> red
rachel -> green
matthew -> blue

注意:在 Python 3 中,izip 改名为 zip,并替换了原来的 zip 成为内置函数

有序地遍历

  • 顺序遍历:

    colors = [ 'red',  'green',  'blue',  'yellow' ]
    
    for color in sorted(colors):
        print (color)
    
    blue
    green
    red
    yellow
    
    
  • 倒序遍历:

    colors = [ 'red',  'green',  'blue',  'yellow' ]
    
    for color in sorted(colors, reverse=True):
        print (color)
    
    yellow
    red
    green
    blue
    
    

自定义排序顺序

colors = [ 'red',  'green',  'blue',  'yellow' ]

def compare_length(c1, c2):
    if len(c1) < len(c2):
        return -1
    if len(c1) > len(c2):
        return 1
    return 0

for color in sorted(colors, cmp=compare_length):
    print (color)
['red', 'blue', 'green', 'yellow']

更好的写法:

print (sorted(colors, key=len)) 
['red', 'blue', 'green', 'yellow']

调用一个函数直到遇到标记值

blocks = []

while True:
    block = f.read(32)
    if block == '':
        break
    blocks.append(block)

更好的写法:

blocks = []

for block in iter(partial(f.read, 32),  ''):
    blocks.append(block)

iter 接受两个参数:第一个是你反复调用的函数,第二个是标记值。第二种写法的优势在于 iter 的返回值是个迭代器,迭代器能用在各种地方,set,sorted,min,max,heapq,sum等

在循环内识别多个退出点

seq = [0, 1, 2, 3, 4, 5, 6]

def find(seq, target):
    found = False

    for i, value in enumerate(seq):
        if value == target:
            found = True
            break

    if not found:
        return -1

    return i

print (find(seq, 3)) # 3 
print (find(seq, 8)) # -1 
3
-1

更好的写法,for 执行完所有的循环后就会执行 else:

def find(seq, target):
    for i, value in enumerate(seq):
        if value == target:
            break

    else:
        return -1

    return i

有两种方法去理解 for-else:

  • 把 for 看作 if,当 for 后面的条件为 False 时执行 else。其实条件为 False 时,就是 for 循环没被 break 出去,把所有循环都跑完的时候
  • 把 else 记成 nobreak,当 for 没有被 break,那么循环结束时会进入到 else

字典

遍历字典的 key

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k in d:
    print (k)
raymond
rachel
matthew

在迭代中修改容器是非常危险的:

for k in list(d.keys()):
    if k.startswith('r'):
        del (d[k])

{'matthew': 'blue'}

list(d.keys()): 把字典里所有的 key 都复制到一个列表里。然后就可以修改字典

遍历一个字典的 key 和 value

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

# 并不快,每次必须要重新哈希并做一次查找 
for k in d:
    print (k, '->', d[k]) 
matthew -> blue
rachel -> green
raymond -> red

更好的写法:

# for k, v in d.iteritems():
#     print (k, '->', v) 

for k, v in d.items():
    print (k, '->', v)
matthew -> blue
rachel -> green
raymond -> red

注意:Python 3 已经没有 iteritems() 了,items() 的行为和 iteritems() 很接近,返回一个迭代器

用 key-value 对构建字典

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue']
# d = dict(izip(names, colors))

d = dict(zip(names, colors)) 
print (d)
{'matthew': 'blue', 'raymond': 'red', 'rachel': 'green'}

用字典计数

简单,基本的计数方法。适合初学者起步时学习:

colors = ['red', 'green', 'red', 'blue', 'green', 'red']

d = {}
for color in colors:
    if color not in d:
        d[color] = 0
    d[color] += 1

print(d)
{'blue': 1, 'green': 2, 'red': 3}

更好的写法,使用初始值:

d = {}

for color in colors:
    d[color] = d.get(color, 0) + 1

{'red':  3, 'green': 2, 'blue': 1}

更新潮的方法:

from collections import defaultdict

d = defaultdict(int)

for color in colors:
    d[color] += 1

print(d) 
defaultdict(<class 'int'>, {'green': 2, 'blue': 1, 'red': 3})

用字典分组

按 name 的长度分组:

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

d = {}

for name in names:
    key = len(name)
    if key not in d:
        d[key] = []
    d[key].append(name)

print(d) 
{5:      ['roger', 'betty'], 6: ['rachel', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']}

更好的写法:

d = {}

for name in names:
    key = len(name)
    d.setdefault(key, []).append(name)

print(d) 
{5:      ['roger', 'betty'], 6: ['rachel', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']}

更新潮的写法:

from collections import defaultdict

d = defaultdict(list)

for name in names:
    key = len(name)
    d[key].append(name)

print(d) 
defaultdict(<class 'list'>, {5: ['roger', 'betty'], 6: ['rachel', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']})

原子操作popitem

popitem 是原子的,所以多线程的时候没必要用锁包着它:

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

while d:
    key, value = d.popitem()
    print(key, '->', value)

print(d)
rachel -> green
matthew -> blue
raymond -> red

{}

连接字典

处理配置参数: 默认使用第一个字典(从配置文件读取),接着用环境变量覆盖它,最后用命令行参数覆盖它,不幸的是,这种方法拷贝数据太疯狂!

defaults = {'color': 'red',
            'USER': 'guest'}

d = defaults.copy()
for k, v in d.items():
    print(k, '->', v)

import os 
d.update(os.environ)
for k, v in d.items():
    print(k, '->', v)

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-u', '-user')
parser.add_argument('-c', '-color')

namespace = parser.parse_args([])
command_line_args = {k: v for k, v in vars(namespace).items() if v}

d.update(command_line_args)
for k, v in d.items():
    print(k, '->', v)
USER -> guest
color -> red

GSETTINGS_BACKEND -> dconf
HUSHLOGIN -> FALSE
......
USER -> klose
......
color -> red
XIM -> fcitx

......

更高效优雅的写法:

from collections import ChainMap
import os

d = ChainMap(command_line_args, os.environ, defaults)
for k, v in d.items():
    print(k, '->', v) 
......
USER -> klose
......
color -> red
......

可读性

位置参数和下标很漂亮,但关键字和名称更好:

  • 第一种方法对计算机来说很便利
  • 第二种方法和人类思考方式一致

用关键字参数提高函数调用的可读性

twitter_search('@obama', False, 20, True) 

更好地做法:

twitter_search('@obama', retweets=False, numtweets=20, popular=True)

第二种方法稍微慢一点,但为了代码的可读性和开发时间,值得。

用 namedtuple 提高多个返回值的可读性

测试结果是好是坏?你看不出来,因为返回值不清晰:

doctest.testmod()
# (0, 4) 

更好的写法,使用一个 namedtuple 作为返回值:

doctest.testmod()
# TestResults(failed=0, attempted=4)

namedtuple 是 tuple 的子类,所以仍适用正常的元组操作,但它更友好

创建namedtuple

from collections import namedtuple

TestResults = namedtuple('TestResults', ['failed', 'attempted'])
testResult = TestResults(failed=0, attempted=4)

print(testResult) 

TestResults(failed=0, attempted=4)

unpack 序列

p =  'Raymond', 'Hettinger',  0x30, 'python@example.com'

# 其它语言的常用方法/习惯 
fname = p[0]
lname = p[1]
age = p[2]
email = p[3]
('Raymond', 'Hettinger', 48, 'python@example.com') 

用unpack元组,更快,可读性更好:

fname, lname, age, email = p

print(fname)
print(lname)
print(age)
print(email) 
'Raymond'
'Hettinger'
48
'python@example.com'

更新多个变量

def fibonacci(n):
    x = 0
    y = 1
    for i in range(n):
        print(x) 
        t = y
        y = x + y
        x = t

print(fibonacci(10))
0
1
1
2
3
5
8
13
21
34

这种写法的问题在于:

  • 状态应该在一次操作中更新
  • 操作有顺序要求
  • 太底层,太细节

更好的写法:

def fibonacci(n):
    x, y = 0, 1
    for i in range(n):
        print(x)
        x, y = y, x + y

print(fibonacci(10)) 
0
1
1
2
3
5
8
13
21
34

效率

优化的基本原则:

  • 除非必要,别无故移动数据
  • 稍微注意一下用线性的操作取代O(n**2)的操作

连接字符串

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

s = names[0]
for name in names[1:]:
    s += ', ' + name

print (s)
raymond, rachel, matthew, roger, betty, melissa, judith, charlie

更好的写法:

print (', '.join(names))
raymond, rachel, matthew, roger, betty, melissa, judith, charlie

更新序列

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

del names[0]
# 下面的代码标志着你用错了数据结构
names.pop(0)
names.insert(0, 'mark')
['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'] 
['rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie']
['matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'] 
['mark', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie']

使用deque(双向链表)往往更有效率:

 from collections  import deque
names = deque(['raymond', 'rachel', 'matthew', 'roger',
               'betty', 'melissa', 'judith', 'charlie'])
# 用deque更有效率
del names[0]
names.popleft()
names.appendleft('mark') 
deque(['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'])
deque(['rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'])
deque(['matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'])
deque(['mark', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'])

装饰器和上下文管理

  • 把业务和管理的逻辑分开
  • 提高代码重用性
  • 起个好名字很关键
  • 能力越大,责任越大

装饰器

混着业务和管理逻辑,无法重用:

def web_lookup(url, saved={}):
    if url in saved:
        return saved[url]
    page = urllib.urlopen(url).read()
    saved[url] = page
    return page

使用装饰器分离缓存逻辑:

@cache
def web_lookup(url):
    return urllib.urlopen(url).read()

上下文管理器

临时上下文

保存旧的上下文,创建新的:

old_context = getcontext().copy()
getcontext().prec = 50
print(Decimal(355) / Decimal(113)) 
setcontext(old_context)

更好的写法:

with localcontext(Context(prec=50)):
    print (Decimal(355) / Decimal(113)) 

文件

f = open('data.txt')
try:
    data = f.read()
finally:
    f.close()

更好的写法:

with open('data.txt') as f:
    data = f.read()

# 创建锁
lock = threading.Lock()

# 使用锁的老方法
lock.acquire()
try:
    print 'Critical section 1'
    print 'Critical section 2'
finally:
    lock.release()

更好的写法:

# 使用锁的新方法
with lock:
    print 'Critical section 1'
    print 'Critical section 2'

忽略异常

try:
    os.remove('somefile.tmp')
except OSError:
    pass

更好的写法:

with ignored(OSError):
    os.remove('somefile.tmp')

注意:ignored是Python 3.4加入的,也可以自己创建ignore上下文管理器

@contextmanager
def ignored(*exceptions):
    try:
        yield
    except exceptions:
        pass

标准输出重定向

# 临时把标准输出重定向到一个文件,然后再恢复正常
with open('help.txt', 'w') as f:
    oldstdout = sys.stdout
    sys.stdout = f
    try:
        help(pow)
    finally:
        sys.stdout = oldstdout

更好的写法:

with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        help(pow)

注意:edirect_stdout在Python 3.4加入,也可以实现自己的redirect_stdout上下文管理器

@contextmanager
def redirect_stdout(fileobj):
    oldstdout = sys.stdout
    sys.stdout = fileobj
    try:
        yield fieldobj
    finally:
        sys.stdout = oldstdout

简洁

两个冲突的原则:

  1. 一行不要有太多逻辑
  2. 不要把单一的想法拆分成多个部分

Raymod的原则:一行代码的逻辑等价于一句自然语言

列表解析和迭代器

表达你在做什么:

result = []

for i in range(10):
    s = i ** 2
    result.append(s)

print (sum(result))
285

更好的做法,表达你想要什么:

print (sum(i**2 for i in range(10))) 
285