python语法进阶

高级特性

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循环其实可以同时使用两个甚至多个变量,比如dictitems()可以同时迭代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。

创建生成器

  1. 第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个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>
    

    创建Lg的区别仅在于最外层的[]()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'
    
  2. 以上就是定义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错误,返回值包含在StopIterationvalue中:

    >>> 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循环的数据类型有以下几种:

一类是集合数据类型,如listtupledictsetstr等;

一类是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对象,但listdictstr虽然是Iterable,却不是Iterator

listdictstrIterable变成Iterator可以使用**iter()函数**:

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

你可能会问,为什么listdictstr等数据类型不是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()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的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是一个IteratorIterator是惰性序列,因此通过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]变换成整数13579reduce就可以派上用场:

>>> 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()函数,你完全可以自己写一个把字符串转化为整数的函数,而且只需要几行代码!

练习:

  1. 利用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"])))
  1. 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)
  1. 利用mapreduce编写一个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

练习

回数是指从左向右读和从右向左读都是一样的数,例如12321909。请利用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()结果应该是149,但实际结果是:

>>> 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可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单

例如:(偏函数出现的的缘由的例子)

  1. int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换

  2. 假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦

  3. 于是我们可以建立一个新的函数,并且设立参数的默认值

    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种参数

  1. 例如参数使用函数对象和关键字参数

    int2 = functools.partial(int, base=2)
    int2('1010101')
    相当于:
    
    kw = { 'base': 2 }
    int('10010', **kw)
    
  2. 例如参数使用函数对象和可变参数

    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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值