-
并发与并行
线程
建议6.1 多线程适用于阻塞式IO场景,不适用于并行计算场景
Python的标准实现是CPython。
CPython执行Python代码分为2个步骤:首先,将文本源码解释编译为字节码,然后再用一个解释器去解释运行字节码。字节码解释器是有状态的,需要维护该状态的一致性,因此使用了GIL(Global Interpreter Lock,全局解释器锁)。
GIL的存在,使得CPython在执行多线程代码的时候,同一时刻只有一个线程在运行,无法利用多CPU提高运算效率。但是这个特点也带来了一个好处:CPython运行多线程的时候,内部对象缺省就是线程安全的。这个特性,被非常多的Python库开发者所依赖,直到CPython的开发者想要去除GIL的时候,发现已经有大量的代码库重度依赖这个GIL带来的内部对象缺省就是线程安全的特性,变成一个无法解决的问题了。
虽然多线程在并行计算场景下无法带来好处,但是在阻塞式IO场景下,却仍然可以起到提高效率的作用。这是因为阻塞式IO场景下,线程在执行IO操作时并不需要占用CPU时间,此时阻塞IO的线程可以被挂起的同时继续执行IO操作,而让出CPU时间给其他线程执行非IO操作。这样一来,多线程并行IO操作就可以起到提高运行效率的作用了。
综上,Python的标准实现CPython,由于GIL的存在,同一个时刻只能运行一个线程,无法充分利用多CPU提升运算效率,因此Python的多线程适用于阻塞式IO的场景,不适用于并行计算的场景。
下面举一个对计算量有要求的求一个数的因数分解的代码实例,来说明Python多线程不适用于并行计算的场景:
-- coding:utf-8 --
from time import time
from threading import Thread
def factorize(number):
for i in range(1, number + 1):
if number % i == 0:
yield i
class FactorizeThread(Thread):
def init(self, number):
Thread.init(self)
self.number = number
def run(self):
self.factors = list(factorize(self.number))
def test(numbers):
start = time()
for number in numbers:
list(factorize(number))
end = time()
print(‘Took %.3f seconds’ % (end - start))
def test_thread(numbers):
start = time()
threads = []
for number in numbers:
thread = FactorizeThread(number)
thread.start()
threads.append(thread)
for t in threads:
t.join()
end = time()
print(‘Mutilthread Took %.3f seconds’ % (end - start))
if name == “main”:
numbers = [2139079, 1214759, 1516637, 1852285]
test(numbers)
test_thread(numbers)
Copy
代码输出:
Took 0.319 seconds
Mutilthread Took 0.539 seconds
Copy
以上代码运行结果只是一个参考值,具体数据跟运行环境相关。但是可以看到单线程方式比多线程方式的计算速度要快。由于CPython运行多线程代码时因为GIL的原因导致每个时刻只有一个线程在运行,因此多线程并行计算并不能带来时间上的收益,反而因为调度线程而导致总时间花费更长。
对于IO阻塞式场景,多线程的作用在于发生IO阻塞操作时可以调度其他线程执行非IO操作,因此在这个场景下,多线程是可以节省时间的。可以用以下的代码来验证:
-- coding:utf-8 --
from time import time
from threading import Thread
import os
def slow_systemcall(n):
for x in range(100):
open(“test_%s” % n, “a”).write(os.urandom(10) * 100000)
def test_io(N):
start = time()
for _ in range(N):
slow_systemcall(_)
end = time()
print(‘Took %.3f seconds’ % (end - start))
def test_io_thread(N):
start = time()
threads = []
for _ in range(N):
thread = Thread(target=slow_systemcall, args=(“t_%s”%_,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
end = time()
print(‘Took %.3f seconds’ % (end - start))
if name == “main”:
N = 5
test_io(N)
test_io_thread(N)
Copy
代码输出:
Took 5.179 seconds
Multithread Took 1.451 seconds
Copy
可以看到单线程花费时间与多线程花费时间之比接近1:4,考虑线程调度的时间,这个跟一般语言的多线程起的作用比较相似。这是因为当Python执行IO操作时,实际上是执行了系统调用,此时线程会释放GIL,直到系统调用结束时,再申请获取GIL,也就是在IO操作期间,线程确实是并行执行的。
Python的另外一个实现JPython就没有GIL,但是它并不是最常见的Python实现。
建议6.2 建议使用Queue来协调各线程之间的工作
如果Python程序同时要执行许多事务,那么开发者经常需要协调这些事务。而在各种协调方式中,较为高效的一种,则是采用函数管线。
管线的工作原理,与制造业中的组装生产线(assembly line)相似。管线分为许多首尾相连的阶段,每个阶段都由一种具体的函数来负责。程序总是把待处理的新部件添加到管线的开端。每一种函数都可以在它所负责的那个阶段内,并发地处理位于该阶段的部件。等负责本阶段的那个函数把某个部件处理好之后,该部件就会传送到管线中的下一个阶段,以此类推,直到全部阶段都经历一遍。涉及阻塞式I/O操作或子进程的工作任务,尤其适合用此办法处理,这样的任务很容易分配到多个Python线程或进程中。
例如,要构建一个照片处理系统,该系统从数码相机里面持续获取照片、调整其尺寸,并将其添加到网络相册中。这样的程序,可以采用三个阶段的管线来做。第一个阶段获取新图片,第二个阶段把下载好的图片传给缩放函数,第三个阶段把缩放后的图片交给上传函数。利用内置模块Queue中的Queue类来实现,可以变得容易且健壮。示例代码如下:
from Queue import Queue
from threading import Thread
def download(item):
print ‘download item’
return item
def resize(item):
print ‘resize item’
return item
def upload(item):
print ‘upload item’
return item
class ClosableQueue(Queue):
SENTINEL = object()
def close(self):
self.put(self.SENTINEL)
def __iter__(self):
while True:
item = self.get()
try:
if item is self.SENTINEL:
return # Cause the thread to exit
yield item
finally:
self.task_done()
class StoppableWorker(Thread):
def init(self, func, in_queue, out_queue):
super(StoppableWorker, self).init()
self.in_queue = in_queue
self.out_queue = out_queue
self.func = func
def run(self):
for item in self.in_queue:
result = self.func(item)
self.out_queue.put(result)
if name == ‘main’:
download_queue = ClosableQueue()
resize_queue = ClosableQueue()
upload_queue = ClosableQueue()
done_queue = ClosableQueue()
threads = [
StoppableWorker(download, download_queue, resize_queue),
StoppableWorker(resize, resize_queue, upload_queue),
StoppableWorker(upload, upload_queue, done_queue),
]
for thread in threads:
thread.start()
for _ in range(1000):
download_queue.put(object())
download_queue.close()
download_queue.join()
resize_queue.close()
resize_queue.join()
upload_queue.close()
upload_queue.join()
print '%s items finished' % done_queue.qsize()
Copy
要点:
管线是一种优秀的任务处理方式,它可以把处理流程划分为若干阶段,并使用多条python线程来同事执行这些任务;
构建并发式的管线时,要注意许多问题,其中包括:如何防止某个阶段陷入持续等待的状态之中、如何停止工作线程、以及如何防止内存膨胀等;
Queue类所提供的机制可以彻底解决上述问题,它具备阻塞式的队列操作、能够指定缓冲区尺寸,而且还支持join方法,这使得开发者可以构建出健壮的管线;
协程
建议6.3 建议使用协程来处理并发场景
Python程序员可以使用线程来运行多个函数,使这些函数看上去好像是在统一时间得到执行,然而,线程有其显著的缺点:
1.多线程的运行协调起来比单线程过程式困难,需要依赖Lock来保证自己的多线程逻辑正确。 2.每个在执行的线程,大约需要8MB内存,在线程处理逻辑较少而数量较多的工程模型中开销较大。 3.线程启动、切换线程上下文的开销较大。
Python的协程(coroutine)可以避免上述问题,它使得Python程序看上去好像是在同时运行多个函数,运行逻辑的调度由程序员自己决定。协程的实现方式,实际上是对生成器的一种扩展。启动生成器协程所需要的开销,与调用函数相仿。处于活跃状态的协程,在其耗尽之前,只会占用到不到1KB的内存。
示例代码: 下面这段代码展示了Python利用yield表达式、生成器来实现的原生协程写法。 grep函数功能为筛选包含关键字的输入。 在定义生成器之后,需要使用next()启动生成器,此时生成器函数,会运行到yield表达式处等待输入。在这之后我们通过生成器的send方法向生成器传递输入,生成器就可以接着yield表达式处向下处理,处理完成后依靠while语句再次等待在yield表达式处。
def grep(pattern): # 生成器函数
… print(“Searching for”, pattern)
… while True:
… line = (yield) # yield表达式,生成器函数接收send的输入
… if pattern in line:
… print(line)
…
generator = grep(“Python”) # 定义生成器
next(generator) # 使用next启动生成器之后,会运行到yield表达式
Searching for Python
generator.send(“I love Python.”) # 给生成器函数发送数据,继续yield处的运行
I love Python.
generator.send(“I love C++.”)
generator.send(“I love Java.”)
generator.send(“I love Python too.”)
I love Python too.
Copy
如果在以上代码的基础上,连续推进多个独立的生成器,即可模拟出Python线程的并发行为,令程序看上去好像是在同时运行多个函数,同时其消耗相比多线程程序开销要小。
并行
建议6.4 建议使用concurrent.futures实现并行计算
Python程序可以将独立的计算任务分配到多个CPU核上运行,提升并行计算的能力。Python的GIL使得无法使用线程实现真正的并行计算。Python程序的真正并行计算建议采用子进程的方式来实现,具体实现如下: 1.对于并行计算的任务与主进程之间传递的数据比较少,且任务之间不需要共享状态和变量时,采用concurrent.furures的ProcessPoolExecutor类的简单实用方式即可实现并行计算; 2.对于并行计算的场景不满足1)的状态时,可以采用multiprocessing模块提供的共享内存、进程锁、队列、代理等高级功能实现并行计算。因为使用复杂,所以如果不是特性场景,不建议使用这种方式;
使用concurrent.furures的ProcessPoolExecutor类实现并行计算的示例代码如下:
def calc_process():
start = time.time()
pool = ProcessPoolExecutor(max_workers=4)
results = list(pool.map(gcd, numbers))
end = time.time()
print(‘process calc, Took %.3f seconds’ % (end - start))
print(results)
Copy
7. 性能
建议7.1 在list成员个数可以预知的情况下,创建list时需预留空间正好容纳所有成员的空间
说明:与Java、C++等语言的list一样,Python语言的list在append()成员时,如果没有多余的空间容纳新的成员,就会分配一块更大的内存,并将原来内存里的成员拷贝到新的内存上,并将最新append()的成员也拷贝到此新内存空间中,然后释放老的内存空间。如果append()调用次数很大,则如上过程会频繁发生,因而会造成灾难性性能下降,而不仅仅是一点下降。
错误示例:
members = []
for i in range(1, 1000000):
members.append(i)
len(members)
Copy
正确示例:
members = [None] * 1000000
for i in range(1, 1000000):
members[i] = i
len(members)
Copy
建议7.2 在成员个数及内容皆不变的场景下尽量使用tuple替代list
说明:list是动态array,而tuple是静态array(其成员个数以及内容皆不可变)。因此,list需要更多的内存来跟踪其成员的状态。
此外,对于成员个数小于等于20的tuple,Python会对其进行缓存,即当此tuple不再使用时,Python并不会立即将其占用的内存返还给操作系统,而是保留以备后用。
错误示例:
myenum = [1, 2, 3, 4, 5]
Copy
正确示例:
myenum = (1, 2, 3, 4, 5) # 如果恰好被缓存过,则初始化速度会为错误示例中的5倍以上。
Copy
建议7.3 对于频繁使用的外界对象,尽量使用局部变量来引用之
说明:在Python中对一个函数、变量、模块的调用,是以一种字典树的方式来查找的。Python首先会查找locals()数组,这里保存着所有的局部变量;如果找不到,则会继续查找globals()数组;如果在这里也找不到,则会到buildtin(其实是一个模块)中的locals()数组中查找,或者到其它import进来的模块/类中查找。
错误示例:
import math
def afunc():
for x in xrange(100000):
return math.tan(x)
Copy
在这个例子中,Python会先到globals()中的名值对字典中,找到math模块;然后在math模块的locals()的字典中查找tan()函数;然后在当前函数的locals()中查找x。这里存在着3次查找。
当调用次数大时,每次调用多出来的1、2次查找,就会被放大。
错误示例:
from math import tan
def afunc():
for x in xrange(100000):
return tan(x)
Copy
在这个例子中,Python会先到globals()的字典中查找tan()函数(其已经被from math import tan语句加载到了globals()中);然后在当前函数的locals()中查找x。这里存在着2次查找,比前一个例子少了一次查找,但是还不是最优解。
正确示例:
import math
def afunc(tan=math.tan):
for x in xrange(100000):
return tan(x)
Copy
在这个例子中,在函数定义时,有且只有一次查找math模块、然后查找tan函数的操作;之后在循环中对tan()函数的调用,都是在afunc()函数的locals()中进行查找,而对函数的locals()中的查找,Python是有特殊优化措施的,速度是非常快的;当然,还包括对本地变量x的查找(也是在当前函数的locals()中查找)。
建议7.4 Python2.x中使用xrange代替range
说明:在Python中,for x in range(1, 10000),等价于Java/C/C++中的for (int i = 0; i < 10000; i++),非常常用。但是,range其实等价于如下定义:
def range(begin, end, step=1):
indices = []
while begin < end:
indices.append(begin)
start += step
Copy
在此过程中,indices数组可能会有内存效率问题,使得range构造枚举值的代价高昂。 xrange等价于如下定义:
def range(begin, end, step=1):
while begin < end:
yield begin
start += step
Copy
在这个过程中,并没有一个大数组的生成。每次for循环都只会向xrange要一个数据,xrange拥有且也只有一个当前数据给for循环。因此,空间占用非常小,并且还不存扩容时在分配新内存、拷贝老内存内容、释放老内存的操作步骤。
错误示例:
for x in range(1, 1000000):
print x
Copy
正确示例:
for x in xrange(1, 1000000):
print x
Copy
建议7.5 尽量使用generator comprehension代替list comprehension
说明:list comprehension可以用来代替lambda表达式的map、reduce语法,从已有的list中,生成新的数据。而generator comprehension无需定义一个包含yield语句的函数,就可以生成一个generator。 二者一个生成list,另外一个生成generator,在内存的占用上,相差悬殊;在生成速度上,相差无几。
错误示例:
even_cnt = len([x for x in range(10) if x % 2 == 0])
Copy
正确示例:
even_cnt = sum(1 for x in range(10) if x % 2 == 0)
Copy
建议7.6 使用format方法、"%“操作符和join方法代替”+“和”+="操作符来完成字符串格式化
说明:即使参数都是字符串,也可以使用format方法或%运算符来格式化字符串。一般性能要求的场景可以使用+或+=运算符,但需要避免使用+和+=运算符在循环中累积字符串。由于字符串是不可变的,因此会产生不必要的临时对象并导致二次而非线性运行时间。
推荐做法:
x = ‘%s, %s!’ % (imperative, expletive)
x = ‘{}, {}!’.format(imperative, expletive)
x = ‘name: %s; score: %d’ % (name, n)
x = ‘name: {}; score: {}’.format(name, n)
items = [’
’]
for last_name, first_name in employee_list:
items.append(’’ % (last_name, first_name))
items.append(’
%s, %s |
’)
employee_table = ‘’.join(items)
Copy
不推荐做法:
x = imperative + ', ’ + expletive + ‘!’
x = 'name: ’ + name + '; score: ’ + str(n)
employee_table = ‘
’
for last_name, first_name in employee_list:
employee_table += ‘’ % (last_name, first_name)
employee_table += ‘
%s, %s |
’
Copy
8. 兼容
建议8.1 按Python官方文档的规定处理Python2.x和Python3.x之间的区别
说明:Python2.x和 Python3.x 在基本类型、语句、运算符等方面都有差异,并且差异非常大,所以在编写兼容的Python脚本时需要关注2.x与3.x不同之处的处理。具体可以参考python官方网站中的每个python 3版本的“What’s New” 文档和在线免费书籍 “Porting to Python 3” 。
错误示例:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int(sys.argv[1]))
except KeyError as e:
print(‘key error’)
except ValueError as e:
print(‘value error’)
print(e) # python2 能够成功执行,但是python3 会报错
bad()
Copy
在Python 2运行结果:
$ python foo.py 1
key error
1
$ python foo.py 2
value error
2
Copy
但是在Python 3里:
$ python3 foo.py 1
key error
Traceback (most recent call last):
File “foo.py”, line 19, in
bad()
File “foo.py”, line 17, in bad
print(e)
UnboundLocalError: local variable ‘e’ referenced before assignment
Copy
正确示例:
import sys
def bar(i):
if i == 1:
KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int(sys.argv[1]))
except KeyError as e:
exception = e
print(‘key error’)
except ValueError as e:
exception = e
print(‘value error’)
print(exception)
good()
Copy
在Python 3中运行结果:
$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2
Copy
9. 编程实践
规则9.1 函数参数中的可变参数不要使用默认值,在定义时使用None
说明:参数的默认值会在方法定义被执行时就已经设定了,这就意味着默认值只会被设定一次,当函数定义后,每次被调用时都会有"预计算"的过程。当参数的默认值是一个可变的对象时,就显得尤为重要,例如参数值是一个list或dict,如果方法体修改这个值(例如往list里追加数据),那么这个修改就会影响到下一次调用这个方法,这显然不是一种好的方式。应对种情况的方式是将参数的默认值设定为None。
错误示例:
def foo(bar=[]): # bar is optional and defaults to [] if not specified
… bar.append(“baz”) # but this line could be problematic, as we’ll see…
… return bar
Copy
在上面这段代码里,一旦重复调用foo()函数(没有指定一个bar参数),那么将一直返回’bar’。因为没有指定参数,那么foo()每次被调用的时候,都会赋予[]。下面来看看,这样做的结果:
foo()
[“baz”]
foo()
[“baz”, “baz”]
foo()
[“baz”, “baz”, “baz”]
Copy
正确示例:None是不错的选择
def foo(bar=None):
… if bar is None: # or if not bar:
… bar = []
… bar.append(“baz”)
… return bar
…
foo()
[“baz”]
foo()
[“baz”]
foo()
[“baz”]
Copy
规则9.2 对子类继承的变量要做显式定义和赋初值
说明:在Python中,类变量都是作为字典进行内部处理的,并且遵循方法解析顺序(MRO)。子类没有定义的属性会引用基类的属性值,如果基类的属性值发生变化,对应的子类引用的基类的属性的值也相应发生了变化。
错误示例:
class A(object):
x = 1
class B(A):
pass
class C(A):
pass
B.x = 2
print A.x, B.x, C.x
1 2 1
A.x = 3
print A.x, B.x, C.x
3 2 3
Copy
这里虽然没有给C.x赋值,但是由于基类的值A.x发生改变,在获取C.x的值得时候发现它引用的数据发生了变化。在上面这段代码中,因为属性x没有在类C中发现,它会查找它的基类(在上面例子中只有A,尽管Python支持多继承)。换句话说,就是C自己没有x属性,因此,引用C.x其实就是引用A.x。
正确示例:如果希望类C中的x不引用自A类,可以在C类中重新定义属性X,这样C类的就不会引用A类的属性x了,值的变化就不会相互影响。
class B(A):
x = 2
class C(A):
x = -1
print A.x, B.x, C.x
1 2 -1
A.x = 3
print A.x, B.x, C.x
3 2 -1
Copy
规则9.3 严禁使用注释行等形式仅使功能失效
说明:python的注释包含:单行注释、多行注释、代码间注释、doc string等。除了doc string是使用""""""括起来的多行注释,常用来描述类或者函数的用法、功能、参数、返回等信息外,其余形式注释都是使用#符号开头用来注释掉#后面的内容。基于python语言运行时编译的特殊性,如果在提供代码的时候提供的是py文件,即便是某些函数和方法在代码中进行了注释,别有用心的人依然可以通过修改注释来使某些功能启用;尤其是某些接口函数,如果不在代码中进行彻底删除,很可能在不知情的情况下就被启用了某些本应被屏蔽的功能。因此根据红线要求,在python中不使用的功能、模块、函数、变量等一定要在代码中彻底删除,不给安全留下隐患。即便是不提供源码py文件,提供编译过的pyc、pyo文件,别有用心的人可以通过反编译来获取源代码,可能会造成不可预测的结果。
错误示例:在main.py中有两个接口被注释掉了,但是没有被删除。
if name == “main”:
if sys.argv[1].startswith(’–’):
option = sys.argv[1][2:]
if option == “load”:
#安装应用
LoadCmd(option, sys.argv[2:3][0])
elif option == ‘unload’:
#卸载应用
UnloadCmd(sys.argv[2:3][0])
elif option == ‘unloadproc’:
#卸载流程
UnloadProcessCmd(sys.argv[2:3][0])
elif option == ‘active’:
ActiveCmd(sys.argv[2:3][0])
elif option == ‘inactive’:
InActiveCmd(sys.argv[2:3][0])
else:
Loginfo("Command %s is unknown"%(sys.argv[1]))
Copy
在上例中很容易让其他人看到我们程序中的两个屏蔽的接口,容易造成不安全的因素,注释的代码应该删除。
if name == “main”:
if sys.argv[1].startswith(’–’):
option = sys.argv[1][2:]
if option == “load”:
#安装应用
LoadCmd(option, sys.argv[2:3][0])
elif option == ‘unload’:
#卸载应用
UnloadCmd(sys.argv[2:3][0])
elif option == ‘unloadproc’:
#卸载流程
UnloadProcessCmd(sys.argv[2:3][0])
else:
Loginfo(“Command %s is unknown”%(sys.argv[1]))
Copy
建议9.4 慎用copy和 deepcopy
说明:在python中,对象赋值实际上是对象的引用。当创建一个对象,然后把它赋给另一个变量的时候,python并没有拷贝这个对象,而只是拷贝了这个对象的引用。如果需要拷贝对象,需要使用标准库中的copy模块。copy模块提供copy和deepcopy两个方法:
copy浅拷贝:拷贝一个对象,但是对象的属性还是引用原来的。对于可变类型,比如列表和字典,只是复制其引用。基于引用所作的改变会影响到被引用对象。
deepcopy深拷贝:创建一个新的容器对象,包含原有对象元素(引用)全新拷贝的引用。外围和内部元素都拷贝对象本身,而不是引用。
Notes:对于数字,字符串和其他原子类型对象等,没有被拷贝的说法。如果对其重新赋值,也只是新创建一个对象,替换掉旧的而已。使用copy和deepcopy时,需要了解其使用场景,避免错误使用。
示例:
import copy
a = [1, 2, [‘x’, ‘y’]]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
a.append(3)
a[2].append(‘z’)
a.append([‘x’, ‘y’])
print a
[1, 2, [‘x’, ‘y’, ‘z’], 3, [‘x’, ‘y’]]
print b
[1, 2, [‘x’, ‘y’, ‘z’], 3, [‘x’, ‘y’]]
print c
[1, 2, [‘x’, ‘y’, ‘z’]]
print d
[1, 2, [‘x’, ‘y’]]
Copy
规则9.5 使用os.path库中的方法代替字符串拼接来完成文件系统路径的操作
说明:os.path库实现了一系列文件系统路径操作方法,这些方法相比单纯的路径字符串拼接来说更为安全,而且为用户屏蔽了不同操作系统之间的差异。
错误示例:如下路径字符串的拼接在Linux操作系统无法使用
path = os.getcwd() + ‘\test.txt’
Copy
正确示例:
path = os.path.join(os.getcwd(), ‘test.txt’)
Copy
建议9.6 使用subprocess模块代替os.system模块来执行shell命令
说明:subprocess模块可以生成新进程,连接到它们的input/output/error管道,并获取它们的返回代码。该模块旨在替换os.system等旧模块,相比os.system模块来说更为灵活。
推荐做法:
subprocess.run([“ls”, “-l”]) # doesn’t capture output
CompletedProcess(args=[‘ls’, ‘-l’], returncode=0)
subprocess.run(“exit 1”, shell=True, check=True)
Traceback (most recent call last):
…
subprocess.CalledProcessError: Command ‘exit 1’ returned non-zero exit status 1
subprocess.run([“ls”, “-l”, “/dev/null”], capture_output=True)
CompletedProcess(args=[‘ls’, ‘-l’, ‘/dev/null’], returncode=0,
stdout=b’crw-rw-rw- 1 root root 1, 3 Jan 23 16:23 /dev/null\n’, stderr=b’’)
Copy
建议9.7 建议使用with语句操作文件
说明:Python 对一些内建对象进行改进,加入了对上下文管理器的支持,可以用于 with 语句中。使用 with 语句可以自动关闭文件,减少文件读取操作错误的可能性,在代码量和健壮性上更优。注意 with 语句要求其操作的类型实现"enter()"和"exit()"方法,需确认实现后再使用。
推荐做法: