下面的代码来自我们某一工具源码中:
file_gz = gzip.GzipFile(file_name) src_path, src_file = os.path.split(file_name) tmp_file_name = os.path.join(path_name, src_file).strip('gz').strip('.') tmp_file = open(tmp_file_name, 'wb') tmp_file.writeline(file_gz.realines()) file_gz.close() tmp_file.close() os.remove(file_name)
从代码健壮角度来看,存在如下两个问题:
能正确释放资源的建议写法是:
src_path, src_file = os.path.split(file_name) dst_file_name = os.path.join(path_name, src_file).strip('gz').strip('.') with gzip.GzipFile(file_name) as src_gz_file, open(dst_file_name, 'wb') as out_file: out_file.writeline(src_gz_file.realines()) os.remove(file_name)
还有一种写法,采用 try-except-finally
,在 finally
中对打开的文件关闭掉,但这种写法的代码显得臃肿。所以Python又提供上述示例中 with
语句写法。
with
语句启用了上下文管理器,标准库中 contextlib
模块包含用于处理上下文管理器一些工具。
上下文管理器涉及两种方法:
__enter__()
方法, 返回要在上下文中使用的对象 with
块时,运行 __exit__()
方法来清理正在使用的任何资源
对于任何一个对象能够使用 with
语句来清理资源,只要像下面来提供 __enter__()
方法与 __exit__()
方法:
class Context: def __enter__(self): print('__enter__()') return self def __exit__(self, exc_type, exc_val, exc_tb): print('__exit__()') with Context(): print('do somethon in the context')
file
类内嵌支持上下文管理器API,但有些历史遗留下的其他对象并不支持,标准库文档中给出的 contextlib
示例是 urllib.urlopen()
返回的对象。还有其他遗留类使用 close()
方法,但不支持上下文管理器API。要确保资源已关闭,要使用 closing()
为其创建上下文管理器。
class Resurce: def __init__(self): print('__init__()') self.status = 'open' def close(self): print('close()') self.status = 'closed' with contextlib.closing(Resurce()) as r: print('inside with statement: {}'.format(r.status))
另外 contextlib
还提供了装饰器来简化上下文管理器相关场景的代码开发。这里不展开讲了,有兴趣的同学找资料研究吧。
对于资源的释放是所有编程语言都要解决的问题,举一反三,我们再来看看其它语言的一些玩法。
在Java1.7之前,是采用 try-catch-finally
的方式解决:
BufferedInputStream bin = null; BufferedOutputStream bout = null; try { bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt"))); int br = -1; while ((br = bin.read()) != -1) { bout.write(br); } } catch (IOException e) { log.error("...."); } finally { if (bin != null) { try { bin.close(); } catch (IOException e) { log.error("....") } } if (bout != null) { try { bout.close(); } catch (IOException e) { log.error("...."); } } }
上面的代码是不是不够简洁?关闭资源也要 try-catch
,否则会导致一个close未被执行。Java 1.7中新增的 try-with-resource
语法糖,简化的代码就成了如下:
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) { int br = -1; while ((br = bin.read()) != -1) { bout.write(br); } } catch (IOException e) { log.error("...."); }
与Python的 with
语句真是异曲同工,为了能够配合 try-with-resource
,资源必须实现AutoClosable接口。
如果熟悉lombok库的同学,也会知道有个 @Cleanup
注解,它会帮你安全的调用close方法来释放资源,相比Java内建的 try-with-resource
语法糖,它还可以调用非 close
方法。 @Cleanup(“dispose”)
,通过指定方法名来调用相应的方法来清理资源。不过约束是被调用的方法要求是无参数方法。
无论是 try-with-resource
,还是lombok的 @Cleanup
注解,他们都是语法糖,通过编译帮你生成的字节码在finally中调用close方法来释放资源。
作为后起之秀的Go,对于资源释放的解决方法,相比Python与Java来得优雅些。它提供了 defer
关键字:
src, err := os.Open(srcFile) if err != nil { return } defer src.Close()
defer
的底层实现是: defer
后面的表达式会被放入一个列表中,在当前方法返回的时候,列表中的表达式就会被执行。采用栈数据结构,一个方法中,当存在多个 defer
语句时,先加入列表则后执行。
当然,由于defer后面可以跟匿名函数块,如:
func test() int { i := 0 defer func (){ i++ fmt.Println("defer2:", i) // 打印结果为 defer2: 2 }() defer func (){ i++ fmt.Println("defer1:", i) // 打印结果为 defer1: 1 }() return i // 假如返回值是a,此时a=i,defer中修改i的值不会影响返回值a,defer也根本访问不到a }
若是像上面代码在 defer
的函数中有使用前面的变量并对它进行修改,则引入了复杂性。有兴趣的同学的不烦再对 defer
深挖一下。是不是像Java一样要求,不要在finally中修改基本类型或对象中的值的既视感?
再来一个例子,返回值有名:
func test() (i int) { i = 5 defer func() { i++ fmt.Println("defer2:", i) // 打印结果为 defer2: 7 }() defer func() { i++ fmt.Println("defer1:", i) // 打印结果为 defer1: 6 }() return i // 返回的结果是几? }
它的返回值又是什么,还有更多的defer坑等你去发现哦。
C++其实在资源管理上是最为成熟,RAII技术被认为是C++中管理资源的最佳方法。 RAII是C++的发明者Bjarne Stroustrup老爷子提出的概念,RAII全称是 Resource Acquisition is Initialization
,直译过来是 资源获取即初始化
,也就是说在构造函数中申请分配资源,在析构函数中释放资源。
智能指针(std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。内存只是资源的一种,如对于文件的打开与关闭,也可以使用RAII来解决,不过有点麻烦,按照常规的RAII技术需要写一堆管理它们的类。
不过C++11有lambda表达式,结合std::function,我们可以利用RAII机制完美地模拟Go的 defer
:
#define SCOPEGUARD_LINENAME_CAT(name, line) name##line #define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line) #define DEFER(callback) ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback) class ScopeGuard { public: explicit ScopeGuard(std::function<void()> f) : handleExitScope(f){}; ~ScopeGuard(){ handleExitScope(); } private: std::function<void()> handleExitScope; }; { std::ofstream file("test.txt"); DEFER([&] { file.close(); }); }
上面的代码看起来是不是很Clean,妈妈再不用担心我的代码出现资源泄露了^_^。
程序使用的资源,不仅仅是内存。在内存管理方面,有垃圾回归器的语言帮程序员省了很多事。但广义上资源还有文件,流,连接,锁等等,这些都需要开发者手动关闭他们,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故。我们也许会记得在正常流程中关闭这些资源,却可能经常忽视了异常场景,我们应该利用语言中最新的特性,既使代码Clean,能又能确保资源被正常释放。