本文展示一些高级的Python设计结构和它们的使用方法。在日常工作中,你可以根据需要选择合适的数据结构,例如对快速查找性的要求、对数据一致性的要求或是对索引的要求等,同时也可以将各种数据结构合适地结合在一起,从而生成具有逻辑性并易于理解的数据模型。Python的数据结构从句法上来看非常直观,并且提供了大量的可选操作。这篇指南尝试将大部分常用的数据结构知识放到一起,并且提供对其最佳用法的探讨。
推导式(Comprehensions)

如果你已经使用了很长时间的Python,那么你至少应该听说过列表推导(list comprehensions)。这是一种将for循环、if表达式以及赋值语句放到单一语句中的一种方法。换句话说,你能够通过一个表达式对一个列表做映射或过滤操作。

一个列表推导式包含以下几个部分:

  •     一个输入序列
  •     一个表示输入序列成员的变量
  •     一个可选的断言表达式
  •     一个将输入序列中满足断言表达式的成员变换成输出列表成员的输出表达式

举个例子,我们需要从一个输入列表中将所有大于0的整数平方生成一个新的序列,你也许会这么写:
 

1

2

3

4

5

6

7

8

9

num = [1, 4, -5, 10, -7, 2, 3, -1]

filtered_and_squared = []

  

for number in num:

 if number > 0:

 filtered_and_squared.append(number ** 2)

print filtered_and_squared

  

# [1, 16, 100, 4, 9]

很简单是吧?但是这就会有4行代码,两层嵌套外加一个完全不必要的append操作。而如果使用filter、lambda和map函数,则能够将代码大大简化:
 

1

2

3

4

5

num = [1, 4, -5, 10, -7, 2, 3, -1]

filtered_and_squared = map(lambda x: x ** 2, filter(lambda x: x > 0, num))

print filtered_and_squared

  

# [1, 16, 100, 4, 9]

嗯,这么一来代码就会在水平方向上展开。那么是否能够继续简化代码呢?列表推导能够给我们答案:
 

1

2

3

4

5

num = [1, 4, -5, 10, -7, 2, 3, -1]

filtered_and_squared = [ x**2 for x in num if x > 0]

print filtered_and_squared

  

# [1, 16, 100, 4, 9]

  •     迭代器(iterator)遍历输入序列num的每个成员x
  •     断言式判断每个成员是否大于零
  •     如果成员大于零,则被交给输出表达式,平方之后成为输出列表的成员。

列表推导式被封装在一个列表中,所以很明显它能够立即生成一个新列表。这里只有一个type函数调用而没有隐式调用lambda函数,列表推导式正是使用了一个常规的迭代器、一个表达式和一个if表达式来控制可选的参数。

另一方面,列表推导也可能会有一些负面效应,那就是整个列表必须一次性加载于内存之中,这对上面举的例子而言不是问题,甚至扩大若干倍之后也都不是问题。但是总会达到极限,内存总会被用完。

针对上面的问题,生成器(Generator)能够很好的解决。生成器表达式不会一次将整个列表加载到内存之中,而是生成一个生成器对象(Generator objector),所以一次只加载一个列表元素。

生成器表达式同列表推导式有着几乎相同的语法结构,区别在于生成器表达式是被圆括号包围,而不是方括号:
 

1

2

3

4

5

6

7

8

9

10

num = [1, 4, -5, 10, -7, 2, 3, -1]

filtered_and_squared = ( x**2 for x in num if x > 0 )

print filtered_and_squared

  

# <generator object <genexpr> at 0x00583E18>

  

for item in filtered_and_squared:

 print item

  

# 1, 16, 100 4,9

这比列表推导效率稍微提高一些,让我们再一次改造一下代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

num = [1, 4, -5, 10, -7, 2, 3, -1]

  

def square_generator(optional_parameter):

 return (x ** 2 for x in num if x > optional_parameter)

  

print square_generator(0)

# <generator object <genexpr> at 0x004E6418>

  

# Option I

for k in square_generator(0):

 print k

# 1, 16, 100, 4, 9

  

# Option II

g = list(square_generator(0))

print g

# [1, 16, 100, 4, 9]

除非特殊的原因,应该经常在代码中使用生成器表达式。但除非是面对非常大的列表,否则是不会看出明显区别的。

下例使用zip()函数一次处理两个或多个列表中的元素:
 

1

2

3

4

5

6

7

8

9

alist = ['a1', 'a2', 'a3']

blist = ['1', '2', '3']

  

for a, b in zip(alist, blist):

 print a, b

  

# a1 1

# a2 2

# a3 3

再来看一个通过两阶列表推导式遍历目录的例子:
 

1

2

3

4

5

6

7

8

import os

def tree(top):

 for path, names, fnames in os.walk(top):

 for fname in fnames:

  yield os.path.join(path, fname)

  

for name in tree('C:\Users\XXX\Downloads\Test'):

 print name

装饰器(Decorators)

装饰器为我们提供了一个增加已有函数或类的功能的有效方法。听起来是不是很像Java中的面向切面编程(Aspect-Oriented Programming)概念?两者都很简单,并且装饰器有着更为强大的功能。举个例子,假定你希望在一个函数的入口和退出点做一些特别的操作(比如一些安全、追踪以及锁定等操作)就可以使用装饰器。

装饰器是一个包装了另一个函数的特殊函数:主函数被调用,并且其返回值将会被传给装饰器,接下来装饰器将返回一个包装了主函数的替代函数,程序的其他部分看到的将是这个包装函数。
 

1

2

3

4

5

6

7

8

9

10

def timethis(func):

 '''

 Decorator that reports the execution time.

 '''

 pass

  

@timethis

def countdown(n):

 while n > 0:

 n -= 1

语法糖@标识了装饰器。

好了,让我们回到刚才的例子。我们将用装饰器做一些更典型的操作:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import time

from functools import wraps

  

def timethis(func):

 '''

 Decorator that reports the execution time.

 '''

 @wraps(func)

 def wrapper(*args, **kwargs):

 start = time.time()

 result = func(*args, **kwargs)

 end = time.time()

 print(func.__name__, end-start)

 return result

 return wrapper

  

@timethis

def countdown(n):

 while n > 0:

 n -= 1

  

countdown(100000)

  

# ('countdown', 0.006999969482421875)

当你写下如下代码时:
 

1

2

@timethis

def countdown(n):

意味着你分开执行了以下步骤:
 

1

2

3

def countdown(n):

...

countdown = timethis(countdown)

装饰器函数中的代码创建了一个新的函数(正如此例中的wrapper函数),它用 *args 和 **kwargs 接收任意的输入参数,并且在此函数内调用原函数并且返回其结果。你可以根据自己的需要放置任何额外的代码(例如本例中的计时操作),新创建的包装函数将作为结果返回并取代原函数。
 

1

2

3

@decorator

def function():

 print("inside function")

当编译器查看以上代码时,function()函数将会被编译,并且函数返回对象将会被传给装饰器代码,装饰器将会在做完相关操作之后用一个新的函数对象代替原函数。

装饰器代码是什么样的?大部分的例子都是将装饰器定义为函数,而我发觉将装饰器定义成类更容易理解其功能,并且这样更能发挥装饰器机制的威力。

对装饰器的类实现唯一要求是它必须能如函数一般使用,也就是说它必须是可调用的。所以,如果想这么做这个类必须实现__call__方法。

这样的装饰器应该用来做些什么?它可以做任何事,但通常它用在当你想在一些特殊的地方使用原函数时,但这不是必须的,例如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

class decorator(object):

  

 def __init__(self, f):

 print("inside decorator.__init__()")

 f() # Prove that function definition has completed

  

 def __call__(self):

 print("inside decorator.__call__()")

  

@decorator

def function():

 print("inside function()")

  

print("Finished decorating function()")

  

function()

  

# inside decorator.__init__()

# inside function()

# Finished decorating function()

# inside decorator.__call__()

译者注:
1. 语法糖@decorator相当于function=decorator(function),在此调用decorator的__init__打印“inside decorator.__init__()”
2. 随后执行f()打印“inside function()”
3. 随后执行“print(“Finished decorating function()”)”
4. 最后在调用function函数时,由于使用装饰器包装,因此执行decorator的__call__打印 “inside decorator.__call__()”。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐