Python高级特性(1)

来源:互联网 发布:mac的照片文件夹 编辑:程序博客网 时间:2024/05/22 15:51

Python高级特性(1):Iterators、Generators和itertools

本文由 伯乐在线 - 熊崽Kevin 翻译自 Sahand Saba。欢迎加入技术翻译小组。转载请参见文章末尾处的要求。

【译注】:作为一门动态脚本语言,Python对编程初学者而言很友好,丰富的第三方库能够给使用者带来很大的便利。而Python同时也能够提供一些高级的特性方便用户使用更为复杂的数据结构。本系列文章共有三篇,本文是系列的第一篇,将会介绍迭代器、生成器以及itertools模块的相关用法。由于作者 Sahand Saba 列举的示例中有诸多专业的数学相关内容,因此翻译中有诸多不妥之处请大家指出,非常感谢。

对数学家来说,Python这门语言有着很多吸引他们的地方。举几个例子:对于tuple、lists以及sets等容器的支持,使用与传统数学类似的符号标记方式,还有列表推导式这样与数学中集合推导式和集的结构式(set-builder notation)很相似的语法结构。

另外一些很吸引数学爱好者的特性是Python中的iterator(迭代器)、generator(生成器)以及相关的itertools包。这些工具帮助人们能够很轻松的写出处理诸如无穷序列(infinite sequence)、随机过程(stochastic processes)、递推关系(recurrence relations)以及组合结构(combinatorial structures)等数学对象的优雅代码。本文将涵盖我关于迭代器和生成器的一些笔记,并且有一些我在学习过程中积累的相关经验。

Iterators

迭代器(Iterator)是一个可以对集合进行迭代访问的对象。通过这种方式不需要将集合全部载入内存中,也正因如此,这种集合元素几乎可以是无限的。你可以在Python官方文档的“迭代器类型(Iterator Type)”部分找到相关文档。

让我们对定义的描述再准确些,如果一个对象定义了__iter__方法,并且此方法需要返回一个迭代器,那么这个对象就是可迭代的(iterable)。而迭代器是指实现了__iter__以及next(在Python 3中为__next__)两个方法的对象,前者返回一个迭代器对象,而后者返回迭代过程的下一个集合元素。据我所知,迭代器总是在__iter__方法中简单的返回自己(self),因为它们正是自己的迭代器。

一般来说,你应该避免直接调用__iter__以及next方法。而应该使用for或是列表推导式(list comprehension),这样的话Python能够自动为你调用这两个方法。如果你需要手动调用它们,请使用Python的内建函数iter以及next,并且把目标迭代器对象或是集合对象当做参数传递给它们。举个例子,如果c是一个可迭代对象,那么你可以使用iter(c)来访问,而不是c.__iter__(),类似的,如果a是一个迭代器对象,那么请使用next(a)而不是a.next()来访问下一个元素。与之相类似的还有len的用法。

说到len,值得注意的是对迭代器而言没必要去纠结length的定义。所以它们通常不会去实现__len__方法。如果你需要计算容器的长度,那么必须得手动计算,或者使用sum。本文末,在itertools模块之后会给出一个例子。

有一些可迭代对象并不是迭代器,而是使用其他对象作为迭代器。举个例子,list对象是一个可迭代对象,但并不是一个迭代器(它实现了__iter__但并未实现next)。通过下面的例子你可以看到list是如何使用迭代器listiterator的。同时值得注意的是list很好地定义了length属性,而listiterator却没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> a =[1,2]
>>>type(a)
<type'list'>
>>>type(iter(a))
<type'listiterator'>
>>> it =iter(a)
>>>next(it)
1
>>>next(it)
2
>>>next(it)
Traceback (most recent call last):
  File"<stdin>", line 1,in<module>
StopIteration
>>>len(a)
2
>>>len(it)
Traceback (most recent call last):
  File"<stdin>", line 1,in<module>
TypeError:objectof type'listiterator' has no len()

当迭代结束却仍然被继续迭代访问时,Python解释器会抛出StopIteration异常。然而,前述中提到迭代器可以迭代一个无穷集合,所以对于这种迭代器就必须由用户负责确保不会造成无限循环的情况,请看下面的例子:

1
2
3
4
5
6
7
8
9
10
classcount_iterator(object):
    n=0
 
    def__iter__(self):
        returnself
 
    defnext(self):
        y=self.n
        self.n+=1
        returny

下面是例子,注意最后一行试图将一个迭代器对象转为list,这将导致一个无限循环,因为这种迭代器对象将不会停止。

1
2
3
4
5
6
7
8
9
10
>>> counter =count_iterator()
>>>next(counter)
0
>>>next(counter)
1
>>>next(counter)
2
>>>next(counter)
3
>>>list(counter) # This will result in an infinite loop!

最后,我们将修改以上的程序:如果一个对象没有__iter__方法但定义了__getitem__方法,那么这个对象仍然是可迭代的。在这种情况下,当Python的内建函数iter将会返回一个对应此对象的迭代器类型,并使用__getitem__方法遍历list的所有元素。如果StopIteration或IndexError异常被抛出,则迭代停止。让我们看看以下的例子:

1
2
3
4
5
6
classSimpleList(object):
    def__init__(self,*items):
        self.items=items
 
    def__getitem__(self, i):
        returnself.items[i]

用法在此:

1
2
3
4
5
6
7
8
9
10
11
12
>>> a =SimpleList(1,2,3)
>>> it =iter(a)
>>>next(it)
1
>>>next(it)
2
>>>next(it)
3
>>>next(it)
Traceback (most recent call last):
  File"<stdin>", line 1,in<module>
StopIteration

现在来看一个更有趣的例子:根据初始条件使用迭代器生成Hofstadter Q序列。Hofstadter在他的著作《Gödel, Escher, Bach: An Eternal Golden Braid》中首次提到了这个嵌套的序列,并且自那时候开始关于证明这个序列对所有n都成立的问题就开始了。以下的代码使用一个迭代器来生成给定n的Hofstadter序列,定义如下:

Q(n)=Q(n-Q(n-1))+Q(n−Q(n−2))

给定一个初始条件,举个例子,qsequence([1, 1])将会生成H序列。我们使用StopIteration异常来指示序列不能够继续生成了,因为需要一个合法的下标索引来生成下一个元素。例如如果初始条件是[1,2],那么序列生成将立即停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
classqsequence(object):
    def__init__(self, s):
        self.s=s[:]
 
    defnext(self):
        try:
            q=self.s[-self.s[-1]]+self.s[-self.s[-2]]
            self.s.append(q)
            returnq
        exceptIndexError:
            raiseStopIteration()
 
    def__iter__(self):
        returnself
 
    defcurrent_state(self):
        returnself.s

用法在此:

1
2
3
4
5
6
7
>>> Q =qsequence([1,1])
>>>next(Q)
2
>>>next(Q)
3
>>> [next(Q)for__ inxrange(10)]
[3,4,5,5,6,6,6,8,8,8]

Generators

生成器(Generator)是一种用更简单的函数表达式定义的生成器。说的更具体一些,在生成器内部会用到yield表达式。生成器不会使用return返回值,而当需要时使用yield表达式返回结果。Python的内在机制能够帮助记住当前生成器的上下文,也就是当前的控制流和局部变量的值等。每次生成器被调用都适用yield返回迭代过程中的下一个值。__iter__方法是默认实现的,意味着任何能够使用迭代器的地方都能够使用生成器。下面这个例子实现的功能同上面迭代器的例子一样,不过代码更紧凑,可读性更强。

1
2
3
4
5
defcount_generator():
   n=0
   whileTrue:
     yieldn
     n+=1

来看看用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> counter =count_generator()
>>> counter
<generatorobjectcount_generator at 0x106bf1aa0>
>>>next(counter)
0
>>>next(counter)
1
>>>iter(counter)
<generatorobjectcount_generator at 0x106bf1aa0>
>>>iter(counter)iscounter
True
>>>type(counter)
<type'generator'>

现在让我们尝试用生成器来实现Hofstadter’s Q队列。这个实现很简单,不过我们却不能实现前的类似于current_state那样的函数了。因为据我所知,不可能在外部直接访问生成器内部的变量状态,因此如current_state这样的函数就不可能实现了(虽然有诸如gi_frame.f_locals这样的数据结构可以做到,但是这毕竟是CPython的特殊实现,并不是这门语言的标准部分,所以并不推荐使用)。如果需要访问内部变量,一个可能的方法是通过yield返回所有的结果,我会把这个问题留作练习。

1
2
3
4
5
6
7
8
9
defhofstadter_generator(s):
    a=s[:]
    whileTrue:
        try:
            q=a[-a[-1]]+a[-a[-2]]
            a.append(q)
            yieldq
        exceptIndexError:
            return

请注意,在生成器迭代过程的结尾有一个简单的return语句,但并没有返回任何数据。从内部来说,这将抛出一个StopIteration异常。

下一个例子来自Groupon的面试题。在这里我们首先使用两个生成器来实现Bernoulli过程,这个过程是一个随机布尔值的无限序列,True的概率是p而False的概率为q=1-p。随后实现一个von Neumann extractor,它从Bernoulli process获取输入(0<p<1),并且返回另一个Bernoulli process(p=0.5)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
importrandom
 
defbernoulli_process(p):
    ifp > 1.0or p < 0.0:
        raiseValueError("p should be between 0.0 and 1.0.")
 
    whileTrue:
        yieldrandom.random() < p
 
defvon_neumann_extractor(process):
    whileTrue:
        x, y =process.next(), process.next()
        ifx !=y:
            yieldx

最后,生成器是一种生成随机动态系统的很有利的工具。下面这个例子将演示著名的帐篷映射(tent map)动态系统是如何通过生成器实现的。(插句题外话,看看数值的不准确性是如何开始关联变化并呈指数式增长的,这是一个如帐篷映射这样的动态系统的关键特征)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
>>>deftent_map(mu, x0):
...    x =x0
...   whileTrue:
...       yieldx
...        x =mu *min(x,1.0- x)
...
>>>
>>> t =tent_map(2.0,0.1)
>>>for__ inxrange(30):
...   printt.next()
...
0.1
0.2
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.8
0.4
0.799999999999
0.400000000001
0.800000000003
0.399999999994
0.799999999988
0.400000000023
0.800000000047
0.399999999907
0.799999999814
0.400000000373
0.800000000745
0.39999999851
0.79999999702

另一个相似的例子是Collatz序列。

1
2
3
4
5
defcollatz(n):
   yieldn
   whilen !=1:
     n=n /2 if n %2 ==0 else 3 * n +1
     yieldn

请注意在这个例子中,我们仍旧没有手动抛出StopIteration异常,因为它会在控制流到达函数结尾的时候自动抛出。

请看用法:

1
2
3
4
5
6
7
8
9
10
>>># If the Collatz conjecture is true then list(collatz(n)) for any n will
...# always terminate (though your machine might run out of memory first!)
>>>list(collatz(7))
[7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1]
>>>list(collatz(13))
[13,40,20,10,5,16,8,4,2,1]
>>>list(collatz(17))
[17,52,26,13,40,20,10,5,16,8,4,2,1]
>>>list(collatz(19))
[19,58,29,88,44,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1]

Recursive Generators

生成器可以像其它函数那样递归。让我们来看一个自实现的简单版本的itertools.permutations,这个生成器通过给定一个item列表生成其全排列(在实际中请使用itertools.permutations,那个实现更快)。基本思想很简单:对列表中的每一个元素,我们通过将它与列表第一个元素交换将其放置到第一的位置上去,而后重新递归排列列表的剩余部分。

1
2
3
4
5
6
7
8
9
defpermutations(items):
    iflen(items)==0:
        yield[]
    else:
        pi=items[:]
        fori inxrange(len(pi)):
            pi[0], pi[i] =pi[i], pi[0]
            forp inpermutations(pi[1:]):
                yield[pi[0]]+p
1
2
3
4
5
6
7
8
9
>>>forp inpermutations([1,2,3]):
...    printp
...
[1,2,3]
[1,3,2]
[2,1,3]
[2,3,1]
[3,1,2]
[3,2,1]

Generator Expressions

生成器表达式可以让你通过一个简单的,单行声明定义生成器。这跟Python中的列表推导式非常类似,举个例子,下面的代码将定义一个生成器迭代所有的完全平方。注意生成器表达式的返回结果是一个生成器类型对象,它实现了next和__iter__两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> g =(x **2 for x initertools.count(1))
>>> g
<generatorobject<genexpr> at 0x1029a5fa0>
>>>next(g)
1
>>>next(g)
4
>>>iter(g)
<generatorobject<genexpr> at 0x1029a5fa0>
>>>iter(g)isg
True
>>> [g.next()for__ inxrange(10)]
[9,16,25,36,49,64,81,100,121,144]

同样可以使用生成器表达式实现Bernoulli过程,在这个例子中p=0.4。如果一个生成器表达式需要另一个迭代器作为循环指示器,并且这个生辰器表达式使用在无限序列上的,那么itertools.count将是一个很好的选择。若非如此,xrange将是一个不错的选择。

1
2
3
>>> g =(random.random() < 0.4for __ initertools.count())
>>> [g.next()for__ inxrange(10)]
[False,False,False,True,True,False,True,False,False,True]

正如前面提到的,生成器表达式能够用在任何需要迭代器作为参数的地方。举个例子,我们可以通过如下代码计算前十个全平方数的累加和:

1
2
>>>sum(x**2 for x inxrange(10))
285

更多生成器表达式的例子将在下一节给出。

itertools模块

itertools模块提供了一系列迭代器能够帮助用户轻松地使用排列、组合、笛卡尔积或其他组合结构。

在开始下面的部分之前,注意到上面给出的所有代码都是未经优化的,在这里只是充当一个示例的作用。在实际使用中,你应该避免自己去实现排列组合除非你能够有更好的想法,因为枚举的数量可是按照指数级增加的。

让我们先从一些有趣的用例开始。第一个例子来看如何写一个常用的模式:循环遍历一个三维数组的所有下标元素,并且循环遍历满足0≤i<j<k≤n条件的所有下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fromitertools importcombinations, product
 
n=4
d=3
 
defvisit(*indices):
    printindices
 
# Loop through all possible indices of a 3-D array
fori inxrange(n):
    forj inxrange(n):
        fork inxrange(n):
            visit(i, j, k)
 
# Equivalent using itertools.product
forindices inproduct(*([xrange(n)]*d)):
    visit(*indices)
 
# Now loop through all indices 0 <= i < j < k <= n
fori inxrange(n):
    forj inxrange(i+1, n):
        fork inxrange(j+1, n):
            visit(i, j, k)
 
# And equivalent using itertools.combinations
forindices incombinations(xrange(n), d):
    visit(*indices)

使用itertools模块提供的枚举器有两个好处:代码能够在单行内完成,并且很容易扩展到更高维度。我并未比较for方法和itertools两种方法的性能,也许跟n有很大关系。如果你想的话请自行测试评判。

第二个例子,来做一些有趣的数学题:使用生成器表达式、itertools.combinations以及itertools.permutations来计算排列的逆序数,并且计算一个列表全排列逆序数之和。如OEIS A001809所示,求和的结果趋近于n!n(n-1)/4。在实际使用中直接通过这公式计算要比上面的代码更高效,不过我写这个例子是为了练习itertools枚举器的使用。

1
2
3
4
5
6
7
8
9
10
importitertools
importmath
 
definversion_number(A):
    """Return the number of inversions in list A."""
    returnsum(1for x, y initertools.combinations(xrange(len(A)),2)ifA[x] > A[y])
 
deftotal_inversions(n):
    """Return total number of inversions in permutations of n."""
    returnsum(inversion_number(A)forA initertools.permutations(xrange(n)))

用法如下:

1
2
3
4
5
>>> [total_inversions(n) forn inxrange(10)]
[0,0,1,9,72,600,5400,52920,564480,6531840]
 
>>> [math.factorial(n) *n *(n -1)/4 for n inxrange(10)]
[0,0,1,9,72,600,5400,52920,564480,6531840]

第三个例子,通过brute-force counting方法计算recontres number。recontres number的定义在此。首先,我们写了一个函数在一个求和过程中使用生成器表达式去计算排列中fixed points出现的个数。然后在求和中使用itertools.permutations和其他生成器表达式计算包含n个数并且有k个fixed points的排列的总数。然后得到结果。当然了,这个实现方法是效率低下的,不提倡在实际应用中使用。再次重申,这只是为了掩饰生成器表达式以及itertools相关函数使用方法的示例。

1
2
3
4
5
6
7
defcount_fixed_points(p):
    """Return the number of fixed points of p as a permutation."""
    returnsum(1for x inp ifp[x] ==x)
 
defcount_partial_derangements(n, k):
    """Returns the number of permutations of n with k fixed points."""
    returnsum(1for p initertools.permutations(xrange(n))ifcount_fixed_points(p) ==k)

用法:

1
2
3
# Usage:
>>> [count_partial_derangements(6, i) fori inxrange(7)]
[265,264,135,40,15,0,1]

扩展阅读

http://linuxgazette.net/100/pramode.html

http://www.dabeaz.com/generators/

致谢

谢谢reddit用户jms_nh对本文的修改建议。

Python高级特性(2):Closures、Decorators和functools

本文由 伯乐在线 - 熊崽Kevin 翻译自 sahandsaba。欢迎加入技术翻译小组。转载请参见文章末尾处的要求。

《Python高级特性(1):Iterators、Generators和itertools》

装饰器(Decorators)

装饰器是这样一种设计模式:如果一个类希望添加其他类的一些功能,而不希望通过继承或是直接修改源代码实现,那么可以使用装饰器模式。简单来说Python中的装饰器就是指某些函数或其他可调用对象,以函数或类作为可选输入参数,然后返回函数或类的形式。通过这个在Python2.6版本中被新加入的特性可以用来实现装饰器设计模式。

顺便提一句,在继续阅读之前,如果你对Python中的闭包(Closure)概念不清楚,请查看本文结尾后的附录,如果没有闭包的相关概念,很难恰当的理解Python中的装饰器。

在Python中,装饰器被用于用@语法糖修辞的函数或类。现在让我们用一个简单的装饰器例子来演示如何做一个函数调用日志记录器。在这个例子中,装饰器将时间格式作为输入参数,在调用被这个装饰器装饰的函数时打印出函数调用的时间。这个装饰器当你需要手动比较两个不同算法或实现的效率时很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deflogged(time_format):
   defdecorator(func):
      defdecorated_func(*args,**kwargs):
         print"- Running '%s' on %s " % (
                                         func.__name__,
                                         time.strftime(time_format)
                              )
         start_time=time.time()
         result=func(*args,**kwargs)
         end_time=time.time()
         print"- Finished '%s', execution time = %0.3fs " % (
                                         func.__name__,
                                         end_time-start_time
                              )
 
         returnresult
     decorated_func.__name__=func.__name__
     returndecorated_func
 returndecorator

来看一个例子,在这里add1和add2函数被logged修饰,下面给出了一个输出示例。请注意在这里时间格式参数是存储在被返回的装饰器函数中(decorated_func)。这就是为什么理解闭包对于理解装饰器来说很重要的原因。同样也请注意返回函数的名字是如何被替换为原函数名的,以防万一如果它还要被使用到,这是为了防止混淆。Python默认可不会这么做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@logged("%b %d %Y - %H:%M:%S")
defadd1(x, y):
    time.sleep(1)
    returnx +y
 
@logged("%b %d %Y - %H:%M:%S")
defadd2(x, y):
    time.sleep(2)
    returnx +y
 
printadd1(1,2)
printadd2(1,2)
 
# Output:
-Running 'add1'on Jul 242013 - 13:40:47
-Finished 'add1', execution time =1.001s
3
-Running 'add2'on Jul 242013 - 13:40:48
-Finished 'add2', execution time =2.001s
3

如果你足够细心,你可能会注意到我们对于返回函数的名字__name__有着特别的处理,但对其他的注入__doc__或是__module__则没有如此。所以如果,在这个例子中add函数有一个doc字符串的话,它就会被丢弃。那么该如何处理呢?我们当然可以像处理__name__那样对待所有的字段,不过如果在每个装饰器内都这么做的话未免太繁冗了。这就是为何functools模块提供了一个名为wraps的装饰器的原因,那正是为了处理这种情况。可能在理解装饰器的过程中会被迷惑,不过当你把装饰器看成是一个接收函数名作为输入参数并且返回一个函数,这样就很好理解了。我们将在下个例子中使用wraps装饰器而不是手动去处理__name__或其他属性。

下个例子会有点复杂,我们的任务是将一个函数调用的返回结果缓存一段时间,输入参数决定缓存时间。传递给函数的输入参数必须是可哈希的对象,因为我们使用包含调用输入参数的tuple作为第一个参数,第二个参数则为一个frozenset对象,它包含了关键词项kwargs,并且作为cache key。每个函数都会有一个唯一的cache字典存储在函数的闭包内。

【译注】set和frozenset为Python的两种内建集合,其中前者为可变对象(mutable),其元素可以使用add()或remove()进行变更,而后者为不可变对象(imutable)并且是可哈希的(hashable),在建立之后元素不可变,他可以作为字典的key或是另一个集合的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
importtime
fromfunctools importwraps
 
defcached(timeout, logged=False):
    """Decorator to cache the result of a function call.
    Cache expires after timeout seconds.
    """
    defdecorator(func):
        iflogged:
            print"-- Initializing cache for", func.__name__
        cache={}
 
        @wraps(func)
        defdecorated_function(*args,**kwargs):
            iflogged:
                print"-- Called function", func.__name__
            key=(args, frozenset(kwargs.items()))
            result=None
            ifkey incache:
                iflogged:
                    print"-- Cache hit for", func.__name__, key
 
                (cache_hit, expiry) =cache[key]
                iftime.time() -expiry < timeout:
                    result=cache_hit
                eliflogged:
                    print"-- Cache expired for", func.__name__, key
            eliflogged:
                print"-- Cache miss for", func.__name__, key
 
            # No cache hit, or expired
            ifresult isNone:
                result=func(*args,**kwargs)
 
            cache[key]=(result, time.time())
            returnresult
 
        returndecorated_function
 
    returndecorator

来看看它的用法。我们使用装饰器装饰一个很基本的斐波拉契数生成器。这个cache装饰器将对代码使用备忘录模式(Memoize Pattern)。请注意fib函数的闭包是如何存放cache字典、一个指向原fib函数的引用、logged参数的值以及timeout参数的最后值的。dump_closure将在文末定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> @cached(10,True)
...deffib(n):
...    """Returns the n'th Fibonacci number."""
...    ifn ==0 or n ==1:
...        return1
...    returnfib(n -1)+fib(n -2)
...
--Initializing cache forfib
>>> dump_closure(fib)
1.Dumping function closure forfib:
--cell 0 = {}
--cell 1 =
--cell 2 = True
--cell 3 = 10
>>>
>>>print"Testing - F(4) = %d" % fib(4)
--Called function fib
--Cache miss forfib ((4,),frozenset([]))
--Called function fib
--Cache miss forfib ((3,),frozenset([]))
--Called function fib
--Cache miss forfib ((2,),frozenset([]))
--Called function fib
--Cache miss forfib ((1,),frozenset([]))
--Called function fib
--Cache miss forfib ((0,),frozenset([]))
--Called function fib
--Cache hit forfib ((1,),frozenset([]))
--Called function fib
--Cache hit forfib ((2,),frozenset([]))
Testing-F(4)=5

Class Decorators

在之前的小节中,我们看了一些函数装饰器和一些使用的小技巧,接下来我们来看看类装饰器。类装饰器将一个class作为输入参数(Python中的一种类类型对象),并且返回一个修改过的class。

第一个例子是一个简单的数学问题。当给定一个有序集合P,我们定义PdP的反序集合P(x,y) <-> Pd(x,y),也就是说两个有序集合的元素顺序互为相反的,这在Python中该如何实现?假定一个类定义了__lt__以及__le__或其他方法来实现有序。那么我们可以通过写一个类装饰器来替换这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
defmake_dual(relation):
    @wraps(relation, ['__name__','__doc__'])
    defdual(x, y):
        returnrelation(y, x)
    returndual
 
defdual_ordering(cls):
    """Class decorator that reverses all the orderings"""
    forfunc in['__lt__','__gt__','__ge__','__le__']:
        ifhasattr(cls, func):
            setattr(cls, func, make_dual(getattr(cls, func)))
    returncls

下面是将这个装饰器用以str类型的例子,创建一个名为rstr的新类,使用反字典序(opposite lexicographic)为其顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@dual_ordering
classrstr(str):
    pass
 
x=rstr("1")
y=rstr("2")
 
printx < y
printx <=y
printx > y
printx >=y
 
# Output:
False
False
True
True

来看一个更复杂的例子。假定我们希望前面所说的logged装饰器能够被用于某个类的所有方法。一个方案是在每个类方法上都加上装饰器。另一个方案是写一个类装饰器自动完成这些工作。在动手之前,我将把前例中的logged装饰器拿出来做一些小改进。首先,它使用functools提供的wraps装饰器完成固定__name__的工作。第二,一个_logged_decorator属性被引入(设置为True的布尔型变量),用来指示这个方法是否已经被装饰器装饰过,因为这个类可能会被继承而子类也许会继续使用装饰器。最后,name_prefix参数被加入用来设置打印的日志信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
deflogged(time_format, name_prefix=""):
    defdecorator(func):
        ifhasattr(func,'_logged_decorator')andfunc._logged_decorator:
            returnfunc
 
        @wraps(func)
        defdecorated_func(*args,**kwargs):
            start_time=time.time()
            print"- Running '%s' on %s " % (
                                            name_prefix+func.__name__,
                                            time.strftime(time_format)
                                 )
            result=func(*args,**kwargs)
            end_time=time.time()
            print"- Finished '%s', execution time = %0.3fs " % (
                                            name_prefix+func.__name__,
                                            end_time-start_time
                                 )
 
            returnresult
        decorated_func._logged_decorator=True
        returndecorated_func
    returndecorator

好的,让我们开始写类装饰器:

1
2
3
4
5
6
7
8
9
10
11
deflog_method_calls(time_format):
    defdecorator(cls):
        foro indir(cls):
            ifo.startswith('__'):
                continue
            a=getattr(cls, o)
            ifhasattr(a,'__call__'):
                decorated_a=logged(time_format, cls.__name__+".")(a)
                setattr(cls, o, decorated_a)
        returncls
    returndecorator

下面是使用方法,注意被继承的或被重写的方法是如何处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@log_method_calls("%b %d %Y - %H:%M:%S")
classA(object):
    deftest1(self):
        print"test1"
 
@log_method_calls("%b %d %Y - %H:%M:%S")
classB(A):
    deftest1(self):
        super(B,self).test1()
        print"child test1"
 
    deftest2(self):
        print"test2"
 
b=B()
b.test1()
b.test2()
 
# Output:
-Running 'B.test1'on Jul 242013 - 14:15:03
-Running 'A.test1'on Jul 242013 - 14:15:03
test1
-Finished 'A.test1', execution time =0.000s
child test1
-Finished 'B.test1', execution time =1.001s
-Running 'B.test2'on Jul 242013 - 14:15:04
test2
-Finished 'B.test2', execution time =2.001s

我们第一个类装饰器的例子是类的反序方法。一个相似的装饰器,可以说是相当有用的,实现__lt__、__le__、__gt__、__ge__和__eq__中的一个,能够实现类的全排序么?这也就是functools.total_ordering装饰器所做的工作。详情请见参考文档。

Flask中的一些例子

让我们来看看Flask中用到的一些有趣的装饰器。

假定你希望让某些函数在特定的调用时刻输出警告信息,例如仅仅在debug模式下。而你又不希望每个函数都加入控制的代码,那么你就能够使用装饰器来实现。以下就是Flask的app.py中定义的装饰器的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
defsetupmethod(f):
    """Wraps a method so that it performs a check in debug mode if the
    first request was already handled.
    """
    defwrapper_func(self,*args,**kwargs):
        ifself.debugandself._got_first_request:
            raiseAssertionError('A setup function was called after the '
                'first request was handled.  This usually indicates a bug '
                'in the application where a module was not imported '
                'and decorators or other functionality was called too late.\n'
                'To fix this make sure to import all your view modules, '
                'database models and everything related at a central place '
                'before the application starts serving requests.')
        returnf(self,*args,**kwargs)
    returnupdate_wrapper(wrapper_func, f)

来看一个更有趣的例子,这个例子是Flask的route装饰器,在Flask类中定义。注意到装饰器可以是类中的一个方法,将self作为第一个参数。完整的代码在app.py中。请注意装饰器简单的将被装饰过的函数注册成为一个URL句柄,这是通过调用add_url_rule函数来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
defroute(self, rule, **options):
 """A decorator that is used to register a view function for a
 given URL rule.  This does the same thing as :meth:`add_url_rule`
 but is intended for decorator usage::
 
     @app.route('/')
     def index():
         return 'Hello World'
 
 For more information refer to :ref:`url-route-registrations`.
 
 :param rule: the URL rule as string
 :param endpoint: the endpoint for the registered URL rule.  Flask
                  itself assumes the name of the view function as
                  endpoint
 :param options: the options to be forwarded to the underlying
                 :class:`~werkzeug.routing.Rule` object.  A change
                 to Werkzeug is handling of method options.  methods
                 is a list of methods this rule should be limited
                 to (`GET`, `POST` etc.).  By default a rule
                 just listens for `GET` (and implicitly `HEAD`).
                 Starting with Flask 0.6, `OPTIONS` is implicitly
                 added and handled by the standard request handling.
 """
 defdecorator(f):
     endpoint=options.pop('endpoint',None)
     self.add_url_rule(rule, endpoint, f, **options)
     returnf
 returndecorator

扩展阅读

1. official Python Wiki

2. metaprogramming in Python 3

附录:闭包

一个函数闭包是一个函数和一个引用集合的组合,这个引用集合指向这个函数被定义的作用域的变量。后者通常指向一个引用环境(referencing environment),这使得函数能够在它被定义的区域之外执行。在Python中,这个引用环境被存储在一个cell的tuple中。你能够通过func_closure或Python 3中的__closure__属性访问它。要铭记的一点是引用及是引用,而不是对象的深度拷贝。当然了,对于不可变对象而言,这并不是问题,然而对可变对象(list)这点就必须注意,随后会有一个例子说明。请注意函数在定义的地方也有__globals__字段来存储全局引用环境。

来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>>defreturn_func_that_prints_s(s):
...    deff():
...            prints
...    returnf
...
>>> g =return_func_that_prints_s("Hello")
>>> h =return_func_that_prints_s("World")
>>> g()
Hello
>>> h()
World
>>> g ish
False
>>> h.__closure__
(,)
>>>print[str(c.cell_contents)forc ing.__closure__]
['Hello']
>>>print[str(c.cell_contents)forc inh.__closure__]
['World']

一个稍复杂的例子。确保明白为什么会这么执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>>defreturn_func_that_prints_list(z):
...    deff():
...            printz
...    returnf
...
>>> z =[1,2]
>>> g =return_func_that_prints_list(z)
>>> g()
[1,2]
>>> z.append(3)
>>> g()
[1,2,3]
>>> z =[1]
>>> g()
[1,2,3]

【译者】:z.append(3)时,g()内部的引用和z仍然指向一个变量,而z=[1]之后,两者就不再指向一个变量了。

最后,来看看代码中使用到的dump_closure方法的定义。

1
2
3
4
5
6
7
defdump_closure(f):
   ifhasattr(f,"__closure__")andf.__closure__ isnot None:
       print"- Dumping function closure for %s:" % f.__name__
       fori, c inenumerate(f.__closure__):
           print"-- cell %d  = %s" % (i, c.cell_contents)
   else:
       print" - %s has no closure!" % f.__name__

0 0
原创粉丝点击