Python入门学习
1. Python 基础
1.1 数据类型和变量
字符串是以单引号’或双引号"括起来的任意文本。
如果字符串里面有很多字符都需要转义,就需要加很多\,为了简化,Python还允许用r’‘表示’‘内部的字符串默认不转义:
|
|
output:
|
|
如果字符串内部有很多换行,用\n写在一行里不好阅读,为了简化,Python允许用’’’…‘‘‘的格式表示多行内容:
|
|
output
|
|
多行字符串’’’…‘‘‘还可以在前面加上r使用:
|
|
output
|
|
在Python中,布尔值和布尔代数的表示完全一致,一个布尔值只有True
、False
两种值(请注意大小写),也可以通过布尔运算计算出来。
布尔值可以用and、or和not运算。
空值是Python里一个特殊的值,用None表示。None不能理解为0,因为0是有意义的,而None是一个特殊的空值。
|
|
1.2 字符串和编码
字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。在最新的Python 3版本中,字符串是以Unicode编码的,也就是说,Python的字符串支持多语言。
对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:
|
|
output
|
|
Python对bytes类型的数据用带b前缀的单引号或双引号表示:
|
|
要注意区分'ABC'
和b'ABC'
,前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。
以Unicode表示的str通过encode()方法可以编码为指定的bytes:
|
|
反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:
|
|
如果bytes中只有一小部分无效的字节,可以传入errors=‘ignore’忽略错误的字节:
|
|
len()函数计算的是str的字符数,如果换成bytes,len()函数就计算字节数:
|
|
可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
在Python中,采用的格式化方式和C语言是一致的,用%
实现,举例如下:
|
|
有些时候,字符串里面的%
是一个普通字符怎么办?这个时候就需要转义,用%%
来表示一个%
:
|
|
另一种格式化字符串的方法是使用字符串的format()
方法,它会用传入的参数依次替换字符串内的占位符{0}
、{1}
……,不过这种方式写起来比%要麻烦得多:
|
|
最后一种格式化字符串的方法是使用以f开头的字符串,称之为f-string
,它和普通字符串不同之处在于,字符串如果包含{xxx}
,就会以对应的变量替换:
|
|
上述代码中,{r}
被变量r的值替换,{s:.2f}
被变量s的值替换,并且:后面的.2f指定了格式化参数(即保留两位小数),因此,{s:.2f}
的替换结果是19.62。
1.3 使用list和tuple
1.3.1 list
Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。
|
|
变量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,如果拆开写就更容易理解了:
|
|
要拿到’php’可以写p[1]或者s[2][1],因此s可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。
如果一个list中一个元素也没有,就是一个空的list,它的长度为0:L = []
1.3.2 tuple
另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:
|
|
现在,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:
|
|
这个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语句实现:
|
|
据Python的缩进规则,如果if
语句判断是True
,就把缩进的两行print
语句执行了,否则,什么也不做。
也可以给if
添加一个else
语句,意思是,如果if
判断是False
,不要执行if
的内容,去把else
执行了:
|
|
注意不要少写了冒号。
可以用elif做更细致的判断:
|
|
elif
是else if
的缩写,完全可以有多个elif
,所以if
语句的完整形式就是:
|
|
if
语句执行有个特点,它是从上往下判断,如果在某个判断上是True
,把该判断对应的语句执行后,就忽略掉剩下的elif
和else
。
用input()读取用户的输入,这样可以自己输入:
|
|
输入1998,结果报错:
|
|
这是因为input()返回的数据类型是str,str不能直接和整数比较,必须先把str转换成整数。Python提供了int()函数来完成这件事情:
|
|
int()函数发现一个字符串并不是合法的数字时就会报错,程序就退出。
1.5 循环
Python的循环有两种,一种是for…in循环,依次把list或tuple中的每个元素迭代出来,例子:
|
|
for x in ...
循环就是把每个元素代入变量x
,然后执行缩进块的语句。
Python提供一个range()
函数,可以生成一个整数序列,再通过list()
函数可以转换为list
。比如range(5)生成的序列是从0开始小于5的整数:list(range(5))
,输入[0, 1, 2, 3, 4]
。
第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:
|
|
1.6 使用dict和set
Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。
用Python写一个dict如下:
|
|
把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:
|
|
如果key不存在,dict就会报错。要避免key不存在的错误,有两种办法,一是通过in判断key是否存在:
|
|
通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:
|
|
要删除一个key,用pop(key)
方法,对应的value也会从dict中删除:
|
|
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]是一个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可以做数学意义上的交集、并集等操作:
|
|
set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。
对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:
|
|
要始终牢记的是,a
是变量,而'abc'
才是字符串对象!有些时候,我们经常说,对象a的内容是'abc'
,但其实是指a
本身是一个变量,它指向的对象的内容才是'abc'
。当我们调用a.replace('a', 'A')
时,实际上调用方法replace
是作用在字符串对象'abc'上
的,而这个方法虽然名字叫replace
,但却没有改变字符串'abc'
的内容。相反,replace
方法创建了一个新字符串'Abc'
并返回,如果我们用变量b
指向该新字符串,就容易理解了,变量a
仍指向原有的字符串'abc'
,但变量b却指向新字符串'Abc'
了。所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。
2. 函数
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
|
|
2.1 定义函数
在Python中,定义一个函数要使用def
语句,依次写出函数名、括号、括号中的参数和冒号,然后,在缩进块中编写函数体,函数的返回值用return语句返回。
我们以自定义一个求绝对值的my_abs函数为例:
|
|
如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。
|
|
如果想定义一个什么事也不做的空函数,可以用pass语句:
|
|
pass
语句什么都不做,那有什么用?实际上pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
,让代码能运行起来。
pass还可以用在其他语句里,比如:
|
|
缺少了pass,代码运行就会有语法错误。
函数可以返回多个值:
|
|
import math
语句表示导入math
包,并允许后续代码引用math
包里的sin
、cos
等函数。
然后,我们就可以同时获得返回值,但其实这只是一种假象,Python函数返回的仍然是单一值。返回值是一个tuple
!但是,在语法上,返回一个tuple
可以省略括号,而多个变量可以同时接收一个tuple
,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple
,但写起来更方便。
2.2 函数的参数
默认参数:
|
|
设置默认参数时,有几点要注意:
- 必选参数在前,默认参数在后;
- 设置默认参数。当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
默认参数很有用,但使用不当,也会掉坑里。如下:
|
|
当你使用默认参数调用时,一开始结果也是对的。但是,再次调用add_end()时,结果就不对了。
Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None这个不变对象来实现:
|
|
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
由于参数个数不确定,我们可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:
|
|
我们把函数的参数改为可变参数:
|
|
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数。
如果已经有一个list或者tuple,要调用一个可变参数,Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
|
|
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
|
|
函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数,也可以传入任意个数的关键字参数。
关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
|
|
**extra
表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。
以person()函数为例,我们希望检查是否有city和job参数:
|
|
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。如下:
|
|
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了。命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。
命名关键字参数可以有缺省值,从而简化调用:
|
|
由于命名关键字参数city具有默认值,调用时,可不传入city参数。
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数:
|
|
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
|
|
通过一个tuple和dict,也可以调用上述函数:
|
|
所以,对于任意函数,无论它的参数是如何定义的,都可以通过类似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)操作符,能大大简化这种操作。
|
|
L[1:3]
表示,从索引1开始取,直到索引3为止,但不包括索引3。即索引1,2,正好是2个元素。
类似的,既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片:
|
|
可以通过切片轻松取出某一段数列:
|
|
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
|
|
字符串'xxx'
也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring),其实目的就是对字符串切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。
3.2 迭代
如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。
|
|
默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。
|
|
由于字符串也是可迭代对象,因此,也可以作用于for循环:
|
|
当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。那么,如何判断一个对象是可迭代对象呢?方法是通过collections.abc模块的Iterable类型判断:
|
|
如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
|
|
在for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:
|
|
小结:任何可迭代对象都可以作用于for循环,包括我们自定义的数据类型,只要符合迭代条件,就可以使用for循环。
3.3 列表生成式
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
可以用list(range(1, 11))
:
|
|
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]
怎么做?方法一是循环:
|
|
但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:
|
|
写列表生成式时,把要生成的元素x * x
放到前面,后面跟for
循环,就可以把list创建出来。
for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
|
|
还可以使用两层循环,可以生成全排列:
|
|
例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:
|
|
for循环其实可以同时使用两个甚至多个变量,比如dict
的items()
可以同时迭代key和value:
|
|
列表生成式也可以使用两个变量来生成list:
|
|
把一个list中所有的字符串变成小写:
|
|
例如,以下代码正常输出偶数:
|
|
但是,我们不能在最后的if加上else:
|
|
这是因为跟在for后面的if是一个筛选条件,不能带else,否则如何筛选?
把if写在for前面必须加else,否则报错:
|
|
这是因为for前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0
,它无法根据x计算出结果,因为缺少else,必须加上else:
|
|
使用内建的isinstance函数可以判断一个变量是不是某个类型:
|
|
3.4 生成器
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
创建一个generator,第一种方法很简单,把一个列表生成式的[]改成(),就创建了一个generator:
|
|
创建L和g的区别仅在于最外层的[]
和()
,L是一个list,而g是一个generator。
如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator。generator函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
举个简单的例子,定义一个generator函数,依次返回数字1,3,5:
|
|
调用该generator函数时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值。可以看到,odd不是普通函数,而是generator函数,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。
请务必注意:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。
这样调用next()每次都返回1:
|
|
原因在于odd()会创建一个新的generator对象,上述代码实际上创建了3个完全独立的generator,对3个generator分别调用next()当然每个都会返回第一个值。正确的写法是创建一个generator对象,然后不断对这一个generator对象调用next()
。
我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
|
|
3.5 迭代器
我们已经知道,可以直接作用于for循环的数据类型有以下几种:
- 一类是集合数据类型,如list、tuple、dict、set、str等;
- 一类是generator,包括生成器和带yield的generator function。
这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。
可以使用isinstance()判断一个对象是否是Iterable对象:
|
|
而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。
可以使用isinstance()判断一个对象是否是Iterator对象:
|
|
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。
把list、dict、str等Iterable变成Iterator可以使用iter()函数:
|
|
Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
小结:
- 凡是可作用于for循环的对象都是Iterable类型;
- 凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
- 集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。
Python的for循环本质上就是通过不断调用next()函数实现的,例如:
|
|
实际上完全等价于:
|
|
4. 函数式编程
函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
4.1 高阶函数
变量可以指向函数:
|
|
变量f指向了abs函数本身。直接调用abs()函数和调用变量f()完全相同。
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
|
|
4.1.2 map/reduce
Python内建了map()
和reduce()
函数。
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
举例说明,比如我们有一个函数f(x)=x2
,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用map()
实现如下:
|
|
|
|
map()
传入的第一个参数是f
,即函数对象本身。由于结果r
是一个Iterator
,Iterator
是惰性序列,因此通过list()
函数让它把整个序列都计算出来并返回一个list
。
map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2
,还可以计算任意复杂的函数,比如,把这个list
所有数字转为字符串:
|
|
reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
|
|
比方说对一个序列求和,就可以用reduce
实现:
|
|
把序列[1, 3, 5, 7, 9]
变换成整数13579
:
|
|
我们就可以写出把str转换为int的函数:
|
|
整理成一个str2int的函数就是:
|
|
用lambda函数进一步简化成:
|
|
利用map()
函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT']
,输出:['Adam', 'Lisa', 'Bart']
:
|
|
Python提供的sum()
函数可以接受一个list并求和,请编写一个prod()
函数,可以接受一个list
并利用reduce()
求积:
|
|
利用map和reduce编写一个str2float函数,把字符串'123.456’转换成浮点数123.456:
|
|
4.1.2 filter
Python内建的filter()函数用于过滤序列。
和map()
类似,filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
|
|
把一个序列中的空字符串删掉,可以这么写:
|
|
可见用filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list
。
filter()
的作用是从一个序列中筛出符合条件的元素。由于filter()
使用了惰性计算,所以只有在取filter()
结果的时候,才会真正筛选并每次返回下一个筛出的元素。
4.1.3 sorted
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
Python内置的sorted()
函数就可以对list进行排序:
|
|
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:
|
|
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs处理过的list:
|
|
然后sorted()函数按照keys进行排序,并按照对应关系返回list相应的元素:
|
|
一个字符串排序的例子:
|
|
默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a'
,结果,大写字母Z会排在小写字母a的前面。
实现忽略大小写的排序:
|
|
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True
:
|
|
sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数。
假设我们用一组tuple表示学生名字和成绩:
|
|
用sorted()对上述列表分别按名字排序:
|
|
按成绩从高到低排序:
|
|
4.2 返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
|
|
但是,如果不需要立刻求和,可以不返回求和的结果,而是返回求和的函数:
|
|
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
|
|
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
注意,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
|
|
注意到返回的函数在其定义内部引用了局部变量args
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()
才执行。我们来看一个例子:
|
|
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是:9 9 9;原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
|
|
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常:
|
|
但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错:
|
|
原因是x作为局部变量并没有初始化,直接计算x+1是不行的。但我们其实是想引用inc()函数内部的x,所以需要在fn()函数内部加一个nonlocal x的声明。加上这个声明后,解释器把fn()的x看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1。
使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。
小结:
- 一个函数可以返回一个计算结果,也可以返回一个函数。
- 返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。
5. 模块
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。
以内建的sys模块为例,编写一个hello的模块:
|
|
6. 面向对象编程
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。
为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块(Module)。
6.1 类和实例
在Python中,定义类是通过class关键字:
|
|
class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
|
|
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name,score等属性绑上去:
|
|
注意:特殊方法
__init__
前后分别有两个下划线!!!
注意到__init__
方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:
|
|
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。