0%

让代码变得Pythonic

#13 让代码变得Pythonic

在学习Python的过程中,肯定听说过这么一个词:Pythonic,它的意思是让你的代码很Python!

一、列表生成式

前面有一节专门讲解了Python的列表,其灵活的使用方法一定让你陶醉其中。当然,也也知道怎么初始化一个列表,比如现在要生成 [0,1,2,3,4] 这样一个列表:

1
2
In [1]: list(range(5))
Out[1]: [0, 1, 2, 3, 4]

现在要将此列表的每个元素平方,要怎么办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 方法一
In [9]: a
Out[9]: [0, 1, 2, 3, 4]

In [10]: b = []

In [11]: for i in a:
...: b.append(i**2)
...:

In [12]: b
Out[12]: [0, 1, 4, 9, 16]

# 使用 for 循环遍历每一个元素,之后将结果保存在新的列表里
1
2
3
4
5
6
7
8
9
10
11
12
# 方法二
In [13]: a
Out[13]: [0, 1, 2, 3, 4]

In [14]: for index,i in enumerate(a):
...: a[index] **=2
...:

In [15]: a
Out[15]: [0, 1, 4, 9, 16]

# 使用内置函数 enumerate() 将可迭代对象返回其索引和相应的值,这种方法直接改变原有列表的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 方法三
In [15]: a
Out[15]: [0, 1, 4, 9, 16]

In [16]: func = lambda x:x**2

In [18]: a = map(func,a)

In [19]: a
Out[19]: <map at 0x21bbb7a30b8>

In [20]: for i in a:
...: print(i)
...:
0
1
16
81
256

# 使用内置函数 map() 也可以实现,map(函数,可迭代对象),将可迭代对象的每一个元素传入函数并返回结果
1
2
3
4
5
6
7
8
#方法四:使用更加Pythonic的方法:列表生成式

In [22]: a = [i for i in range(5)]

In [23]: a
Out[23]: [0, 1, 2, 3, 4]

# 可以看到生成一个列表就是如此简单
1
2
3
4
5
6
In [24]: a = [i\*\*2 for i in range(5)]

In [25]: a
Out[25]: [0, 1, 4, 9, 16]

# 可以看到列表生成式很方便,很好用,这该死的无处安放的魅力啊
1
2
3
4
5
6
In [26]: a = [i for i in range(10) if i%2 == 0]

In [27]: a
Out[27]: [0, 2, 4, 6, 8]

# 列表生成式还可以加入 if 判断
1
2
3
4
5
In [28]: [p + q for p in range(3) for q in range(5)]
Out[28]: [0, 1, 2, 3, 4, 1, 2, 3, 4, 5, 2, 3, 4, 5, 6]

# 使用两层循环实现全排列
# 0+0 0+1 0+2 0+3 0+4 1+0 1+1 1+2 1+3 1+4 2+0 2+1 2+2 2+3 2+4

二、生成器

列表生成式很实用,但是有一个致命的缺点,就是不能创建大数据量的列表,数据量太大时会导致计算机内存不够用,同时,如果创建的大数据量列表被使用的元素很少的话,那么就会造成存储空间的大量浪费,那有没有一种方法,可以不提前生成列表,而是在使用列表的时候生成一个列表,换句话说就是:边循环边计算,这就是生成器—— generator。生成器在需要的时候才产生结果,不是立即产生结果,生成器效率高,节省CPU生成器只能遍历一次,是一个特殊的迭代器。

生成器表达式

类似于列表生成式,只不过将方括号 [] 改变为圆括号 ()

1
2
3
4
5
6
7
8
9
10
11
In [29]: l = [i for i in range(8)]

In [30]: l
Out[30]: [0, 1, 2, 3, 4, 5, 6, 7]

In [31]: g = (i for i in range(8))

In [32]: g
Out[32]: <generator object <genexpr> at 0x0000021BBBB16E08>

# 可以看到 l 是列表,而 g 是一个generator

如何获得生成器的元素呢?使用next()方法可以获取一个元素:

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
In [33]: next(g)
Out[33]: 0

In [34]: next(g)
Out[34]: 1

In [35]: next(g)
Out[35]: 2

In [36]: next(g)
Out[36]: 3

In [37]: next(g)
Out[37]: 4

In [38]: next(g)
Out[38]: 5

In [39]: next(g)
Out[39]: 6

In [40]: next(g)
Out[40]: 7

In [41]: next(g)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-41-e734f8aca5ac> in <module>
----> 1 next(g)

StopIteration:

# 可以看到当生成器没有元素可以取的时候,会抛出StopIteration异常

可以看到上面的代码总司不停的手动使用next()获取下一个元素,很烦~,在Python中其实不经常使用next(),而是用for循环的方法迭代生成器:

1
2
3
4
5
6
7
8
9
10
11
In [43]: g = (i for i in range(8))

In [45]: for p in g:
...: print(p)
1
2
3
4
5
6
7

创建一个生成器以后,基本上不会使用next()方法,而是使用for循环,迭代完成以后不会抛出StopIteration异常。

生成器函数

将函数返回时的关键字return改为yield。函数将每次返回一个结果,之后挂起,再次调用时,继续从挂起的位置执行

1
2
3
4
5
6
7
8
9
10
11
12
13
In [46]: def print_num():
...: '''
...: print num to screen
...: '''
...: a = 'No.1'
...: b = 'No.2'
...: c = 'No.3'
...: print(a)
...: yield a
...: print(b)
...: yield b
...: print(c)
...: yield c
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
运行函数可以使用 next() ,当然也不常用,for循环才是generator的真爱:

In [46]: def print_num():
...: '''
...: print num to screen
...: '''
...: a = 'No.1'
...: b = 'No.2'
...: c = 'No.3'
...: print(a)
...: yield a
...: print(b)
...: yield b
...: print(c)
...: yield c
...:

In [52]: a = print_num()

In [53]: next(a)
No.1
Out[53]: 'No.1'

In [54]: next(a)
No.2
Out[54]: 'No.2'

In [55]: next(a)
No.3
Out[55]: 'No.3'

In [56]: next(a)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-56-15841f3f11d4> in <module>
----> 1 next(a)

StopIteration:
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
# 菲波那切数列
def Fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b # 还记得这里的交换变量方法吗?
n = n + 1
return '到头了!!!!'

In [61]: f = Fib(10)

In [62]: for p in f:
...: print(p)
1
1
2
3
5
8
13
21
34
55



# for循环才是generator的真爱

三、迭代器

在Python中,list、string、tuple、dict都是可以使用for循环进行遍历的,现在又多了一类generator。这些可以使用for循环的对象称为可迭代对象。迭代器是用来帮助我们记录每次迭代的位置,而可迭代对象使用内置函数iter()是可以转换为迭代器的:

1
2
3
4
5
6
7
8
9
In [63]: a = [1,2,3]      # 创建一个新列表

In [64]: print(a) # 可以看到a是列表
[1, 2, 3]

In [65]: i = iter(a) # 将其变为迭代器

In [67]: print(i) # 可以看到i为迭代器
<list_iterator object at 0x0000021BBCE00240>

获取迭代器中的元素可以使用内置函数next(),但不经常使用,经常使用的是for循环:

1
2
3
4
5
6
7
8
In [68]: i
Out[68]: <list_iterator at 0x21bbce00240>

In [70]: for p in i:
...: print(p)
1
2
3

补充:对于列表、字符串、元组、字典等数据类型,在使用for循环时,在后台for语句对这些对象调用iter()函数,之后使用next()逐个访问每一个元素,直到遇到StopIteration异常,迭代结束。

在Python中,可以使用 isinstance() 判断一个对象是否为可迭代对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
In [71]: from collections import Iterable   # 导入Iterable模块,之后会讲

In [72]: isinstance([],Iterable)
Out[72]: True

In [73]: isinstance((),Iterable)
Out[73]: True

In [74]: isinstance({},Iterable)
Out[74]: True

In [75]: isinstance('',Iterable)
Out[75]: True

四、装饰器

装饰器是什么呢?来举个例子就明白了:有一个长发飘飘的漂亮女明星,被邀出演尼姑,肯定不会把头发剃光了吧,怎么办呢,聪明的你一定想到戴个头套就行。是的,在Python中,长发飘飘的女明星就是源代码,头套就是装饰器。转时期的本质就是在不改变函数原有代码并且不改变原有函数的调用方式的基础上给函数加上新的功能,听起来很迷人,用起来一样有趣,让你的代码一下子就提高档次了。

过程No.1

现在有一个 tell_name() 函数:

1
2
def tell_name():
print('I am MinuteSheep')

要求记录它的执行时间,对原有函数改写,这样来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
import time    # 引入time模块,这是一个时间模块,以后会讲到
def tell_name():
start_time = time.time() # 获取现在的时间
print('I am MinuteSheep')
time.sleep(2) # 为了体现执行时间,让程序等两秒
end_time = time.time() # 获取现在的时间
print('执行时间为:',end_time - start_time)

tell_name()

# 运行结果:
I am MinuteSheep
执行时间为: 2.001427173614502

过程No.2

现在又100个函数需要计算其执行时间,总不能改写100个函数的源代码吧,怎么办呢?还记的高阶函数吗,可以将函数当作变量传给函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time    # 引入time模块,这是一个时间模块,以后会讲到


def tell_name():
print('I am MinuteSheep')
time.sleep(2) # 为了体现执行时间,让程序等两秒


def Time(func): # 使用高阶函数
start_time = time.time()
func()
end_time = time.time()
print('执行时间为:', end_time - start_time)

Time(tell_name) # 调用方式发生改变

# 运行结果:
I am MinuteSheep
执行时间为: 2.00026535987854

上面代码似乎实现了这个功能,也没有修改原函数的代码,但是却改变了它的调用方式,如果一个程序中有上百条调用,都要改的话还是很麻烦

过程No.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time    # 引入time模块,这是一个时间模块,以后会讲到


def tell_name():
print('I am MinuteSheep')
time.sleep(2) # 为了体现执行时间,让程序等两秒


def Time(func):
def wrapper(): # 使用函数的嵌套
start_time = time.time()
func()
end_time = time.time()
print('执行时间为:', end_time - start_time)
return wrapper


tell_name = Time(tell_name) # 相当于 tell_name = wrapper
tell_name() # 相当于执行 wrapper()

上面代码已经基本实现了这个功能,但是每次都要写两条调用语句才行,很烦

过程No.4

在Python中,为了克服上述问题,出现了一个叫做语法糖的语句,所以装饰器又叫做语法糖,在函数定义之前使用@语法糖可增加相应的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time    # 引入time模块,这是一个时间模块,以后会讲到


def Time(func):
def wrapper(): # 使用函数的嵌套
start_time = time.time()
func()
end_time = time.time()
print('执行时间为:', end_time - start_time)
return wrapper


@Time # 这就是装饰器,也叫语法糖
def tell_name():
print('I am MinuteSheep')
time.sleep(2) # 为了体现执行时间,让程序等两秒


tell_name() # 相当于执行 wrapper()

# 运行结果:
I am MinuteSheep
执行时间为: 2.000563621520996

上面代码实现了一个最简单的装饰器。

过程No.5

但是,又有新的问题出现了,如果被装饰函数有参数怎么办,这么办:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time    # 引入time模块,这是一个时间模块,以后会讲到


def Time(func):
def wrapper(name): # 使用函数的嵌套
start_time = time.time()
func(name)
end_time = time.time()
print('执行时间为:', end_time - start_time)
return wrapper


@Time
def tell_name(name):
print('I am',name)
time.sleep(2) # 为了体现执行时间,让程序等两秒


tell_name('MS') # 相当于执行 wrapper('MS')

# 运行结果:
I am MS
执行时间为: 2.0003795623779297

看起来不错

过程No.6

上面代码实现了装饰有一个参数函数的功能,但是,装饰器被应用与不同的函数,谁能知道这个函数有没有参数,有几个参数,为了实现通用性,这么办:

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
import time    # 引入time模块,这是一个时间模块,以后会讲到


def Time(func):
def wrapper(*args,**kwargs): # 通过非固定参数实现各种参数的通用装饰器
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print('执行时间为:', end_time - start_time)
return wrapper


@Time
def tell_name(name):
print('I am', name)
time.sleep(2) # 为了体现执行时间,让程序等两秒


@Time
def add(a, b):
c = a + b
print(c)


tell_name('MS')
add(5, 6)

# 运行结果:
I am MS
执行时间为: 2.00108003616333
11
执行时间为: 0.0004711151123046875

过程No.7

上面的过程中装饰器没有参数,其实装饰器时可以带参数的:

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
import time    # 引入time模块,这是一个时间模块,以后会讲到


def Time(num): # 使用两层嵌套实现带参数的装饰器
def decorator(func):
def wrapper(*args, **kwargs):
if num == 1:
start_time = time.time()
func(*args, **kwargs)
end_time = time.time()
print('执行时间为:', end_time - start_time)
elif num == 0:
func(args, **kwargs)
print('不需要计算时间')
return wrapper
return decorator


@Time(num=1)
def tell_name(name):
print('I am', name)
time.sleep(2) # 为了体现执行时间,让程序等两秒


@Time(num=0)
def add(a, b):
c = a + b
print(c)


tell_name('MS')
add(5, 6)

# 运行结果:
I am MS
执行时间为: 2.0000314712524414
11
不需要计算时间

过程No.8

一个函数可以使用多个装饰器,装饰器运行顺序从里到外:

1
2
3
4
5
6
7
@a
@b
@c
def func():
pass

# 先运行c,再运行b,最后运行a

以上就是装饰器99%的功能,还有一种叫做类装饰器,等记录完Python面向对象的知识后再补充,拜拜~