高级特性
1.1切片
-
取一个list或tuple的部分元素是非常常见的操作
-
切片的第一个冒号左右的数字表示的是位置信息
-
取前N个元素,也就是索引为0-(N-1)的元素,可以用循环:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] >>> r = [] >>> n = 3 >>> for i in range(n): ... r.append(L[i]) ... >>> r ['Michael', 'Sarah', 'Tracy']
-
用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作。
-
切片操作十分有用。我们先创建一个0-99的数列:
>>> L = list(range(100)) >>> L [0, 1, 2, 3, ..., 99]
可以通过切片轻松取出某一段数列。比如前10个数:
>>> L[:10] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
后10个数:
>>> L[-10:] [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
前11-20个数:
>>> L[10:20]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
L[10:20]的理解:从10开始(从下标10开始)到第20个数
前10个数,每两个取一个:
>>> L[:10:2]
[0, 2, 4, 6, 8]
-
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
>>> (0, 1, 2, 3, 4, 5)[:3] (0, 1, 2)
-
字符串也可以用切片操作,只是操作结果仍是字符串:
>>> 'ABCDEFG'[:3] 'ABC' >>> 'ABCDEFG'[::2] 'ACEG'
-
在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring),其实目的就是对字符串切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。
练习:
利用切片操作,实现一个trim()函数,去除字符串首尾的空格,注意不要调用str的strip()
方法:
用递归解决:
def trim(L):
if L=='':
return L
elif L[0]==" ":
return trim(L[1:])
elif L[-1]==" ":
return trim(L[:-2])
else:
return L
print(trim(" hjk "))
1.2迭代
也可以理解为for循环
-
果给定一个list或tuple,我们可以通过
for
循环来遍历这个list或tuple,这种遍历我们称为迭代 -
在Python中,迭代是通过
for ... in
来完成的,而很多语言比如C语言,迭代list是通过下标完成的,比如Java代码: -
只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:
>>> d = {'a': 1, 'b': 2, 'c': 3} >>> for key in d: ... print(key) ... a c b
因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。
-
默认情况下,dict迭代的是key。如果要迭代value,可以用
for value in d.values()
,如果要同时迭代key和value,可以用for k, v in d.items()
-
所以,当我们使用
for
循环时,只要作用于一个可迭代对象,for
循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。那么,如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:
>>> from collections import Iterable >>> isinstance('abc', Iterable) # str是否可迭代 True >>> isinstance([1,2,3], Iterable) # list是否可迭代 True >>> isinstance(123, Iterable) # 整数是否可迭代 False
-
最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的
enumerate
函数可以把一个list变成索引-元素对,这样就可以在for
循环中同时迭代索引和元素本身:>>> for i, value in enumerate(['A', 'B', 'C']): ... print(i, value) ... 0 A 1 B 2 C
上面的
for
循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:>>> for x, y in [(1, 1), (2, 4), (3, 9)]: ... print(x, y) ... 1 1 2 4 3 9
1.3列表生成式
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
例子:
-
如果要生成
[1x1, 2x2, 3x3, ..., 10x10]
怎么做?方法一是循环:>>> L = [] >>> for x in range(1, 11): ... L.append(x * x) ... >>> L [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:
>>> [x * x for x in range(1, 11)] [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
写列表生成式时,把要生成的元素
x * x
放到前面,后面跟for
循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法 -
**for循环后面还可以加上if判断,**这样我们就可以筛选出仅偶数的平方:
for
后面的if
是一个筛选条件,不能带else
,否则如何筛选?>>> [x * x for x in range(1, 11) if x % 2 == 0] [4, 16, 36, 64, 100]
-
还可以使用两层循环,可以生成全排列:
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
-
例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:
>>> import os # 导入os模块,模块的概念后面讲到 >>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录 ['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
-
for
循环其实可以同时使用两个甚至多个变量,比如dict
的items()
可以同时迭代key和value:>>> d = {'x': 'A', 'y': 'B', 'z': 'C' } >>> for k, v in d.items(): ... print(k, '=', v) ... y = B x = A z = C
因此,列表生成式也可以使用两个变量来生成list:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' } >>> [k + '=' + v for k, v in d.items()] ['y=B', 'x=A', 'z=C']
-
最后把一个list中所有的字符串变成小写:
>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
- 另一些童鞋发现把
if
写在for
前面必须加else
,否则报错
>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
上述for
前面的表达式x if x % 2 == 0 else -x
才能根据x
计算出确定的结果。
可见,在一个列表生成式中,for
前面的if ... else
是表达式,而for
后面的if
是过滤条件,不能带else
。
例如:
def generateList():
L=[2,'CHEN','MA','Ding',67]
m=[x.lower() if isinstance(x,str) else x for x in L ]
print(m)
print(generateList())
[2, 'chen', 'ma', 'ding', 67]
1.4生成器
generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。
**出现的原因:**创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?在Python中,这种一边循环一边计算的机制,称为生成器:generator。
创建生成器
-
第一种方法很简单,只要把一个列表生成式的
[]
改成()
,就创建了一个generator:>>> L = [x * x for x in range(10)] >>> L [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> g = (x * x for x in range(10)) >>> g <generator object <genexpr> at 0x1022ef630>
创建
L
和g
的区别仅在于最外层的[]
和()
,L
是一个list,而g
是一个generator但我们怎么打印出generator的每一个元素呢?
如果要一个一个打印出来,可以通过
next()
函数获得generator的下一个返回值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration
的错误。>>> next(g) 0 >>> next(g) 1 >>> next(g) 4 >>> next(g) >>> next(g) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
上面这种不断调用
next(g)
实在是太变态了,正确的方法是使用for
循环,因为generator也是可迭代对象:>>> g = (x * x for x in range(10)) >>> for n in g: ... print(n)
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
def fib(max): n, a, b = 0, 0, 1 while n < max: print(b) a, b = b, a + b n = n + 1 return 'done'
注意,赋值语句 a, b = b, a + b 从等号两边分看,看成两部分
仔细观察,可以看出,
fib
函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。也就是说,上面的函数和generator仅一步之遥。要把
fib
函数变成generator,只需要把print(b)
改为yield b
就可以了:def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1 return 'done'
-
以上就是定义generator的另一种方法。如果一个函数定义中包含
yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator:函数是顺序执行,遇到
return
语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。举个简单的例子,定义一个generator,依次返回数字1,3,5:
def odd(): print('step 1') yield 1 print('step 2') yield(3) print('step 3') yield(5)
用该generator时,首先要生成一个generator对象,然后用
next()
函数不断获得下一个返回值:>>> o = odd() >>> next(o) step 1 1 >>> next(o) step 2 3 >>> next(o) step 3 5 >>> next(o) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
可以看到,
odd
不是普通函数,而是generator,在执行过程中,遇到yield
就中断,下次又继续执行。同样的,把函数改成generator后,我们基本上从来不会用
next()
来获取下一个返回值,而是直接使用for
循环来迭代,因为生成器是可迭代对象:>>> for n in fib(6): ... print(n)
但是用
for
循环调用generator时,发现拿不到generator的return
语句的返回值。如果想要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中:>>> g = fib(6) >>> while True: ... try: ... x = next(g) ... print('g:', x) ... except StopIteration as e: ... print('Generator return value:', e.value) ... break g: 1 g: 1 g: 2 g: 3 g: 5 g: 8 Generator return value: done
请注意区分普通函数和generator函数,普通函数调用直接返回结果:
>>> r = abs(6) >>> r 6
generator函数的“调用”实际返回一个generator对象:
>>> g = fib(6) >>> g <generator object fib at 0x1022ef948>
杨辉三角:
1
/ \
1 1
/ \ / \
1 2 1
/ \ / \ / \
1 3 3 1
/ \ / \ / \ / \
1 4 6 4 1
/ \ / \ / \ / \ / \
1 5 10 10 5 1
def triangles():
L = [1]
i=0
while i<6:
yield L
L = [L[0], *[L[m] + L[m + 1] for m in range(len(L) - 1)], L[-1]]
i=i+1
for n in triangles():
print(n)
*[L[m] + L[m + 1] for m in range(len(L) - 1)]
的理解,*为多次附加
列表也可用加法,为列表增加数,例如 L=L+[1,4,5]
1.5迭代器
我们已经知道,可以直接作用于for
循环的数据类型有以下几种:
一类是集合数据类型,如list
、tuple
、dict
、set
、str
等;
一类是generator
,包括生成器和带yield
的generator function。
这些可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False
可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
。
可以使用isinstance()
判断一个对象是否是Iterator
对象:
>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
生成器都是Iterator
对象,但list
、dict
、str
虽然是Iterable
,却不是Iterator
。
把list
、dict
、str
等Iterable
变成Iterator
可以使用**iter()
函数**:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
你可能会问,为什么list
、dict
、str
等数据类型不是Iterator
?
这是因为Python的Iterator
对象表示的是一个数据流,Iterator对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。
凡是可作用于for
循环的对象都是Iterable
类型
凡是可作用于next()
函数的对象都是Iterator
类型,它们表示一个惰性计算的序列
函数式编程
2.1 高阶函数
变量可以指向函数:
>>> abs
<built-in function abs>
如果把函数本身赋值给变量呢?
>>> f = abs
>>> f
<built-in function abs>
结论:函数本身也可以赋值给变量,即:变量可以指向函数。
如果一个变量指向了一个函数,那么,可否通过该变量来调用这个函数?用代码验证一下:
>>> f = abs
>>> f(-10)
10
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
def add(x, y, f):
return f(x) + f(y)
我们可以调用add(-5, 6, abs)
2.1.1Map/Reduce
map
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回
举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用map()
实现如下
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
map()
传入的第一个参数是f
,即函数对象本身。由于结果r
是一个Iterator
,Iterator
是惰性序列,因此通过list()
函数让它把整个序列都计算出来并返回一个list
reduce
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
比方说对一个序列求和,就可以用reduce
实现:
>>> from functools import reduce
>>> def add(x, y):
... return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25
当然求和运算可以直接用Python内建函数sum()
,没必要动用reduce
。
但是如果要把序列[1, 3, 5, 7, 9]
变换成整数13579
,reduce
就可以派上用场:
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579
这个例子本身没多大用处,但是,如果考虑到字符串str
也是一个序列,对上面的例子稍加改动,配合map()
,我们就可以写出把str
转换为int
的函数:
>>> from functools import reduce
>>> def fn(x, y):
... return x * 10 + y
...
>>> def char2num(s):
... digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
... return digits[s]
...
>>> reduce(fn, map(char2num, '13579'))
13579
还可以用lambda函数进一步简化成:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def char2num(s):
return DIGITS[s]
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))
也就是说,假设Python没有提供int()
函数,你完全可以自己写一个把字符串转化为整数的函数,而且只需要几行代码!
练习:
- 利用
map()
函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT']
,输出:['Adam', 'Lisa', 'Bart']
:
def normalize(name):
return name[0].upper() + name[1:].lower()
#注意要有return
print(list(map(normalize,["YhGhh","CHeN","WWANF"])))
- python提供的
sum()
函数可以接受一个list并求和,请编写一个prod()
函数,可以接受一个list并利用reduce()
求积:
from functools import reduce
def prod(x,y):
return x*y
print(reduce(prod,[2,3,4]))
用lambda函数:
def prod(L):
return reduce(lambda x,y:x*y,L)
-
利用
map
和reduce
编写一个str2float
函数,把字符串'123.456'
转换成浮点数123.456
:from functools import reduce def str2float(s): DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9} def str2int(s): return DIGITS[s] i = s.index('.') return reduce(lambda x,y:10*x+y,map(str2int,s[:i]))+reduce(lambdax,y:10*x+y,map(str2int,s[i+1:]))/10**len(s[i+1:]) print(str2float("123.567"))
str.index(.)函数,返回点在字符串中的位置
2.1.2filter
Python内建的filter()
函数用于过滤序列。
和map()
类似,filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
def is_odd(n):
return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]
把一个序列中的空字符串删掉,可以这么写:
def not_empty(s):
return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
# 结果: ['A', 'B', 'C']
strip()函数法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
split函数,所得结果为list
>>> str="000303000"
>>> print(str.strip("0"))
303
>>> str="12321"
>>> str.split("3")
['12', '21']
注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list。
用filter求素数
首先,列出从2
开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取序列的第一个数2
,它一定是素数,然后用2
把序列的2
的倍数筛掉:
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数3
,它一定是素数,然后用3
把序列的3
的倍数筛掉:
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数5
,然后用5
把序列的5
的倍数筛掉:
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
不断筛下去,就可以得到所有的素数。
- 用Python来实现这个算法,可以先构造一个从
3
开始的奇数序列:
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
注意这是一个生成器,并且是一个无限序列。
- 然后定义一个筛选函数:
def _not_divisible(n):
return lambda x: x % n > 0
#_not_divisible(n)代表了lambda函数
- 最后,定义一个生成器,不断返回下一个素数:
def primes():
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列 filter函数第一个参数会是函数名,而此处带了参数n,并不是it中元素,而是n = next(it)的n,真正接受接收it中元素的是lambda中的x
这个生成器先返回第一个素数2
,然后,利用filter()
不断产生筛选后的新的序列。
- 由于
primes()
也是一个无限序列,所以调用时需要设置一个退出循环的条件:
# 打印1000以内的素数:
for n in primes():
if n < 1000:
print(n)
else:
break
练习
回数是指从左向右读和从右向左读都是一样的数,例如12321
,909
。请利用filter()
筛选出回数:
list的倒置:
>>> L=[2,4,6,7,8,2]
>>> L[::-1]
[2, 8, 7, 6, 4, 2]
def _odd_iter():
yield 1
n=1
while True:
n = n + 1
yield n
def is_palindrome(n):
L = str(n)
return L == L[::-1]
for s in filter(is_palindrome,_odd_iter()):
if s<100000:
print(s)
str(X)函数可以把一个数字转化为字符,还有int(X) float(X)
2.1.3sorted
默认从小到大排序
排序也是在程序中经常用到的算法,如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来
Python内置的sorted()
函数就可以对list进行排序:
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。
我们再看一个字符串排序的例子:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']
默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a'
,结果,大写字母Z
会排在小写字母a
的前面。
现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
这样,我们给sorted
传入key函数,即可实现忽略大小写的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True
:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
练习:
假设我们用一组tuple表示学生名字和成绩:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
请用sorted()
对上述列表分别按名字排序:
def byname(s):
return s[0]
print(sorted( [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)],key=byname,))
再按成绩从高到低排序:
def byscore(s):
return s[1]
print(sorted( [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)],key=byscore,reverse=True))
或:
def byscore(s):
return -s[1]
print(sorted( [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)],key=byscore))
sorted函数中的key代表的函数作用于可迭代对象的每一个参数,每个元素被key代表的函数处理以后,在对其进行sorted,但最后的排序结果按照原来的值进行输出
2.2返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回
什么是返回函数:
我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
在函数中在定义函数
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
调用函数f
时,才真正计算求和的结果:
>>> f()
25
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
f1()
和f2()
的调用结果互不影响。
闭包:
需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()
才执行。我们来看一个例子:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
全部都是9
!原因就在于返回的函数引用了变量i
,**但它并非立刻执行。**等到3个函数都返回时,它们所引用的变量i
已经变成了3
,因此最终结果为9
。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
再看看结果:
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9
缺点是代码较长,可利用lambda函数缩短代码
def count():
def f(j):
return lambda :j*j
fs=[]
for i in range(1,4):
fs.append(f(i))
return fs
a,b,c=count()
print(a(),b(),c())
2.2匿名函数
也就是lambda表达式
也可以把匿名函数作为返回值返回,比如:
def build(x, y):
return lambda: x * x + y * y
返回的是一个函数,得按照返回函数的那种情况来调用
...
>>> build(2,6)
<function build.<locals>.<lambda> at 0x000002679500E400>
>>> f=build(2,4)
>>> f()
20
2.3装饰器
-
由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
-
函数对象有一个
__name__
属性,可以拿到函数的名字>>> def now(): ... print('2015-3-25') ... >>> f = now >>> f() 2015-3-25 >>> now.__name__ 'now' >>> f.__name__ 'now'
现在,假设我们要增强now()
函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()
函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)(代理模式?)
所以,我们要定义一个能打印日志的decorator,可以定义如下:
def log(func)://函数作为参数,同时定义一个返回函数
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw) //返回一个函数,func变量中保存的是函数
return wrapper
察上面的log
,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@log
def now():
print('2015-3-25')
调用now()
函数,不仅会运行now()
函数本身,还会在运行now()
函数前打印一行日志:
>>> now()
call now():
2015-3-25
decorator就是一个返回函数的高阶函数
解释此段话:
1.高阶函数就是函数作为参数
2.返回函数就是返回值为一个函数(即在函数内部定义一个函数,并把这个函数返回
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
这个3层嵌套的decorator用法如下:
@log('execute')
def now():
print('2015-3-25')
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__
等属性,但你去看经过decorator装饰之后的函数,它们的__name__
已经从原来的'now'
变成了'wrapper'
:
>>> now.__name__
'wrapper'
因为返回的那个wrapper()
函数名字就是'wrapper'
,所以,需要把原始函数的__name__
等属性复制到wrapper()
函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__
这样的代码,Python内置的functools.wraps
就是干这个事的,所以,一个完整的decorator的写法如下:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
记住在定义wrapper()
的前面加上@functools.wraps(func)
或者针对带参数的decorator:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
例题:
请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间:
错误做法:
def metric(fn):
@functools.wraps(fn)
def wraper(*args,**ky):
start = time.time()
end = time.time()
print('%s executed in %s ms' % (fn.__name__, (end-start)))
return fn(*args, **ky)
return wraper
#并没有计算出时间
正确做法
def metric(fn):
@functools.wraps(fn)
def wraper(*args,**ky):
start = time.time()
ret = fn(*args, **ky)
end = time.time()
print('%s executed in %s ms' % (fn.__name__, (end-start)))
return ret
return wraper
2.4偏函数
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。
作用:
当函数的参数个数太多,需要简化时,使用functools.partial
可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。
例如:(偏函数出现的的缘由的例子)
-
int()
函数还提供额外的base
参数,默认值为10
。如果传入base
参数,就可以做N进制的转换 -
假设要转换大量的二进制字符串,每次都传入
int(x, base=2)
非常麻烦 -
于是我们可以建立一个新的函数,并且设立参数的默认值
def int2(x, base=2): return int(x, base)
这样,我们转换二进制就非常方便了:
>>> int2('1000000') 64 >>> int2('1010101') 85
然鹅functools.partial
就是帮助我们创建一个偏函数的,不需要我们自己定义int2()
,可以直接使用下面的代码创建一个新的函数int2
:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
创建偏函数时,实际上参数可以是函数对象、*args
和**kw
这3种参数
-
例如参数使用函数对象和关键字参数
int2 = functools.partial(int, base=2) int2('1010101') 相当于: kw = { 'base': 2 } int('10010', **kw)
-
例如参数使用函数对象和可变参数
max2 = functools.partial(max, 10) 实际上会把10作为*args的一部分自动加到左边,也就是: max2(5, 6, 7) 相当于: args = (10, 5, 6, 7) max(*args) 结果为10。
模块
-
在Python中,一个.py文件就称之为一个模块(Module)。
-
使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中
-
如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。
-
请注意,每一个包目录下面都会有一个
__init__.py
的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包 -
__init__.py
本身就是一个模块,而它的模块名就是mycompany
。 -
模块名要遵循Python变量命名规范,不要使用中文、特殊字符
使用模块
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
' a test module '
__author__ = 'Michael Liao'
import sys
def test():
args = sys.argv
if len(args)==1:
print('Hello, world!')
elif len(args)==2:
print('Hello, %s!' % args[1])
else:
print('Too many arguments!')
if __name__=='__main__':
test()
-
第1行和第2行是标准注释,第1行注释可以让这个
hello.py
文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码; -
第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;
-
类似
__xxx__
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊变量 -
import sys 是导入模块
-
sys
模块有一个argv
变量,用list存储了命令行的所有参数。argv
至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:运行
python3 hello.py
获得的sys.argv
就是['hello.py']
; -
最后,注意到这两行代码:
if __name__=='__main__': test()
作用:在命令行运行hello模块时,python解释器会把特殊变量__name__
置为__main__
,所以可以通过if判断,直接执行test函数。
如果是交互式的环境 import hello导入时,没有打印Hello, word!
,因为没有执行test()
函数调用hello.test()
时,才能打印出Hello, word!
作用域
- 类似
__xxx__
这样的变量是特殊变量,可以被直接引用 - 类似
_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用 - 之所以我们说,private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量
- 那它们有什么用呢?外部不需要引用的函数全部定义成private,只有外部需要引用的函数才定义为public
安装第三方模块
- 在Python中,安装第三方模块,是通过包管理工具pip完成的。
- 如果你正在使用Mac或Linux,安装pip本身这个步骤就可以跳过了。
- 一般来说,第三方库都会在Python官方的pypi.python.org网站注册
- 用pip一个一个安装费时费力,还需要考虑兼容性。我们推荐直接使用Anaconda
- 安装好Anaconda后,重新打开命令行窗口,输入python,可以看到Anaconda的信息
模块搜索路径:
-
当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错
>>> import mymodule Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: No module named mymodule
-
默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在
sys
模块的path
变量中:>>> import sys >>> sys.path ['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', ..., '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']
如果我们要添加自己的搜索目录,有两种方法:
一是直接修改sys.path
,添加要搜索的目录:
>>> import sys
>>> sys.path.append('/Users/michael/my_py_scripts')
这种方法是在运行时修改,运行结束后失效。
第二种方法是设置环境变量PYTHONPATH
,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加你自己的搜索路径,Python自己本身的搜索路径不受影响
遇到的问题:
在添加文件目录时要对\进行转义用\\
>>> import sys
>>> sys.path.append("E:\2020研究生暑假\python")
>>> import hello
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'hello'
>>> import hello.py
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'hello'
>>> sys.path
['', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\python35.zip', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\DLLs', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\lib', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\lib\\site-packages', 'E:\x820研究生暑假\\python']
>>> sys.path.append("E:\\2020研究生暑假\\python")
>>> sys.path
['', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\python35.zip', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\DLLs', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\lib', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35', 'C:\\Users\\hello\\AppData\\Local\\Programs\\Python\\Python35\\lib\\site-packages', 'E:\x820研究生暑假\\python', 'E:\\2020研究生暑假\\python']
>>> import hello
hello cwb
line1
...line2