本章分析TensorFlow的高层模块Estimator.
TF的高层模块Estimator是采用Python语言开发的,在分析此模块之前先普及一下Python语法,本文不准备作为Python的入门教程,所以只列举几个TF中广泛使用但又不是很好理解的Python语法。
Python
列表生成式
1、基本语法格式是:
v = [exp for iter_var int iterable]
等价于:
v = []
for iter_var in interable:
v.append(exp)
2、含过滤功能的语法格式:
v = [exp for iter_var in iterable if_exp]
等价于
v=[]
for iter_var in iterable:
if if_exp:
v.append(exp)
3、嵌套语法格式
v = [exp for iter_var_a in iterable_a for iter_var_b in iterable_b]
等价于
v=[]
for iter_var_a in iterable_a:
for iter_var_b int iterable_b:
v.append(exp)
yield 关键字
例如我们需要生成一个随机数数组,很自然的实现方式如下:
def random_array(len):
a = []
while len > 0:
a.append(random.randint(0, 9))
len -= 1
return a
##打印生成的随机数数组
for r in random_array(10):
print(r)
但是这个函数有个问题,那就是当len值比较大的时候,会占用很大一块内存,可以通过可迭代对象来改进内存占用问题,得到下面的版本:
class random_array(object):
def __init__(self, len):
self.len = len
def __iter__(self):
return self
def next(self):
if self.len > 0:
self.len -= 1
return random.randint(0, 9)
else:
raise StopIteration()
##打印生成的随机数数组
for r in random_array(10):
print(r)
这个版本的random_array不会有长度为len的数组产生,也就避免的内存占用的问题。但是这个版本用了申明了一个类来完成这个功能,代码比较复杂,使用yield关键字,得到下面这个最终版本:
def random_array(len):
while len > 0:
yield random.randint(0, 9)
len -= 1
##打印生成的随机数数组
for r in random_array(10):
print(r)
这个版本的代码看起来十分的简洁,我来解释一下发生了什么:
- 包含yield关键的函数与普通函数区别很大,在它被调用的时候,它的代码不会被执行,而是生成一个称为生成器(generator)的对象. 例如,上面for代码中的random_array(10)不会执行random_array函数的代码,而是生成了一个生成器对象。
- 生成器对象都是可迭代的,也就说生成器对象实现了迭代器所要求的两个方法 和 .
- 每次调用生成器的next的方法会执行生成器函数(例如上面的函数random_array)的代码,直到遇到一个yield关键字则本次next函数执行结束,返回yield后面的值。
- 下次next函数的调用,会从上一次的yield语句的后面开始执行。如果某一次的执行没有了yiled语句,则会产生一个异常,结束for语句,例如下面的代码在打印了10个随机数之后会抛出一个StopIteration异常:
r = random_array(10)
for i in xrange(20):
print(r.next())
输出结果为:
8
0
2
1
2
6
7
2
1
5
Traceback (most recent call last):
File "example.py", line 36, in <module>
print(r.next())
StopIteration
生成器
上面的包含yield关键字的函数,就是构造生成器的方式之一,还有另一种方式就是通过类似列表生成式的方式生成。例如:
generator = (x*x for x in xrange(10))
with 语句
with语句的语法格式为:
with context_exp [as target]:
with_body
- 这里的context_exp需要返回一个上下文管理器(Context Manager)对象
- 上下文管理器(Context Manager)对象需要需要实现__enter__()和__exit__()两个方法。
- 如果包含as关键字,则target=ContextManager.__enter__(), 即上下文管理对象的__enter__()方法的返回值会赋值给target.
具体来说,以上的with语句等价于下面的语句:
context_manager = context_expression
exit = type(context_manager).__exit__
value = type(context_manager).__enter__(context_manager)
exc = True # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理
try:
try:
target = value # 如果使用了 as 子句
with-body # 执行 with-body
except:
# 执行过程中有异常发生
exc = False
# 如果 __exit__ 返回 True,则异常被忽略;如果返回 False,则重新抛出异常
# 由外层代码对异常进行处理
if not exit(context_manager, *sys.exc_info()):
raise
finally:
# 正常退出,或者通过 statement-body 中的 break/continue/return 语句退出
# 或者忽略异常退出
if exc:
exit(context_manager, None, None, None)
# 缺省返回 None,None 在布尔上下文中看做是 False
装饰器Decorator
装饰器本身也是一个函数,在不修改被装饰函数代码的前题下,扩展被装饰函数的功能。我们来直接看例子:
def sum(x, y):
reutrn x + y
print 'sum=%d' % sum(1,2)
代码输出如下:
sum=3
现在需要添加一个功能,打印出sum的参数x和y,但是不能修改sum的代码,我们可以这样来做:
def logging_sum(f):
def inner_sum(x, y):
print('x=%d, y=%d' % (x, y))
return f(x, y)
return inner_sum
print 'sum=%d' % logging_sum(sum)(1, 2)
代码的输出结果如下:
x=1, y=2
sum=3
这里我们虽然没有修改sum函数,但是修改了函数的调用代码:sum(1,2)改成了logging_sum(sum)(1,2),那么有没有办法不修改函数调用代码呢? 答案是有:
def logging_sum(f):
def inner_sum(x, y):
print('x=%d, y=%d' % (x, y))
return f(x, y)
return inner_sum
@logging_sum
def sum(x, y):
return x + y
print 'sum=%d' % sum(1, 2)
代码是输出结果为:
x=1, y=2
sum=3
这就是装饰器的语法。
结合应用
TF中经常可以看到下面形式的代码:
from contextlib import contextmanager
@contextmanager
def managed_resource(*args, **kwds):
# Code to acquire resource, e.g.:
resource = acquire_resource(*args, **kwds)
try:
yield resource
finally:
# Code to release resource, e.g.:
release_resource(resource)
with managed_resource(timeout=3600) as resource:
with_body
基于上面基础只是的介绍,我们来看一下with语句中发生了什么;
- contextmanager装饰器将managed_resource函数生成的生成器对象封装成一个上下文管理对象。
- 此上下文管理对象的__enter__()函数会调用生成器对象的next方法,导致managed_resource函数的开始到yield语句的代码被执行,也就进行了资源的分配。
- 次上下文广利对象的__exit__()函数会再次调用生成器对象的next方法,导致managed_resource函数的finally后的语句被执行,也就进行了相关资源的释放。
- 简单来说也就是,我们需要把普通上下文管理器对象的__enter__()函数需要完成的工作放到yield语句之前,而将__exit__()函数需要完成的工作放到yield语句之后。