返回介绍

使用标准库模块

发布于 2025-04-20 18:52:18 字数 15091 浏览 0 评论 0 收藏

笔者在面试产品开发人员的时候,都会和面试者聊一聊其工作中常用的几个 Python 标准库模块的作用和功能(像 os、sys 这样的模块除外),这其实从侧面反映了一个工程师对技术的态度和对 Python 的熟悉程度。

Python 内置了非常多的模块,大部分是纯 Python 的,也有一些用 C 编写的。它们提供标准化的解决方案去解决日常编程中出现的许多问题,建议熟读这些标准库代码。很多最佳实践的代码实现都在标准库里面可以找到,不需要自己造轮子,你也可以参考标准库的实现再去扩展功能。举个例子,笔者之前经常需要将一个多层嵌套的数组(嵌套可以是任何层数)转换为只有一层的数组,也就是编写一个函数 flatten,让 flatten([1, [2], [3, [[4]]]]) 的执行结果是[1, 2, 3, 4]。在不同的项目中都要把笔者实现的这个 flatten 函数放进去。后来发现其实标准库中已经有非常好的实现了:

In : from compiler.ast import flatten
In : flatten([1, [2], [3, [[4]]]])
Out: [1, 2, 3, 4]

本节将介绍一些非常有用的标准库模块。

errno

在日常开发中经常需要捕获各种异常,做特殊处理。举个例子:

In : os.kill(12345, 0)
---------------------------------------------------------------------------
OSError                      Traceback (most recent call last)
<ipython-input-32-11dc6caa503c>in<module>()
----> 1 os.kill(12345, 0)
 
OSError:[Errno 3] No such process

信号为 0,表示这只是检查 PID 的有效性。根据提示,这是一个“No such process”类型的错误,注意方括号中的“Errno 3”,这其实是 Python 内置的错误系统提供的编号:

In : os.strerror(3)
Out: 'No such process'

还可以使用 errno 模块找到对应的错误类型,更精准地做异常处理(errno_example.py):

import os
import errno
 
 
def listdir(dirname):
    try:
        os.listdir(dirname)
    except OSError as e:
        error=e.errno
    if error==errno.ENOENT:
        print 'No such file or directory'
    elif error==errno.EACCES:
        print 'Prmission denied'
    elif error==errno.ENOSPC:
        print 'No space left on device'
    else:
        print e.strerror
    else:
        print 'No error!'
for filename in ['/no/such/dir', '/root', '/home/ubuntu']:
    listdir(filename)

通过对比异常对象的 errno 属性值就能知道异常类型。

subprocess

subprocess 模块用来取代如下模块和函数:

os.system
os.spawn*
os.popen*
popen2.*
commands.*

subprocess 模块提供了几个有用的方法。

1.call:执行系统命令,可以替代 os.system。call 只返回命令的返回值。

In : subprocess.call('ls-l/tmp/mongodb-27017.sock', shell=True)
srwx------1 mongodb mongodb 0 May 10 12:24/tmp/mongodb-27017.sock
Out: 0
In : subprocess.call('exit 1', shell=True)
Out: 1

通常由于安全问题,不建议使用 shell=True,可以把命令拆分成列表:

In : subprocess.call(['ls', '/tmp/mongodb-27017.sock'], shell=False)
/tmp/mongodb-27017.sock
Out: 0

拆分命令最简单的方法是使用 shlex 模块:

In : shlex.split('ls/tmp/mongodb-27017.sock')
Out: ['ls', '/tmp/mongodb-27017.sock']

2.check_call:添加了错误处理的执行系统命令方法。当执行 call 方法的返回值不为 0,就会抛出 CalledProcessError 异常。

In : subprocess.check_call("exit 1", shell=True)
----------------------------------------------------------------------
CalledProcessError         Traceback (most recent call last)
<ipython-input-14-1f03a695d5c6>in<module>()
---->1 subprocess.check_call("exit 1", shell=True)
...
CalledProcessError:Command 'exit 1' returned non-zero exit status 1

3.Popen:一个用来执行子进程的类,通过 communicate 方法获得执行结果:

In : from subprocess import Popen, PIPE
In : proc=Popen(['echo', 'hello!'], stdout=PIPE)
In : stdout, stderr=proc.communicate()
In : print stdout
hello!

Popen 类经常用来实现 Shell 的管道功能,假设要执行“ls/tmp|grep mongodb”,可以使用如下方式:

In : p1=Popen(['ls', '/tmp'], stdout=PIPE)
In : p2=Popen(['grep', 'mongodb'], stdin=p1.stdout, stdout=PIPE)
In : stdout,_=p2.communicate()
In : stdout
Out: 'mongodb-27017.sock\n'

4.check_output:在 Python 2.7 和 Python 3 中都可用,它比 Popen 更简单地获得输出,但是需要执行的返回值为 0,否则仍然抛出 CalledProcessError 异常。

In : subprocess.check_output(['ls', '-l', '/tmp/mongodb-27017.sock'])
Out: 'srwx------1 mongodb mongodb 0 May 10 12:24/tmp/mongodb-27017.sock\n'

在出现错误的时候,可以额外地执行 exit 0,就能正常获得输出:

In : subprocess.check_output('ls/no_such_file;exit 0', stderr=subprocess.
     STDOUT, shell=True)
Out: 'ls:cannot access/no_such_file:No such file or directory\n'

contextlib

写 Python 代码的时候经常将一系列操作放在一个语句块中,Python 2.5 加入了 with 语法,实现上下文管理功能,这让代码的可读性更强并且错误更少。最常见的例子就是 open,如果不使用 with,使用 open 时会是这样:

In : f=open('/tmp/a', 'a')
In : f.write('hello world')
In : f.close()

如果使用 with,可以简化为两行:

In : with open('/tmp/a', 'a') as f:
....:   f.write('hello world')
....:

在执行完缩进的代码块后会自动关闭文件。

同样的例子还有 threading.Lock,如果不使用 with,需要这样写:

import threading
lock=threading.Lock()
      
lock.acquire()
try:
    my_list.append(item)
finally:
    lock.release()

如果使用 with,就会非常简单:

with lock:
    my_list.append(item)

创建上下文管理器实际就是创建一个类,添加__enter__和__exit__方法。看看 threading.Lock 的上下文管理功能是怎么实现的:

class LockContext(object):
         
    def __init__(self):
        print '__init__'
        self.lock=threading.Lock()
      
    def __enter__(self):
        print '__enter__'
        self.lock.acquire()
        return self
      
    def __exit__(self, exc_type, exc_val, exc_tb):
        print '__exit_'
        self.lock.release()
      
with LockContext():
    print 'In the context'

执行的输出如下:

__init__
__enter__
In the context
__exit_

上面的例子比较简单,在执行 self.__enter__的时候没有传递参数。下面我们来实现 open 的上下文管理功能:

class OpenContext(object):
      
    def __init__(self, filename, mode):
        self.fp=open(filename, mode)
      
    def __enter__(self):
        return self.fp
      
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.fp.close()

    
with OpenContext('/tmp/a', 'a') as f:
    f.write('hello world')

自定义上下文管理器确实很方便,但是 Python 标准库还提供了更易用的上下文管理器工具模块 contextlib,它是通过生成器实现的,我们不必再创建类以及__enter__和__exit__这两个特殊的方法:

from contextlib import contextmanager
      
 
@contextmanager
def make_open_context(filename, mode):
    fp=open(filename, mode)
    try:
        yield fp
    finally:
        fp.close()
  
      
with make_open_context('/tmp/a', 'a') as f:
    f.write('hello world')

yield 关键词把上下文分割成两部分:yield 之前就是__init__中的代码块;yield 之后其实就是__exit__中的代码块;yield 生成的值会绑定到 with 语句 as 子句中的变量(如果没有生成,也就没有 as 字句)。

如果有多个上下文,通常需要嵌套:

@contextmanager
def make_context(*args):
    print args
    yield
 
     
with make_context(1, 2) as A:
    with make_context(3, 4) as B:
        print 'In the context'

事实上,Python 2.7 中 with 语句不需要嵌套:

with make_context(1, 2) as A, make_context(3, 4) as B:
    print 'In the context'

如果用的 Python 版本小于 2.7,需要使用 contextlib.nested 减少 with 语句的嵌套:

from contextlib import nested
          
                      
with nested(make_context(1, 2), make_context(3, 4)) as (A, B):
    print 'In the context'

glob

glob 用来匹配 UNIX 风格的路径名字的模块,它支持“*”、“?”、“[]”这三种通配符,“*”代表 0 个或多个字符,“?”代表一个字符,“[]”匹配指定范围内的字符,例如[0-9]匹配数字。我们先创建一个目录,目录中包含如下文件:

> ls/tmp/chapter14
1.txt 2.txt a2.txt a3.txt a.txt b.txt

看一下 glob 的匹配效果:

In : glob.glob('/tmp/chapter14/a*.txt')
Out: ['/tmp/chapter14/a3.txt', '/tmp/chapter14/a.txt', '/tmp/chapter14/a2.txt']
In : glob.glob('/tmp/chapter14/a?.txt')
Out: ['/tmp/chapter14/a3.txt', '/tmp/chapter14/a2.txt']
In : glob.glob('/tmp/chapter14/a[0-9].txt')
Out: ['/tmp/chapter14/a3.txt', '/tmp/chapter14/a2.txt']
In : glob.glob('/tmp/chapter14/a[0-2].txt')
Out: ['/tmp/chapter14/a2.txt']

operator

operator 是一个内建操作的函数式接口。举个例子,如果想对列表的值求和,可以使用 sum,但是如果要相乘呢?其实可以结合 reduce 和 operator 模块来实现:

In : import operator
In : reduce(operator.mul, (5, 4, 3, 2, 1))
Out: 120

还能借用 operator 模块实现伪 lisp 代码:

In : def f(op,*args):
...:    return {
...:        '+' : operator.add,
...:        '-' : operator.sub,
...:        '*' : operator.mul,
...:        '/' : operator.div,
...:        '%' : operator.mod
...:    }[op](*args)
...:
In : f('*', f('+', 1, 2), 3)
Out: 9

operator 模块还提供了非常有用的 itemgetter、attrgetter 和 methodcaller 方法。

1.itemgetter。通过被求值对象的__getitem__方法获得符合的条目:

In : l=[1, 2, 3, 4, 5]
In : operator.itemgetter(1)(l)
Out: 2
In : operator.itemgetter(1, 3, 4)(l)
Out: (2, 4, 5)

另一个使用 itemgetter 的场景是排序:

In : objs=[('a', 2), ('b', 4), ('c', 1)]
      
In : sorted(objs, key=operator.itemgetter(1))
Out: [('c', 1), ('a', 2), ('b', 4)]

上述例子是对列表每个元组的第二个元素进行升序排序,等价于:

In : sorted(objs, key=lambda x:x[1])
Out: [('c', 1), ('a', 2), ('b', 4)]

2.attrgetter。attrgetter 根据被求值对象的属性获得符合条件的结果:

In : import sys
In : operator.attrgetter('platform')(sys)
Out: 'linux2'
In : operator.attrgetter('platform', 'maxint')(sys)
Out: ('linux2', 9223372036854775807)

想要获得 a.b.c.d 这样的层级很深的属性或者想动态获得嵌套属性,若不使用 attrgetter,则写法是:

In : attr=a
      
In : for sub in ['b', 'c', 'd']:
....:    attr=getattr(attr, sub)
....:

再简单一点:

In : reduce(getattr, 'b.c.d'.split('.'), a)

而使用 attrgetter 就很简洁:

In : operator.attrgetter('b.c.d')(a)

3.methodcaller。methodcaller 将调用被求值对象的方法:

# 相当于 h.hello()
operator.methodcaller('hello')(h)
# 相当于 h.func(1, b=2)
operator.methodcaller('func', 1, b=2)(h)

functools

functools 模块中包含了一系列操作其他函数的工具。

1.partial。partial 可以重新定义函数签名,也就是在执行函数之前把一些参数预先传给函数,待执行时传入的参数数量会减少。

In : import functools
      
In : def f(a, b=3):
...:    return a + b
...:
In : f2=functools.partial(f, 1)
In : f2(5)
Out: 6
In : f2()
Out: 4

上例中,本来函数 f 需要传入两个参数,但是使用 partial 把参数 a 传入(值为 1),生成了函数 f2,调用 f2 只需要传入参数 b(或者不传入,直接使用默认值)即可。当然可以在 partial 的时候把参数 b 的值预先传入:

In : f2=functools.partial(f, b=10)
In : f2(1)
Out: 11

2.wraps。把被封装函数的__name__、__module__、__doc__和__dict__复制到封装函数中,这样在未来排错或者函数自省的时候能够获得正确的源函数的对应属性,所以使用 wraps 是一个好习惯。我们先看不使用 wraps 的例子:

In : def deco(f):
...:    def wrapper(*args,**kwargs):
...:        return f(*args,**kwargs)
...:    return wrapper
...:
In : @deco
...: def func():
...:    '''This is__doc__'''
...:    return 1
...:
In : func.__doc__
In : func.__name__
Out: 'wrapper'

可以发现 func.__doc__和 func.__name__等都是错误的,它们错误地使用装饰器的对应属性。正确的方法是使用 wraps 拷贝源函数属性:

In : def deco2(f):
...:    @functools.wraps(f)
...:    def wrapper(*args,**kwargs):
...:        return f(*args,**kwargs)
...:    return wrapper
...:
In : @deco2
...: def func():
...:    '''This is __doc__'''
...:    return 1
...:
In : func.__doc__
Out: 'This is__doc__'
In : func.__name__
Out: 'func'

3.total_ordering。对比自定义对象需要添加__lt__、__le__、__gt__、__ge__和__eq__等方法,如果使用 total_ordering,只需要定义__eq__,以及定义__lt__、__le__、__gt__、__ge__四种方法之一就可以了:

In : @functools.total_ordering
...: class Size(object):
...:    def __init__(self, value):
...:        self.value=value
...:    def __lt__(self, other):
...:        return self.value < other.value
...:    def __eq__(self, other):
...:        return self.value==other.value
      
In : Size(3) > Size(2)
Out: True
In : Size(2)==Size(2)
Out: True

total_ordering 会自动设置未定义的三种特殊方法。

4.cmp_to_key。Python 2 的 sorted 函数除了通过指定 key 参数的值作为依据来排序,还支持 cmp 参数:

In : def numeric_compare(x, y):
...:    return x[1] - y[1]
...:
In : sorted(objs, cmp=numeric_compare)
Out: [('c', 1), ('a', 2), ('b', 4)]

cmp_to_key 可以帮助我们将这种旧式比较的 cmp 函数转换为 key 的参数:

In : sorted(objs, key=functools.cmp_to_key(numeric_compare))
Out: [('c', 1), ('a', 2), ('b', 4)]

除了兼容 Python 3 的 sorted,还可以把比较函数转换后用于 min、max、heapq.nlargest、heapq.nsmallest、itertools.groupby 等支持 key 参数的函数上。

collections

collections 模块包含了 5 个高性能的数据类型。

1.Counter:一个方便、快速计算的计时器工具。

In : import collections
      
In : words=['a', 'b', 'a', 'c', 'd', 'c']
      
In : cnt=collections.Counter(words)
In : cnt.most_common(3)
Out: [('a', 2), ('c', 2), ('b', 1)]

Counter 除了可以接受多种类型的参数以及方便获得根据计数后的排序外,还有一个最重要的功能,就是可以和其他 Counter 实例做计算:

In : cnt2=collections.Counter('aldcdae')
In : cnt+cnt2 # 两个计数结果组合
Out: Counter({'a': 4, 'b': 1, 'c': 3, 'd': 3, 'e': 1, 'l': 1})
In : cnt-cnt2 # cnt2 相对于 cnt 计数结果的差集
Out: Counter({'b':1, 'c':1})
In : cnt&cnt2 # 两个计数结果的交集
Out: Counter({'a': 2, 'c': 1, 'd': 1})
In : cnt|cnt2 # 两个计数结果的并集
Out: Counter({'a': 2, 'b': 1, 'c': 2, 'd': 2, 'e': 1, 'l': 1})

2.deque:一个双端队列,能够在队列两端添加或删除队列元素。它支持线程安全,能够有效利用内存。无论从队列的哪端入队和出队,性能都能够接近于 O(1)。

In : d=collections.deque('ab')
In : d
Out: deque(['a', 'b'])
In : d.append('c')
In : d
Out: deque(['a', 'b', 'c'])
In : d.appendleft('d')
In : d
Out: deque(['d', 'a', 'b', 'c'])
In : d.popleft()
Out: 'd'
In : d
Out: deque(['a', 'b', 'c'])
In : d.extend('xy')
In : d
Out: deque(['a', 'b', 'c', 'x', 'y'])
In : d.extendleft('op')
In : d
Out: deque(['p', 'o', 'a', 'b', 'c', 'x', 'y'])

除了列表操作,deque 还支持队列的旋转操作:

In : d.rotate(2)
In : d
Out: deque(['x', 'y', 'p', 'o', 'a', 'b', 'c'])
In : d.rotate(-3)
In : d
Out: deque(['o', 'a', 'b', 'c', 'x', 'y', 'p'])

如果 rotate 的参数 N 的值大于 0,表示将右端的 N 个元素移动到左端,否则相反。虽然 Python 内置的数据结构 list 也支持类似的操作,但是当遇到 pop(0) 和 insert(0, v) 这样既改变了列表长度又改变其元素位置的操作时,其复杂度就变为 O(n ) 了。

3.defaultdict。defaultdict 简化了处理不存在的键的场景。如果不使用 defaultdict,对一个单词的计数要这样实现:

In : d={}
      
In : words
Out: ['a', 'b', 'a', 'c', 'd', 'c']
      
In : for w in words:
....:    if w in d:
....:        d[w]+=1
....:    else:
....:        d[w]=1
....:

如果使用 defaultdict,就不需要判断字典中是否已经存在此键:

In : d=collections.defaultdict(int)
In : for w in words:
....:    d[w]+=1
....:

defaultdict 参数就是值的类型,还可以使用自定义类型。下面演示一个插入后自动排序的自定义列表类型:

In : import bisect
In : class bisectedList(list):
....:    def insort(self, arr):
....:        bisect.insort_left(self, arr)
....:
In : d=collections.defaultdict(bisectedList)
In : d['l'].insort(1)
In : d['l'].insort(9)
In : d['l'].insort(3)
In : d['l']
Out: [1, 3, 9]

defaultdict 对于不同的数据结构都有默认值,比如 int 的默认值是 0:

In : d=collections.defaultdict(int)
In : d['a']
Out: 0

如果想使用其他默认值,可以借用匿名函数 lambda 来实现:

In : d=collections.defaultdict(lambda x=10:x)
In : d['a']
Out: 10
In : d['b']=1
In : d['b']
Out: 1

4.OrderedDict。Python 的 dict 结构是无序的:

In : d=dict([('a', 1), ('b', 2), ('c', 3)])
In : for k, v in d.items():
....:    print k, v
....:
a 1
c 3
b 2

在一些场景下,是必须要有顺序的,可以使用 OrderedDict 这个数据结构来保证字典键值对的顺序:

In : d=collections.OrderedDict([('a', 1), ('b', 2), ('c', 3)])
In : for k, v in d.items():
...:    print k, v
...:
a 1
b 2
c 3

5.namedtuple。namedtuple 能创建可以通过属性访问元素内容的扩展元组。使用 namedtuple 能创建更健壮、可读性更好的代码。假设不使用 namedtuple,查询数据库获得如下一条记录:

user=('xiaoming', 23, 'beijing', '01012345678')

需要通过索引获取对应的内容,比如想知道用户的年龄,就得使用 user[1]。在大型应用中使用这种不明显的方式会加大项目的复杂度和维护成本,因为每个看到这段代码的人都得去找到 user 的定义,再联想 user[1]是什么。使用 namedtuple 则让代码非常清晰:

In : User=collections.namedtuple('User', 'name age location phone')
In : user=User('xiaoming', 23, 'beijing', '01012345678')
In : user.age
Out: 23
In : user._asdict()
Out:
OrderedDict([('name', 'xiaoming'),
            ('age', 23),
            ('location', 'beijing'),
            ('phone', '01012345678')])

user.age 这个名字可以明确地告诉开发者它所表达的意义,同时还可以非常方便地把从数据库等来源获得的数据转化为 User 对象:

In : cursor=conn.cursor()
In : cursor.execute('SELECT name, age, location, phone FROM users')
In : for user in map(User._make, cursor.fetchall()):
...:    print user.name, user.phone

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。