转载自:
https://www.liaoxuefeng.com/wiki/1016959663602400
前言
初级篇涉及内容如下:
数据类型
变量
常量
编码和格式化
运算符
条件+循环
函数
高级特性
函数式编程
1. 数据类型
Numbers(数字)
String(字符串)
Boolean(布尔值)
None(空值)
List(列表)
Tuple(元组)
Dictionary(字典)
- Set
后三个属于集合类型。
1. 数字(Numbers)
- int(有符号整型)
- long(长整型[也可以代表八进制和十六进制])
- float(浮点型)
- complex(复数)
整数和浮点数在计算机内部存储的方式是不同的,整数运算永远是精确的(除法难道也是精确的?是的!),而浮点数运算则可能会有四舍五入的误差。
2. 字符串(String)
字符串或串(String)是由数字、字母、下划线组成的一串字符。
它是编程语言中表示文本的数据类型。
Python的字串列表有2种取值顺序:
- 从左到右索引默认0开始的,最大范围是字符串长度少1
- 从右到左索引默认-1开始的,最大范围是字符串开头
左闭右开
1 | s = 'i love python' |
注意换行符的使用
如果字符串里面有很多字符都需要转义,就需要加很多\
,为了简化,Python还允许用r''
表示''
内部的字符串默认不转义。
1 | >>> print('\\\t\\') |
如果字符串内部有很多换行,用\n
写在一行里不好阅读,为了简化,Python允许用'''...'''
的格式表示多行内容,可以自己试试:
1 | >>> print('''line1 |
3. Boolean(布尔值)
- True
- False
布尔值可以使用 and 、or、 not运算,经常用语条件判断:
1 | if age >= 18: |
4. None(空值)
空值是Python里一个特殊的值,用None
表示。None
不能理解为0
,因为0
是有意义的,而None
是一个特殊的空值。
5. 列表(List)可变
Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。
比如,列出班里所有同学的名字,就可以用一个list表示:
1 | >>> classmates = ['Michael', 'Bob', 'Tracy'] |
变量classmates
就是一个list。用len()
函数可以获得list元素的个数:
1 | >>> len(classmates) |
用索引来访问list中每一个位置的元素,索引是从0
开始的:
1 | >>> classmates[0] |
当索引超出了范围时,Python会报一个IndexError
错误,所以,要确保索引不要越界,记得最后一个元素的索引是len(classmates) - 1
。
如果要取最后一个元素,除了计算索引位置外,还可以用-1
做索引,直接获取最后一个元素:
1 | >>> classmates[-1] |
list是一个可变的有序表,所以,可以往list中追加元素到末尾:
1 | >>> classmates.append('Adam') |
也可以把元素插入到指定的位置,比如索引号为1
的位置:
1 | >>> classmates.insert(1, 'Jack') |
要删除list末尾的元素,用pop()
方法:
1 | >>> classmates.pop() |
要删除指定位置的元素,用pop(i)
方法,其中i
是索引位置:
1 | >>> classmates.pop(1) |
要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:
1 | >>> classmates[1] = 'Sarah' |
list里面的元素的数据类型也可以不同,比如:
1 | >>> L = ['Apple', 123, True] |
list元素也可以是另一个list,比如:
1 | >>> s = ['python', 'java', ['asp', 'php'], 'scheme'] |
要注意s
只有4个元素,其中s[2]
又是一个list,如果拆开写就更容易理解了:
1 | >>> p = ['asp', 'php'] |
要拿到'php'
可以写p[1]
或者s[2][1]
,因此s
可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。
如果一个list中一个元素也没有,就是一个空的list,它的长度为0:
1 | >>> L = [] |
操作实例
1 | list = ['apple','jack',798,2.22,36] |
6. 元祖(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的元素就必须被确定下来,比如:
1 | >>> t = (1, 2) |
如果要定义一个空的tuple,可以写成()
:
1 | >>> t = () |
但是,要定义一个只有1个元素的tuple,如果你这么定义:
1 | >>> t = (1) |
定义的不是tuple,是1
这个数!这是因为括号()
既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1
。
所以,只有1个元素的tuple定义时必须加一个逗号,
,来消除歧义:
1 | >>> t = (1,) |
来看一个“可变的”tuple:
1 | >>> t = ('a', 'b', ['A', 'B']) |
表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list并没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。即指向'a'
,就不能改成指向'b'
,指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!
理解了“指向不变”后,要创建一个内容也不变的tuple怎么做?那就必须保证tuple的每一个元素本身也不能变。
7. 字典(dict)
字典(dictionary)是除列表以外Python之中最灵活的内置数据结构类型。列表是有序的对象结合,字典是无序的对象集合。
字典用”{ }”标识。字典由索引(key)和它对应的值value组成。
在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。
1 | >>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85} |
多次对一个key放入value,后面的值会把前面的值冲掉:
1 | >>> d['Jack'] = 90 |
如果key不存在,dict就会报错,免key不存在的错误,有两种办法。
一是通过in
判断key是否存在:
1 | >>> 'Thomas' in d |
二是通过dict提供的get()
方法,如果key不存在,可以返回None
,或者自己指定的value:
1 | >>> d.get('Thomas') |
要删除一个key,用pop(key)
方法,对应的value也会从dict中删除:
1 | >>> d.pop('Bob') |
8. set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
1 | >>> s = set([1, 2, 3]) |
注意,传入的参数[1, 2, 3]
是一个list,而显示的{1, 2, 3}
只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。
重复元素在set中自动被过滤:
1 | >>> s = set([1, 1, 2, 2, 3, 3]) |
通过add(key)
方法可以添加元素到set中,可以重复添加,但不会有效果:
1 | >>> s.add(4) |
通过remove(key)
方法可以删除元素:
1 | >>> s.remove(4) |
set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
1 | >>> s1 = set([1, 2, 3]) |
9. 再议不可变对象
str是不变对象,而list是可变对象。
对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:
1 | >>> a = ['c', 'b', 'a'] |
而对于不可变对象,比如str,对str进行操作呢:
1 | >>> a = 'abc' |
虽然字符串有个replace()
方法,也确实变出了'Abc'
,但变量a
最后仍是'abc'
,应该怎么理解呢?
我们先把代码改成下面这样:
1 | >>> a = 'abc' |
2. 变量
变量名必须是大小写英文、数字和_
的组合,且不能用数字开头。
在Python中,等号=
是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量。
这种变量本身类型不固定的语言称之为_动态语言_,与之对应的是_静态语言_。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如Java是静态语言。
1 | a = 'ABC' |
小结
Python支持多种数据类型,在计算机内部,可以把任何数据都看成一个“对象”,而变量就是在程序中用来指向这些数据对象的,对变量赋值就是把数据和变量给关联起来。
对变量赋值x = y
是把变量x
指向真正的对象,该对象是变量y
所指向的。随后对变量y
的赋值_不影响_变量x
的指向。
3. 常量
常量就是不能变的变量,在Python中,通常用全部大写的变量名表示常量。
1 | PI = 3.14159265359 |
但事实上PI
仍然是一个变量,Python根本没有任何机制保证PI
不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量PI
的值,也没人能拦住你。
最后解释一下整数的除法为什么也是精确的。在Python中,有两种除法,一种除法是/
:
1 | >>> 10 / 3 |
/
除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:
1 | >>> 9 / 3 |
还有一种除法是//
,称为地板除,两个整数的除法仍然是整数:
1 | >>> 10 // 3 |
你没有看错,整数的地板除//
永远是整数,即使除不尽。要做精确的除法,使用/
就可以。
因为//
除法只取结果的整数部分,所以Python还提供一个余数运算,可以得到两个整数相除的余数:
1 | >>> 10 % 3 |
无论整数做//
除法还是取余数,结果永远是整数,所以,整数运算结果永远是精确的。
Python的整数没有大小限制,Python的浮点数也没有大小限制,但是超出一定范围就直接表示为inf
(无限大)
4. 编码和格式化
1. 编码
在最新的Python 3版本中,字符串是以Unicode编码的,也就是说,Python的字符串支持多语言。
对于单个字符的编码,Python提供了ord()
函数获取字符的整数表示,chr()
函数把编码转换为对应的字符:
1 | >>> ord('A') |
要计算str
包含多少个字符,可以用len()
函数:
1 | >>> len('ABC') |
由于Python源代码也是一个文本文件,所以,当你的源代码中包含中文的时候,在保存源代码时,就需要务必指定保存为UTF-8编码。当Python解释器读取源代码时,为了让它按UTF-8编码读取,我们通常在文件开头写上这两行:
1 | #!/usr/bin/env python3 |
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码,否则,你在源代码中写的中文输出可能会有乱码。
申明了UTF-8编码并不意味着你的.py
文件就是UTF-8编码的,必须并且要确保文本编辑器正在使用UTF-8 without BOM编码:
2. 格式化
在Python中,采用的格式化方式和C语言是一致的,用%
实现,举例如下:
1 | >>> 'Hello, %s' % 'world' |
%
运算符就是用来格式化字符串的。在字符串内部,%s
表示用字符串替换,%d
表示用整数替换,有几个%?
占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?
,括号可以省略。
常见的占位符
有些时候,字符串里面的%
是一个普通字符怎么办?这个时候就需要转义,用%%
来表示一个%
:
1 | >>> 'growth rate: %d %%' % 7 |
另一种格式化字符串的方法是使用字符串的format()
方法,它会用传入的参数依次替换字符串内的占位符{0}
、{1}
……,不过这种方式写起来比%要麻烦得多:
1 | >>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) |
5. 运算符
常规的运算符就不列举了,列举一些对我来说是新的操作。
逻辑运算符
成员运算符
身份运算符
6. 条件+循环
条件
循环
Python的循环有两种
一种是for…in循环,依次把list或tuple中的每个元素迭代出来,看例子:
1 | names = ['Michael', 'Bob', 'Tracy'] |
执行这段代码,会依次打印names
的每一个元素:
1 | Michael |
所以for x in ...
循环就是把每个元素代入变量x
,然后执行缩进块的语句。
如果要计算1-100的整数之和,从1写到100有点困难,幸好Python提供一个range()
函数,可以生成一个整数序列,再通过list()
函数可以转换为list。比如range(5)
生成的序列是从0开始小于5的整数:
1 | >>> list(range(5)) |
1 | sum = 0 |
第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。
比如我们要计算100以内所有奇数之和,可以用while循环实现:
1 | sum = 0 |
在循环内部变量n
不断自减,直到变为-1
时,不再满足while条件,循环退出。
break
在循环中,break
语句可以提前退出循环。
1 | n = 1 |
执行上面的代码可以看到,打印出1~10后,紧接着打印END
,程序结束。
continue
在循环过程中,也可以通过continue
语句,跳过当前的这次循环,直接开始下一次循环。
1 | # 打印奇数 |
_要特别注意_,不要滥用break
和continue
语句。break
和continue
会造成代码执行逻辑分叉过多,容易出错。
7. 函数
1. 调用函数
Python内置了很多有用的函数,我们可以直接调用。
自带函数查找手册:https://docs.python.org/3/library/functions.html
1 |
|
2. 定义函数
在Python中,定义一个函数要使用def
语句,依次写出函数名、括号、括号中的参数和冒号:
,然后,在缩进块中编写函数体,函数的返回值用return
语句返回。
以自定义一个求绝对值的my_abs
函数为例:
1 | def my_abs(x): |
函数体内部的语句在执行时,一旦执行到return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为None
。return None
可以简写为return
。
在Python交互环境中定义函数时,注意Python会出现...
的提示。函数定义结束后需要按两次回车重新回到>>>
提示符下
如果你已经把my_abs()
的函数定义保存为abstest.py
文件了,那么,可以在该文件的当前目录下启动Python解释器,用from abstest import my_abs
来导入my_abs()
函数,注意abstest
是文件名(不含.py
扩展名):
空函数:函数体使用pass
语句
1 | def nop(): |
pass
语句什么都不做,那有什么用?实际上pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
,让代码能运行起来。
pass
还可以用在其他语句里,比如:
1 | if age >= 18: |
缺少了pass
,代码运行就会有语法错误。
参数检查:参数不对抛出TypeError
返回多个值
1 | import math |
可以同时获得返回值:
1 | >>> x, y = move(100, 100, 60, math.pi / 6) |
但其实这只是一种假象,Python函数返回的仍然是单一值:
1 | >>> r = move(100, 100, 60, math.pi / 6) |
原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
3. 函数的参数
Python提供了 必选参数(位置参数)外,还提供了 默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
1. 位置参数
第一个版本
1 | def power(x): |
第二个版本:计算 x3 、x4、xn呢?
1 | def power(x, n): |
修改后的power(x, n)
函数有两个参数:x
和n
,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数x
和n
。
2. 默认参数
1 | def power(x, n=2): |
注意:
一是必选参数在前,默认参数在后,否则Python的解释器会报错;
二是如何设置默认参数。
当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
使用默认参数有什么好处?最大的好处是能降低调用函数的难度。
默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。
有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用enroll('Bob', 'M', 7)
,意思是,除了name
,gender
这两个参数外,最后1个参数应用在参数age
上,city
参数由于没有提供,仍然使用默认值。
也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll('Adam', 'M', city='Tianjin')
,意思是,city
参数用传进去的值,其他默认参数继续使用默认值。
默认参数的坑
先定义一个函数,传入一个list,添加一个END
再返回:
1 | def add_end(L=[]): |
当你正常调用时,结果似乎不错:
1 | >>> add_end([1, 2, 3]) |
当你使用默认参数调用时,一开始结果也是对的:
1 | >>> add_end() |
但是,再次调用add_end()
时,结果就不对了:
1 | >>> add_end() |
很多初学者很疑惑,默认参数是[]
,但是函数似乎每次都“记住了”上次添加了'END'
后的list。
Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
默认参数必须指向不变对象!
只需要提供默认参数值 (L=None) 即可。
为什么要设计str
、None
这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
3. 可变参数
1 | def calc(*numbers): |
仅仅在参数前面加了一个*
号。在函数内部,参数numbers
接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
1 | >>> calc(1, 2) |
如果已经有了一个 list 或 tuple,想将元素依次传入呢?
Python允许在list或tuple前面加一个*
号,把list或tuple的元素变成可变参数传进去:
1 | >>> nums = [1, 2, 3] |
*nums
表示把nums
这个list的所有元素作为可变参数传进去。
4. 关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
1 | def person(name, age, **kw): |
函数person
除了必选参数name
和age
外,还接受关键字参数kw
。在调用该函数时,可以只传入必选参数:
1 | >>> person('Michael', 30) |
也可以传入任意个数的关键字参数:
1 | >>> person('Bob', 35, city='Beijing') |
关键字参数有什么用?它可以扩展函数的功能。比如,在person
函数里,我们保证能接收到name
和age
这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
1 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} |
简化的写法:
1 | >>> extra = {'city': 'Beijing', 'job': 'Engineer'} |
**extra
表示把extra
这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw
将获得一个dict,注意kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
5. 命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw
检查。
仍以person()
函数为例,我们希望检查是否有city
和job
参数:
1 | def person(name, age, **kw): |
但是调用者仍可以传入不受限制的关键字参数:
1 | >>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456) |
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
1 | def person(name, age, *, city, job): |
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
调用方式如下:
1 | >>> person('Jack', 24, city='Beijing', job='Engineer') |
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
1 | def person(name, age, *args, city, job): |
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
1 | >>> person('Jack', 24, 'Beijing', 'Engineer') |
由于调用时缺少参数名city
和job
,Python解释器把这4个参数均视为位置参数,但person()
函数仅接受2个位置参数。
命名关键字参数可以有缺省值,从而简化调用:
1 | def person(name, age, *, city='Beijing', job): |
由于命名关键字参数city
具有默认值,调用时,可不传入city
参数:
1 | >>> person('Jack', 24, job='Engineer') |
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数:
1 | def person(name, age, city, job): |
6. 参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
1 | def f1(a, b, c=0, *args, **kw): |
在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
1 | >>> f1(1, 2) |
最神奇的是通过一个tuple和dict,你也可以调用上述函数:
1 | >>> args = (1, 2, 3, 4) |
所以,对于任意函数,都可以通过类似func(*args, **kw)
的形式调用它,无论它的参数是如何定义的。
7. 小结
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的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*
,否则定义的将是位置参数。
8. 高级特性
1. 切片
经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作。
1 | >>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] |
L[0:3]
表示,从索引0
开始取,直到索引3
为止,但不包括索引3
。即索引0
,1
,2
,正好是3个元素。
如果第一个索引是0
,还可以省略:
1 | >>> L[:3] |
切片操作十分有用。我们先创建一个0-99的数列:
1 | >>> L = list(range(100)) |
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
1 | >>> (0, 1, 2, 3, 4, 5)[:3] |
字符串'xxx'
也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
1 | >>> 'ABCDEFG'[:3] |
2. 迭代
如果给定一个list或tuple,我们可以通过for
循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。Python的for
循环不仅可以用在list或tuple上,还可以作用在其他可迭代对象上。
list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:
1 | >>> d = {'a': 1, 'b': 2, 'c': 3} |
默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values()
,如果要同时迭代key和value,可以用for k, v in d.items()
。
由于字符串也是可迭代对象,因此,也可以作用于for
循环:
1 | >>> for ch in 'ABC': |
如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:
1 | >>> from collections import Iterable |
3. 列表生成式
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
可以用list(range(1, 11))
:
1 | >>> list(range(1, 11)) |
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]
怎么做?
方法一是循环:
1 | >>> L = [] |
但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:
1 | >>> [x * x for x in range(1, 11)] |
思路: 先写 [for x in range(1,11)]
,可以认为已经产生了1-10的数据,再对这些数据统一做一些处理,如上述的 对每个元素 做 自乘 操作。
for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
1 | >>> [x * x for x in range(1, 11) if x % 2 == 0] |
还可以使用两层循环,可以生成全排列:
1 | >>> [m + n for m in 'ABC' for n in 'XYZ'] |
三层和三层以上的循环就很少用到了。
运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:
1 | >>> import os # 导入os模块 |
for
循环其实可以同时使用两个甚至多个变量,比如dict
的items()
可以同时迭代key和value:
1 | >>> d = {'x': 'A', 'y': 'B', 'z': 'C' } |
因此,列表生成式也可以使用两个变量来生成list:
1 | >>> d = {'x': 'A', 'y': 'B', 'z': 'C' } |
最后把一个list中所有的字符串变成小写:
1 | >>> L = ['Hello', 'World', 'IBM', 'Apple'] |
** if … else**:
以下代码正常输出偶数:
1 | >>> [x for x in range(1, 11) if x % 2 == 0] |
但是,我们不能在最后的if
加上else
:
1 | >>> [x for x in range(1, 11) if x % 2 == 0 else 0] |
这是因为跟在for
后面的if
是一个筛选条件,不能带else
发现把if
写在for
前面必须加else
,否则报错:
1 | >>> [x if x % 2 == 0 for x in range(1, 11)] |
这是因为for
前面的部分是一个表达式,它必须根据x
计算出一个结果。因此,考察表达式:x if x % 2 == 0
,它无法根据x
计算出结果,因为缺少else
,必须加上else
:
1 | >>> [x if x % 2 == 0 else -x for x in range(1, 11)] |
上述for
前面的表达式x if x % 2 == 0 else -x
才能根据x
计算出确定的结果。
在一个列表生成式中,for
前面的if ... else
是表达式,而for
后面的if
是过滤条件,不能带else
。
4. 生成器
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]
改成()
,就创建了一个generator:
1 | >>> L = [x * x for x in range(10)] |
创建L
和g
的区别仅在于最外层的[]
和()
,L
是一个list,而g
是一个generator。
我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?
如果要一个一个打印出来,可以通过next()
函数获得generator的下一个返回值:
1 | >>> next(g) |
generator保存的是算法,每次调用next(g)
,就计算出g
的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration
的错误。
当然,上面这种不断调用next(g)
实在是太变态了,正确的方法是使用for
循环,因为generator也是可迭代对象:
1 | >>> g = (x * x for x in range(10)) |
所以,我们创建了一个generator后,基本上永远不会调用next()
,而是通过for
循环来迭代它,并且不需要关心StopIteration
的错误。
generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for
循环无法实现的时候,还可以用函数来实现。
比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, …
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
1 | def fib(max): |
注意,赋值语句:
1 | a, b = b, a + b |
相当于:
1 | t = (b, a + b) # t是一个tuple |
但不必显式写出临时变量t就可以赋值。
上面的函数可以输出斐波那契数列的前N个数:
1 | >>> fib(6) |
仔细观察,可以看出,fib
函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。
也就是说,上面的函数和generator仅一步之遥。要把fib
函数变成generator,只需要把print(b)
改为yield b
就可以了:
1 | def fib(max): |
这就是定义generator的另一种方法。如果一个函数定义中包含yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator:
1 | >>> f = fib(6) |
这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return
语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
举个简单的例子,定义一个generator,依次返回数字1,3,5:
1 | def odd(): |
调用该generator时,首先要生成一个generator对象,然后用next()
函数不断获得下一个返回值:
1 | >>> o = odd() |
可以看到,odd
不是普通函数,而是generator,在执行过程中,遇到yield
就中断,下次又继续执行。执行3次yield
后,已经没有yield
可以执行了,所以,第4次调用next(o)
就报错。
回到fib
的例子,我们在循环过程中不断调用yield
,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
同样的,把函数改成generator后,我们基本上从来不会用next()
来获取下一个返回值,而是直接使用for
循环来迭代:
1 | >>> for n in fib(6): |
但是用for
循环调用generator时,发现拿不到generator的return
语句的返回值。如果想要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中:
1 | >>> g = fib(6) |
把每一行看做一个list,试写一个generator,不断输出下一行的list:
方法1:
方法2:
方法3:
小结
generator是非常强大的工具,在Python中,可以简单地把列表生成式改成generator,也可以通过函数实现复杂逻辑的generator。
要理解generator的工作原理,它是在for
循环的过程中不断计算出下一个元素,并在适当的条件结束for
循环。对于函数改成的generator来说,遇到return
语句或者执行到函数体最后一行语句,就是结束generator的指令,for
循环随之结束。
5. 迭代器
可以直接作用于for
循环的数据类型有以下几种:
一类是集合数据类型,如list
、tuple
、dict
、set
、str
等;
一类是generator
,包括生成器和带yield
的generator function。
这些可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
1 | >>> from collections.abc import Iterable |
而生成器不但可以作用于for
循环,还可以被next()
函数不断调用并返回下一个值,直到最后抛出StopIteration
错误表示无法继续返回下一个值了。
可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
。
可以使用isinstance()
判断一个对象是否是Iterator
对象:
1 | >>> from collections.abc import Iterator |
生成器都是Iterator
对象,但list
、dict
、str
虽然是Iterable
,却不是Iterator
。
把list
、dict
、str
等Iterable
变成Iterator
可以使用iter()
函数:
1 | >>> isinstance(iter([]), Iterator) |
为什么list
、dict
、str
等数据类型不是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 | for x in [1, 2, 3, 4, 5]: |
实际上完全等价于:
1 | # 首先获得Iterator对象: |
9. 函数式编程
函数式编程的一个特点就是允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
1. 高阶函数
高阶函数英文叫Higher-order function。什么是高阶函数?我们以实际代码为例子,一步一步深入概念。
变量可以指向函数
以Python内置的求绝对值的函数abs()
为例,调用该函数用以下代码:
1 | >>> abs(-10) |
但是,如果只写abs
呢?
1 | >>> abs |
可见,abs(-10)
是函数调用,而abs
是函数本身。
要获得函数调用结果,我们可以把结果赋值给变量:
1 | >>> x = abs(-10) |
但是,如果把函数本身赋值给变量呢?
1 | >>> f = abs |
结论:函数本身也可以赋值给变量,即:变量可以指向函数。
如果一个变量指向了一个函数,那么,可否通过该变量来调用这个函数?用代码验证一下:
1 | >>> f = abs |
成功!说明变量f
现在已经指向了abs
函数本身。直接调用abs()
函数和调用变量f()
完全相同。
函数名也是变量
那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()
这个函数,完全可以把函数名abs
看成变量,它指向一个可以计算绝对值的函数!
如果把abs
指向其他对象,会有什么情况发生?
1 | >>> abs = 10 |
把abs
指向10
后,就无法通过abs(-10)
调用该函数了!因为abs
这个变量已经不指向求绝对值函数而是指向一个整数10
!
当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs
函数,请重启Python交互环境。
注:由于abs
函数实际上是定义在import builtins
模块中的,所以要让修改abs
变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
。
传入函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
1 | def add(x, y, f): |
当我们调用add(-5, 6, abs)
时,参数x
,y
和f
分别接收-5
,6
和abs
,根据函数定义,我们可以推导计算过程为:
1 | x = -5 |
高阶函数,就是让函数的参数能够接收别的函数,即把函数作为参数传入。函数式编程就是指这种高度抽象的编程范式。
map/reduce
Python内建了map()
和reduce()
函数。
如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白map/reduce的概念。
我们先看map。map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
举例说明,比如我们有一个函数f(x)=x2>,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用map()
实现如下:
Python代码实现:
1 | >>> def f(x): |
map()
传入的第一个参数是f
,即函数对象本身。由于结果r
是一个Iterator
,Iterator
是惰性序列,因此通过list()
函数让它把整个序列都计算出来并返回一个list。
你可能会想,不需要map()
函数,写一个循环,也可以计算出结果:
1 | L = [] |
的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?
所以,map()
作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
1 | >>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
再看reduce
的用法。reduce
把一个函数作用在一个序列[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
1 | reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) |
比方说对一个序列求和,就可以用reduce
实现:
1 | >>> from functools import reduce |
当然求和运算可以直接用Python内建函数sum()
,没必要动用reduce
。
但是如果要把序列[1, 3, 5, 7, 9]
变换成整数13579
,reduce
就可以派上用场:
1 | >>> from functools import reduce |
这个例子本身没多大用处,但是,如果考虑到字符串str
也是一个序列,对上面的例子稍加改动,配合map()
,我们就可以写出把str
转换为int
的函数:
1 | >>> from functools import reduce |
整理成一个str2int
的函数就是:
1 | from functools import reduce |
还可以用lambda函数进一步简化成:
1 | from functools import reduce |
filter
Python内建的filter()
函数用于过滤序列。
和map()
类似,filter()
也接收一个函数和一个序列。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是True
还是False
决定保留还是丢弃该元素。
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
1 | def is_odd(n): |
把一个序列中的空字符串删掉,可以这么写:
1 | def not_empty(s): |
可见用filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。
注意到filter()
函数返回的是一个Iterator
,也就是一个惰性序列,所以要强迫filter()
完成计算结果,需要用list()
函数获得所有结果并返回list。
练习:回数是指从左向右读和从右向左读都是一样的数,例如12321
,909
。请利用filter()
筛选出回数
1 | def is_palindrome(n): |
sorted
排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
Python内置的sorted()
函数就可以对list进行排序:
1 | >>> sorted([36, 5, -12, 9, -21]) |
此外,sorted()
函数也是一个高阶函数,它还可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
1 | >>> sorted([36, 5, -12, 9, -21], key=abs) |
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs
处理过的list:
1 | list = [36, 5, -12, 9, -21] |
再看一个字符串排序的例子:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit']) |
默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a'
,结果,大写字母Z
会排在小写字母a
的前面。
现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
这样,我们给sorted
传入key函数,即可实现忽略大小写的排序:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) |
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True
:
1 | >>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) |
高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
2. 返回函数
1. 函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:
1 | def calc_sum(*args): |
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:
1 | def lazy_sum(*args): |
当我们调用lazy_sum()
时,返回的并不是求和结果,而是求和函数:
1 | >>> f = lazy_sum(1, 3, 5, 7, 9) |
调用函数f
时,才真正计算求和的结果:
1 | >>> f() |
在这个例子中,我们在函数lazy_sum
中又定义了函数sum
,并且,内部函数sum
可以引用外部函数lazy_sum
的参数和局部变量,当lazy_sum
返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。
请再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
1 | >>> f1 = lazy_sum(1, 3, 5, 7, 9) |
2. 闭包
注意到返回的函数在其定义内部引用了局部变量args
,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()
才执行。我们来看一个例子:
1 | def count(): |
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
1 | >>> f1() |
全部都是9
!原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i
已经变成了3
,因此最终结果为9
。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
1 | def count(): |
再看看结果:
1 | >>> f1, f2, f3 = count() |
缺点是代码较长,可利用lambda函数缩短代码。
3. 匿名函数
我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。
在Python中,对匿名函数提供了有限支持。还是以map()
函数为例,计算f(x)=x2时,除了定义一个f(x)
的函数外,还可以直接传入匿名函数:
1 | >>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])) |
通过对比可以看出,匿名函数lambda x: x * x
实际上就是:
1 | def f(x): |
关键字lambda
表示匿名函数,冒号前面的x
表示函数参数。
匿名函数有个限制,就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
1 | >>> f = lambda x: x * x |
同样,也可以把匿名函数作为返回值返回,比如:
1 | def build(x, y): |
4. 装饰器
由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
1 | >>> def now(): |
函数对象有一个__name__
属性,可以拿到函数的名字:
1 | >>> now.__name__ |
现在,假设我们要增强now()
函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()
函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:
1 | def log(func): |
观察上面的log
,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
1 | @log |
调用now()
函数,不仅会运行now()
函数本身,还会在运行now()
函数前打印一行日志:
1 | >>> now() |
把@log
放到now()
函数的定义处,相当于执行了语句:
1 | now = log(now) |
由于log()
是一个decorator,返回一个函数,所以,原来的now()
函数仍然存在,只是现在同名的now
变量指向了新的函数,于是调用now()
将执行新函数,即在log()
函数中返回的wrapper()
函数。
wrapper()
函数的参数定义是(*args, **kw)
,因此,wrapper()
函数可以接受任意参数的调用。在wrapper()
函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
1 | def log(text): |
这个3层嵌套的decorator用法如下:
1 | @log('execute') |
执行结果如下:
1 | >>> now() |
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
1 | >>> now = log('execute')(now) |
我们来剖析上面的语句,首先执行log('execute')
,返回的是decorator
函数,再调用返回的函数,参数是now
函数,返回值最终是wrapper
函数。
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__
等属性,但你去看经过decorator装饰之后的函数,它们的__name__
已经从原来的'now'
变成了'wrapper'
:
1 | >>> now.__name__ |
因为返回的那个wrapper()
函数名字就是'wrapper'
,所以,需要把原始函数的__name__
等属性复制到wrapper()
函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__
这样的代码,Python内置的functools.wraps
就是干这个事的,所以,一个完整的decorator的写法如下:
1 | import functools |
或者针对带参数的decorator:
1 | import functools |
import functools
是导入functools
模块。现在,只需记住在定义wrapper()
的前面加上@functools.wraps(func)
即可
5. 偏函数
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。
在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下:
int()
函数可以把字符串转换为整数,当仅传入字符串时,int()
函数默认按十进制转换:
1 | >>> int('12345') |
但int()
函数还提供额外的base
参数,默认值为10
。如果传入base
参数,就可以做N进制的转换:
1 | >>> int('12345', base=8) |
假设要转换大量的二进制字符串,每次都传入int(x, base=2)
非常麻烦,于是,我们想到,可以定义一个int2()
的函数,默认把base=2
传进去:
1 | def int2(x, base=2): |
这样,我们转换二进制就非常方便了:
1 | >>> int2('1000000') |
functools.partial
就是帮助我们创建一个偏函数的,不需要我们自己定义int2()
,可以直接使用下面的代码创建一个新的函数int2
:
1 | >>> import functools |
所以,简单总结functools.partial
的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
注意到上面的新的int2
函数,仅仅是把base
参数重新设定默认值为2
,但也可以在函数调用时传入其他值:
1 | >>> int2('1000000', base=10) |
最后,创建偏函数时,实际上可以接收函数对象、*args
和**kw
这3个参数,当传入:
1 | int2 = functools.partial(int, base=2) |
实际上固定了int()函数的关键字参数base
,也就是:
1 | int2('10010') |
相当于:
1 | kw = { 'base': 2 } |
当传入:
1 | max2 = functools.partial(max, 10) |
实际上会把10
作为*args
的一部分自动加到左边,也就是:
1 | max2(5, 6, 7) |
相当于:
1 | args = (10, 5, 6, 7) |
结果为10
。
当函数的参数个数太多,需要简化时,使用functools.partial
可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。