Python入门学习

1. Python 基础

1.1 数据类型和变量

字符串是以单引号’或双引号"括起来的任意文本。

如果字符串里面有很多字符都需要转义,就需要加很多\,为了简化,Python还允许用r’‘表示’‘内部的字符串默认不转义:

1
2
3
print('\\\t\\')

print(r'\\\t\\')

output:

1
2
\	\
\\\t\\

如果字符串内部有很多换行,用\n写在一行里不好阅读,为了简化,Python允许用’’’…‘‘‘的格式表示多行内容:

1
2
3
print('''line1
line2
line3''')

output

1
2
3
line1
line2
line3

多行字符串’’’…‘‘‘还可以在前面加上r使用:

1
2
print(r'''hello,\n
world''')

output

1
2
hello,\n
world

在Python中,布尔值和布尔代数的表示完全一致,一个布尔值只有TrueFalse两种值(请注意大小写),也可以通过布尔运算计算出来。

布尔值可以用and、or和not运算。

空值是Python里一个特殊的值,用None表示。None不能理解为0,因为0是有意义的,而None是一个特殊的空值。

1
2
3
4
5
6
none = None

if none is None:
    print("is None")
else:
    print(none)

1.2 字符串和编码

字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。在最新的Python 3版本中,字符串是以Unicode编码的,也就是说,Python的字符串支持多语言。

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:

1
2
3
4
5
print(ord('A'))
print(ord('中'))

print(chr(66))
print(chr(25991))

output

1
2
3
4
65
20013
B

Python对bytes类型的数据用带b前缀的单引号或双引号表示:

1
2
3
x = b'ABC'

y = 'ABC'

要注意区分'ABC'b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。

以Unicode表示的str通过encode()方法可以编码为指定的bytes:

1
2
'ABC'.encode('ascii')
'中文'.encode('utf-8')

反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:

1
2
b'ABC'.decode('ascii')  # ABC
b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8') # 中文

如果bytes中只有一小部分无效的字节,可以传入errors=‘ignore’忽略错误的字节:

1
print(b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore'))

len()函数计算的是str的字符数,如果换成bytes,len()函数就计算字节数:

1
2
3
4
print(len(b'ABC'))  # 3
print(len('中文'))  # 2
print(len(b'\xe4\xb8\xad\xe6\x96\x87')) # 6
print(len('中文'.encode('utf-8')))  # 6

可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。

在Python中,采用的格式化方式和C语言是一致的,用%实现,举例如下:

1
2
3
4
name = input("请输入你的名字:")
age = input("请输入你的年龄:")

print('你好啊, %s! 你的年龄是 %d' % (name, int(age)))

有些时候,字符串里面的%是一个普通字符怎么办?这个时候就需要转义,用%%来表示一个%

1
print('growth rate: %d%%' % 7)

另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}{1}……,不过这种方式写起来比%要麻烦得多:

1
print('Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125))

最后一种格式化字符串的方法是使用以f开头的字符串,称之为f-string,它和普通字符串不同之处在于,字符串如果包含{xxx},就会以对应的变量替换:

1
2
3
4
r = 2.5
s = 3.14 * r ** 2

print(f'The area of a circle with radius {r} is {s:.2f}')

上述代码中,{r}被变量r的值替换,{s:.2f}被变量s的值替换,并且:后面的.2f指定了格式化参数(即保留两位小数),因此,{s:.2f}的替换结果是19.62。

1.3 使用list和tuple

1.3.1 list

Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。

1
classmates = ['Michael', 'Bob', 'Tracy']

变量classmates就是一个list。用len()函数可以获得list元素的个数:len(classmates)

用索引来访问list中每一个位置的元素:classmates[0]

如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素:classmates[-1]

list是一个可变的有序表,所以,可以往list中追加元素到末尾:classmates.append('Adam')

也可以把元素插入到指定的位置,比如索引号为1的位置:classmates.insert(1, 'Jack')

要删除list末尾的元素,用pop()方法:classmates.pop()

要删除指定位置的元素,用pop(i)方法,其中i是索引位置:classmates.pop(i)

要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:classmates[1] = 'Sarah'

list里面的元素的数据类型也可以不同,比如:L = ['Apple', 123, True]

list元素也可以是另一个list,比如:s = ['python', 'java', ['asp', 'php'], 'scheme']

要注意s只有4个元素,其中s[2]又是一个list,如果拆开写就更容易理解了:

1
2
p = ['asp', 'php']
s = ['python', 'java', p, 'scheme']

要拿到’php’可以写p[1]或者s[2][1],因此s可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。

如果一个list中一个元素也没有,就是一个空的list,它的长度为0:L = []

1.3.2 tuple

另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:

1
classmates = ('Michael', 'Bob', 'Tracy')

现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。

不可变的tuple有什么意义?因为tuple不可变,所以代码更安全。如果可能,能用tuple代替list就尽量用tuple。

tuple的陷阱:当你定义一个tuple时,在定义的时候,tuple的元素就必须被确定下来,比如:t = (1, 2)

如果要定义一个空的tuple,可以写成():t = ()

但是,要定义一个只有1个元素的tuple,如果你这么定义:t = (1)。定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。所以,只有1个元素的tuple定义时必须加一个逗号,,来消除歧义: t = (1,)

Python在显示只有1个元素的tuple时,也会加一个逗号,以免你误解成数学计算意义上的括号。

最后来看一个“可变的”tuple:

1
2
3
t = ('a', 'b', ['A', 'B'])
t[2][0] = 'X'
t[2][1] = 'Y'

这个tuple定义的时候有3个元素,分别是’a’,‘b’和一个list。不是说tuple一旦定义后就不可变了吗?怎么后来又变了?

定义tuple的时候包含的3个元素,当我们把list的元素’A’和’B’修改为’X’和’Y’后,表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list并没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。即指向'a',就不能改成指向'b',指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!

理解了“指向不变”后,要创建一个内容也不变的tuple怎么做?那就必须保证tuple的每一个元素本身也不能变。

小结:list和tuple是Python内置的有序集合,一个可变,一个不可变。根据需要来选择使用它们。

1.4 条件判断

if语句实现:

1
2
3
4
age = 20
if age >= 18:
    print('your age is', age)
    print('adult')

据Python的缩进规则,如果if语句判断是True,就把缩进的两行print语句执行了,否则,什么也不做。

也可以给if添加一个else语句,意思是,如果if判断是False,不要执行if的内容,去把else执行了:

1
2
3
4
5
6
7
age = 3
if age >= 18:
    print('your age is', age)
    print('adult')
else:
    print('your age is', age)
    print('teenager')

注意不要少写了冒号。

可以用elif做更细致的判断:

1
2
3
4
5
6
7
age = 3
if age >= 18:
    print('adult')
elif age >= 6:
    print('teenager')
else:
    print('kid')

elifelse if的缩写,完全可以有多个elif,所以if语句的完整形式就是:

1
2
3
4
5
6
7
8
if <条件判断1>:
    <执行1>
elif <条件判断2>:
    <执行2>
elif <条件判断3>:
    <执行3>
else:
    <执行4>

if语句执行有个特点,它是从上往下判断,如果在某个判断上是True,把该判断对应的语句执行后,就忽略掉剩下的elifelse

用input()读取用户的输入,这样可以自己输入:

1
2
3
4
5
birth = input('birth: ')
if birth < 2000:
    print('00前')
else:
    print('00后')

输入1998,结果报错:

1
2
3
 if birth < 2000:
       ^^^^^^^^^^^^
TypeError: '<' not supported between instances of 'str' and 'int'

这是因为input()返回的数据类型是str,str不能直接和整数比较,必须先把str转换成整数。Python提供了int()函数来完成这件事情:

1
2
3
4
5
6
s = input('birth: ')
birth = int(s)
if birth < 2000:
    print('00前')
else:
    print('00后')

int()函数发现一个字符串并不是合法的数字时就会报错,程序就退出。

1.5 循环

Python的循环有两种,一种是for…in循环,依次把list或tuple中的每个元素迭代出来,例子:

1
2
3
names = ['Michael', 'Bob', 'Tracy']
for name in names:
    print(name)

for x in ...循环就是把每个元素代入变量x,然后执行缩进块的语句。

Python提供一个range()函数,可以生成一个整数序列,再通过list()函数可以转换为list。比如range(5)生成的序列是从0开始小于5的整数:list(range(5)),输入[0, 1, 2, 3, 4]

第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:

1
2
3
4
5
6
sum = 0
n = 99
while n > 0:
    sum = sum + n
    n = n - 2
print(sum)

1.6 使用dict和set

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。

用Python写一个dict如下:

1
2
3
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}

print(d['Bob']) # 75

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:

1
2
3
4
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
d['Tao'] = 100

print(d)    # {'Michael': 95, 'Bob': 75, 'Tracy': 85, 'Tao': 100}

如果key不存在,dict就会报错。要避免key不存在的错误,有两种办法,一是通过in判断key是否存在:

1
2
3
4
5
6
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}

if 'Tao' in d:
    print(d['Tao'])
else:
    print(d)

通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:

1
2
3
4
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}

print('Empty' if d.get('Tao') is None else d['Tao'])
print(d.get('Tao', 'Empty'))

要删除一个key,用pop(key)方法,对应的value也会从dict中删除:

1
2
3
4
5
6
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}

if 'Tao' in d:
    d.pop('Tao')    # 删除不存在的元素会报错

print(d)

dict内部存放的顺序和key放入的顺序是没有关系的。

dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象

这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key。

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。要创建一个set,需要提供一个list作为输入集合:

1
2
3
s = {1, 2, 3}

print(s)    # {1, 2, 3}

注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。

重复元素在set中自动被过滤:s = set([1, 1, 2, 2, 3, 3])

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:s.add(4)

通过remove(key)方法可以删除元素:s.remove(4)

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

1
2
3
4
5
6
7
8
s1 = {1, 2, 3}
s2 = {2, 3, 4}

# 交集
print(s1 & s2)  # {2, 3}

# 并集
print(s1 | s2)  # {1, 2, 3, 4}

set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。

对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:

1
2
3
4
a = 'abc'

print(a.replace('a', 'A'))  # Abc
print(a)    # abc

要始终牢记的是,a是变量,而'abc'才是字符串对象!有些时候,我们经常说,对象a的内容是'abc',但其实是指a本身是一个变量,它指向的对象的内容才是'abc'。当我们调用a.replace('a', 'A')时,实际上调用方法replace是作用在字符串对象'abc'上的,而这个方法虽然名字叫replace,但却没有改变字符串'abc'的内容。相反,replace方法创建了一个新字符串'Abc'并返回,如果我们用变量b指向该新字符串,就容易理解了,变量a仍指向原有的字符串'abc',但变量b却指向新字符串'Abc'了。所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的

2. 函数

函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

1
2
3
a = max

print(a(1, 2))

2.1 定义函数

在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号,然后,在缩进块中编写函数体,函数的返回值用return语句返回。

我们以自定义一个求绝对值的my_abs函数为例:

1
2
3
4
5
6
7
8
def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x


print(my_abs(-99))

如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。

1
2
3
4
5
def my_fc():
    print('my_fc')


print(my_fc())

如果想定义一个什么事也不做的空函数,可以用pass语句:

1
2
def nop():
    pass

pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。

pass还可以用在其他语句里,比如:

1
2
if age >= 18:
    pass

缺少了pass,代码运行就会有语法错误。

函数可以返回多个值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import math


def move(x, y, step, angle=0):
    nx = x + step * math.cos(angle)
    ny = y - step * math.sin(angle)
    return nx, ny


x, y = move(100, 100, 60, int(math.pi / 6))

t = move(100, 100, 60, int(math.pi / 6))

print(x, y) # 160.0 100.0
print(t)    # (160.0, 100.0)

import math语句表示导入math包,并允许后续代码引用math包里的sincos等函数。

然后,我们就可以同时获得返回值,但其实这只是一种假象,Python函数返回的仍然是单一值。返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

2.2 函数的参数

默认参数:

1
2
3
4
5
6
def power(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s

设置默认参数时,有几点要注意:

  1. 必选参数在前,默认参数在后;
  2. 设置默认参数。当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

默认参数很有用,但使用不当,也会掉坑里。如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def add_end(L=[]):
    L.append('END')
    return L


print(add_end())
print(add_end())
print(add_end())


###
output:
    ['END']
    ['END', 'END']
    ['END', 'END', 'END']
###

当你使用默认参数调用时,一开始结果也是对的。但是,再次调用add_end()时,结果就不对了。

Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。

定义默认参数要牢记一点:默认参数必须指向不变对象!

要修改上面的例子,我们可以用None这个不变对象来实现:

1
2
3
4
5
def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L

在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。

由于参数个数不确定,我们可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def calc(numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum


# 调用的时候,需要先组装出一个list或tuple
print(calc([1, 2, 3]))
print(calc((1, 3, 5, 7)))

我们把函数的参数改为可变参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum


print(calc())
print(calc(1, 2, 3))
print(calc(1, 3, 5, 7))

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数。

如果已经有一个list或者tuple,要调用一个可变参数,Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

1
2
3
nums = [1, 2, 3]
print(calc(nums[0], nums[1], nums[2]))
print(calc(*nums))

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。

1
2
3
4
5
6
7
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)


person('Michael', 30)   # name: Michael age: 30 other: {}
person('Bob', 35, city='Beijing')   # name: Bob age: 35 other: {'city': 'Beijing'}
person('Adam', 45, gender='M', job='Engineer')  # name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数,也可以传入任意个数的关键字参数。

关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)


extra = {'city': 'Beijing', 'job': 'Engineer'}
# name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, city=extra['city'], job=extra['job'])

# 简化版
extra = {'city': 'Beijing', 'job': 'Engineer'}
# name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, **extra)

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。

以person()函数为例,我们希望检查是否有city和job参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def person(name, age, **kw):
    if 'city' in kw:
        # 有city参数
        pass
    if 'job' in kw:
        # 有job参数
        pass
    print('name:', name, 'age:', age, 'other:', kw)


person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。如下:

1
2
3
4
5
def person(name, age, *, city, job):
    print(name, age, city, job)


person('Jack', 24, city='Beijing', job='Engineer')

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了。命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。

命名关键字参数可以有缺省值,从而简化调用:

1
2
3
4
5
def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)


person('Jack', 24, job='Engineer')

由于命名关键字参数city具有默认值,调用时,可不传入city参数。

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:

1
2
3
def person(name, age, city, job):
    # 缺少 *,city和job被视为位置参数
    pass

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)


def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)


f1(1, 2)  # a = 1 b = 2 c = 0 args = () kw = {}
f1(1, 2, c=3)  # a = 1 b = 2 c = 3 args = () kw = {}
f1(1, 2, 3, 'a', 'b')  # a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
f1(1, 2, 3, 'a', 'b', x=99)  # a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
f2(1, 2, d=99, ext=None)    # a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

通过一个tuple和dict,也可以调用上述函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)


def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)


args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)  # a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}

args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kw)  # a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

所以,对于任意函数,无论它的参数是如何定义的,都可以通过类似func(*args, **kw)的形式调用它。

虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。

小结: Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

要注意定义可变参数和关键字参数的语法:

  • *args是可变参数,args接收的是一个tuple
  • **kw是关键字参数,kw接收的是一个dict

以及调用函数时如何传入可变参数和关键字参数的语法:

  • 可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))
  • 关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{'a': 1, 'b': 2})

使用*args**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。 命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。 定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。

2.3 递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

小结

  • 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
  • 针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。

Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。

3. 高级特性

3.1 切片

对经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作。

1
2
3
L = ['Michael', 'Sarah', 'Tracy']

print(L[1:3])   # ['Sarah', 'Tracy']

L[1:3]表示,从索引1开始取,直到索引3为止,但不包括索引3。即索引1,2,正好是2个元素。

类似的,既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片:

1
2
3
L = ['Michael', 'Sarah', 'Tracy']

print(L[-1:1]) # ['Tracy']

可以通过切片轻松取出某一段数列:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
L = list(range(100))

# 前10个数
print(L[:10])  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 后10个数
print(L[-10:])  # [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

# 前11-20个数
print(L[10:20])  # [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

# 前10个数,每两个取一个
print(L[:10:2])  # [0, 2, 4, 6, 8]

# 所有数,每5个取一个
print(L[::5])   # [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

# 甚至什么都不写,只写[:]就可以原样复制一个list
print(L[:])

tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:

1
print((0, 1, 2, 3, 4, 5)[:3])   # (0, 1, 2)

字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:

在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring),其实目的就是对字符串切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。

3.2 迭代

如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。

1
2
3
4
5
d = {'a': 1, 'b': 2, 'c': 3}

# 因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。
for key in d:
    print(key)

默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。

1
2
3
4
d = {'a': 1, 'b': 2, 'c': 3}

for val in d.values():
    print(val)

由于字符串也是可迭代对象,因此,也可以作用于for循环:

1
2
for ch in 'ABC':
    print(ch)

当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。那么,如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:

1
2
3
4
5
from collections.abc import Iterable

isinstance('abc', Iterable)  # str是否可迭代
isinstance([1, 2, 3], Iterable)  # list是否可迭代
isinstance(123, Iterable)  # 整数是否可迭代

如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

1
2
for i, value in enumerate(['A', 'B', 'C']):
    print(i, value)

在for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:

1
2
for x, y in [(1, 1), (2, 4), (3, 9)]:
    print(x, y)

小结:任何可迭代对象都可以作用于for循环,包括我们自定义的数据类型,只要符合迭代条件,就可以使用for循环。

3.3 列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。

举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11))

1
print(list(range(1, 11)))   # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环:

1
2
3
4
5
L = []
for x in range(1, 11):
    L.append(x * x)

print(L)    # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

1
2
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
print([x * x for x in range(1, 11)])

写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来。

for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:

1
2
# [4, 16, 36, 64, 100]
print([x * x for x in range(1, 11) if x % 2 == 0])

还可以使用两层循环,可以生成全排列:

1
2
# ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
print([m + n for m in 'ABC' for n in 'XYZ'])

例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:

1
2
3
4
5
import os  # 导入os模块

files = [d for d in os.listdir('E:\project\python_project_test')]  # os.listdir可以列出文件和目录

print(files)

for循环其实可以同时使用两个甚至多个变量,比如dictitems()可以同时迭代key和value:

1
2
3
d = {'x': 'A', 'y': 'B', 'z': 'C'}
for k, v in d.items():
    print(k, '=', v)

列表生成式也可以使用两个变量来生成list:

1
2
3
4
d = {'x': 'A', 'y': 'B', 'z': 'C'}

# ['x=A', 'y=B', 'z=C']
print([k + '=' + v for k, v in d.items()])

把一个list中所有的字符串变成小写:

1
2
3
4
L = ['Hello', 'World', 'IBM', 'Apple']

# ['hello', 'world', 'ibm', 'apple']
print([s.lower() for s in L])

例如,以下代码正常输出偶数:

1
2
# [2, 4, 6, 8, 10]
print([x for x in range(1, 11) if x % 2 == 0])

但是,我们不能在最后的if加上else:

1
2
3
 print([x for x in range(1, 11) if x % 2 == 0 else 0])
                                                 ^^^^
SyntaxError: invalid syntax

这是因为跟在for后面的if是一个筛选条件,不能带else,否则如何筛选?

把if写在for前面必须加else,否则报错:

1
2
3
 print([x if x % 2 == 0 for x in range(1, 11)])
           ^^^^^^^^^^^^^^^
SyntaxError: expected 'else' after 'if' expression

这是因为for前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据x计算出结果,因为缺少else,必须加上else:

1
2
# [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
print([x if x % 2 == 0 else -x for x in range(1, 11)])

使用内建的isinstance函数可以判断一个变量是不是某个类型:

1
2
3
4
5
x = 'abc'
y = 123

print(isinstance(x, str))
print(isinstance(y, int))

3.4 生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

创建一个generator,第一种方法很简单,把一个列表生成式的[]改成(),就创建了一个generator:

1
2
3
4
5
L = [x * x for x in range(10)]
g = (x * x for x in range(10))

print(L)    # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(g)    # <generator object <genexpr> at 0x000001A2334C9150>

创建L和g的区别仅在于最外层的[](),L是一个list,而g是一个generator。

如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator。generator函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

举个简单的例子,定义一个generator函数,依次返回数字1,3,5:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)


o = odd()
print(next(o))  # step 1 1
print(next(o))  # step 2 3
print(next(o))  # step 3 5
print(next(o))  # StopIteration

调用该generator函数时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值。可以看到,odd不是普通函数,而是generator函数,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。

请务必注意:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。

这样调用next()每次都返回1:

1
2
3
next(odd()) # step 1 1
next(odd()) # step 1 1
next(odd()) # step 1 1

原因在于odd()会创建一个新的generator对象,上述代码实际上创建了3个完全独立的generator,对3个generator分别调用next()当然每个都会返回第一个值。正确的写法是创建一个generator对象,然后不断对这一个generator对象调用next()

我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'


g = fib(6)

while True:
    try:
        x = next(g)
        print('g:', x)
    except StopIteration as e:
        print('Generator return value:', e.value)
        break

3.5 迭代器

我们已经知道,可以直接作用于for循环的数据类型有以下几种:

  • 一类是集合数据类型,如list、tuple、dict、set、str等;
  • 一类是generator,包括生成器和带yield的generator function。

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。

可以使用isinstance()判断一个对象是否是Iterable对象:

1
2
3
4
5
6
7
from collections.abc import Iterable

print(isinstance([], Iterable))  # True
print(isinstance({}, Iterable))  # True
print(isinstance('abc', Iterable))  # True
print(isinstance((x for x in range(10)), Iterable))  # True
print(isinstance(100, Iterable))  # False

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。

可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。

可以使用isinstance()判断一个对象是否是Iterator对象:

1
2
3
4
5
6
from collections.abc import Iterator

print(isinstance((x for x in range(10)), Iterator))  # True
print(isinstance([], Iterator))  # False
print(isinstance({}, Iterator))  # False
print(isinstance('abc', Iterator))  # False

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator

把list、dict、str等Iterable变成Iterator可以使用iter()函数:

1
2
3
4
from collections.abc import Iterator

print(isinstance(iter([]), Iterator))
print(isinstance(iter('abc'), Iterator))

Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

小结:

  • 凡是可作用于for循环的对象都是Iterable类型;
  • 凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
  • 集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

Python的for循环本质上就是通过不断调用next()函数实现的,例如:

1
2
for x in [1, 2, 3, 4, 5]:
    pass

实际上完全等价于:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(it)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break

4. 函数式编程

函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。

4.1 高阶函数

变量可以指向函数:

1
2
3
f = abs

print(f(-10))   # 10

变量f指向了abs函数本身。直接调用abs()函数和调用变量f()完全相同。

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

1
2
3
4
5
def add(x, y, f):
    return f(x) + f(y)


print(add(-5, 6, abs))  # 11

4.1.2 map/reduce

Python内建了map()reduce()函数。

map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
            f(x) = x * x

  ┌───┬───┬───┬───┼───┬───┬───┬───┐
  │   │   │   │   │   │   │   │   │
  ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼

[ 1   2   3   4   5   6   7   8   9 ]

  │   │   │   │   │   │   │   │   │
  │   │   │   │   │   │   │   │   │
  ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼

[ 1   4   9  16  25  36  49  64  81 ]
1
2
3
4
5
6
7
def f(x):
    return x * x


r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])

print(list(r))  # [1, 4, 9, 16, 25, 36, 49, 64, 81]

map()传入的第一个参数是f,即函数对象本身。由于结果r是一个IteratorIterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list

map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:

1
2
3
4
5
6
def f(x):
    return x * x


# ['1', '2', '3', '4', '5', '6', '7', '8', '9']
print(list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])))

reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

1
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

比方说对一个序列求和,就可以用reduce实现:

1
2
3
4
5
6
7
8
from functools import reduce


def add(x, y):
    return x + y


print(reduce(add, [1, 3, 5, 7, 9]))     # 25

把序列[1, 3, 5, 7, 9]变换成整数13579:

1
2
3
4
5
6
7
8
from functools import reduce


def fn(x, y):
    return x * 10 + y


print(reduce(fn, [1, 3, 5, 7, 9]))     # 13579

我们就可以写出把str转换为int的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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]


print(reduce(fn, map(char2num, '13579')))     # 13579

整理成一个str2int的函数就是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 str2int(s):
    def fn(x, y):
        return x * 10 + y

    def char2num(s):
        return DIGITS[s]

    return reduce(fn, map(char2num, s))


print(str2int("13223"))     # 13223

用lambda函数进一步简化成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 normalize(name):
    return name.capitalize()


L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)   # ['Adam', 'Lisa', 'Bart']
def char2num(s):


    return DIGITS[s]


def str2int(s):
    return reduce(lambda x, y: x * 10 + y, map(char2num, s))


print(str2int("13223"))     # 13223

利用map()函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT'],输出:['Adam', 'Lisa', 'Bart']

1
2
3
4
5
6
7
def normalize(name):
    return name.capitalize()


L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)   # ['Adam', 'Lisa', 'Bart']

Python提供的sum()函数可以接受一个list并求和,请编写一个prod()函数,可以接受一个list并利用reduce()求积:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from functools import reduce


def prod(L):
    return reduce(lambda x, y: x * y, L)


print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))

if prod([3, 5, 7, 9]) == 945:
    print('测试成功!')
else:
    print('测试失败!')

利用map和reduce编写一个str2float函数,把字符串'123.456’转换成浮点数123.456:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from functools import reduce


def str2float(s):
    # 将字符串按小数点分割成整数部分和小数部分
    parts = s.split('.')
    # 将整数部分和小数部分分别转换为数字
    integer = reduce(lambda x, y: x * 10 + y, map(int, parts[0]))
    decimal = reduce(lambda x, y: x * 10 + y, map(int, parts[1])) / (10 ** len(parts[1]))
    # 返回整数部分和小数部分的和
    return integer + decimal


print('str2float(\'123.456\') =', str2float('123.456'))
if abs(str2float('123.456') - 123.456) < 0.00001:
    print('测试成功!')
else:
    print('测试失败!')

4.1.2 filter

Python内建的filter()函数用于过滤序列。

map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

例如,在一个list中,删掉偶数,只保留奇数,可以这么写:

1
2
3
4
5
def is_odd(n):
    return n % 2 == 1


print(list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15])))    # [1, 5, 9, 15]

把一个序列中的空字符串删掉,可以这么写:

1
2
3
4
5
6
def not_empty(s):
    return s and s.strip()


# ['A', 'B', 'C']
print(list(filter(not_empty, ['A', '', 'B', None, 'C', '  '])))

可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list

filter()的作用是从一个序列中筛出符合条件的元素。由于filter()使用了惰性计算,所以只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素。

4.1.3 sorted

排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

Python内置的sorted()函数就可以对list进行排序:

1
2
# [-21, -12, 5, 9, 36]
print(sorted([36, 5, -12, 9, -21]))

此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

1
2
# [5, 9, -12, -21, 36]
print(sorted([36, 5, -12, 9, -21], key=abs))

key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs处理过的list:

1
2
3
list = [36, 5, -12, 9, -21]

keys = [36, 5,  12, 9,  21]

然后sorted()函数按照keys进行排序,并按照对应关系返回list相应的元素:

1
2
3
keys排序结果 => [5, 9,  12,  21, 36]
                |  |    |    |   |
最终结果     => [5, 9, -12, -21, 36]

一个字符串排序的例子:

1
2
# ['Credit', 'Zoo', 'about', 'bob']
print(sorted(['bob', 'about', 'Zoo', 'Credit']))

默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a',结果,大写字母Z会排在小写字母a的前面。

实现忽略大小写的排序:

1
2
# ['about', 'bob', 'Credit', 'Zoo']
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower))

要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True

1
2
# ['Zoo', 'Credit', 'bob', 'about']
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True))

sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数。

假设我们用一组tuple表示学生名字和成绩:

1
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]

用sorted()对上述列表分别按名字排序:

1
2
3
4
5
6
7
8
9
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]


def by_name(t):
    return t[0]


# [('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]
print(sorted(L, key=by_name))

按成绩从高到低排序:

1
2
3
4
5
6
7
8
9
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]


def by_score(t):
    return t[1]


# [('Bart', 66), ('Bob', 75), ('Lisa', 88), ('Adam', 92)]
print(sorted(L, key=by_score))

4.2 返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:

1
2
3
4
5
def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax

但是,如果不需要立刻求和,可以不返回求和的结果,而是返回求和的函数:

1
2
3
4
5
6
7
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum


f = lazy_sum(1, 3, 5, 7, 9)
print(f())  # 25

在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。

注意,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum


f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1() == f2())  # True     f1()和f2()的调用结果互不影响。
print(f1 == f2)     # False

注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。我们来看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs


f1, f2, f3 = count()
print(f1())  # 9
print(f2())  # 9
print(f3())  # 9

在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:9 9 9;原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。

返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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()
print(f1())  # 1
print(f2())  # 4
print(f3())  # 9

使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def inc():
    x = 0

    def fn():
        # 仅读取x的值:
        return x + 1

    return fn


f = inc()
print(f())  # 1
print(f())  # 1

但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def inc():
    x = 0
    def fn():
        # nonlocal x
        x = x + 1   # UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
        return x
    return fn

f = inc()
print(f()) # 1
print(f()) # 2

原因是x作为局部变量并没有初始化,直接计算x+1是不行的。但我们其实是想引用inc()函数内部的x,所以需要在fn()函数内部加一个nonlocal x的声明。加上这个声明后,解释器把fn()的x看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1。

使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。

小结:

  • 一个函数可以返回一个计算结果,也可以返回一个函数。
  • 返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。

5. 模块

在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。

以内建的sys模块为例,编写一个hello的模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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()

6. 面向对象编程

在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块(Module)

6.1 类和实例

在Python中,定义类是通过class关键字:

1
2
class Student(object):
    pass

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Student:
    pass


bart = Student()
bart.name = 'Bart Simpson'
bart.old = 18
print(bart.name)    # Bart Simpson
print(bart.old)     # 18
print(bart)         # <__main__.Student object at 0x00000186F7A6D0D0>

由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name,score等属性绑上去:

1
2
3
4
5
class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

注意:特殊方法__init__前后分别有两个下划线!!!

注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score


bart = Student('Bart Simpson', 60)
print(bart.name)    # Bart Simpson
print(bart.score)   # 60
print(bart)         # <__main__.Student object at 0x00000219DE34DC10>

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

0%