目录
输出
输入
数据类型
变量
常量
字符编码
格式化
tuple
条件判断
input
循环
dict
set
不可变对象
小结
调用函数
数据类型转换
定义函数
空函数
参数检查
返回多个值
小结
函数的参数
位置参数
默认参数
可变参数
关键字参数
参数组合
练习
小结
递归函数
小结
python基础语法
#tuple与list类似,不同之处在于tuple的元素不能修改。tuple写在小括号里,元素之间用逗号隔开。
#元组的元素不可变,但可以包含可变对象,如list。
输出
用print()
在括号中加上字符串,就可以向屏幕上输出指定的文字。比如输出'hello, world'
,用代码实现如下:
print()
函数也可以接受多个字符串,用逗号“,”隔开,就可以连成一串输出:
-
>>> print('The quick brown fox', 'jumps over', 'the lazy dog')
-
The quick brown fox jumps over the lazy dog
print()
会依次打印每个字符串,遇到逗号“,”会输出一个空格
print()
也可以打印整数,或者计算结果:
-
>>> print(300)
-
300
-
>>> print(100 + 200)
-
300
因此,我们可以把计算100 + 200
的结果打印得更漂亮一点:
-
>>> print('100 + 200 =', 100 + 200)
-
100 + 200 = 300
输入
Python提供了一个input()
,可以让用户输入字符串,并存放到一个变量里。比如输入用户的名字:
-
>>> name = input()
-
Michael
当你输入name = input()
并按下回车后,Python交互式命令行就在等待你的输入了。这时,你可以输入任意字符,然后按回车后完成输入。
输入完成后,不会有任何提示,Python交互式命令行又回到>>>
状态了。那我们刚才输入的内容到哪去了?答案是存放到name
变量里了。可以直接输入name
查看变量内容:
-
>>> name
-
'Michael'
结合输入输出
-
name = input()
-
print('hello,', name)
数据类型
整数
Python可以处理任意大小的整数,当然包括负整数,在程序中的表示方法和数学上的写法一模一样,例如:1
,100
,-8080
,0
,等等。
计算机由于使用二进制,所以,有时候用十六进制表示整数比较方便,十六进制用0x
前缀和0-9,a-f表示,例如:0xff00
,0xa5b4c3d2
,等等。
浮点数
浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,比如,1.23x109和12.3x108是完全相等的。浮点数可以用数学写法,如1.23
,3.14
,-9.01
,等等。但是对于很大或很小的浮点数,就必须用科学计数法表示,把10用e替代,1.23x109就是1.23e9
,或者12.3e8
,0.000012可以写成1.2e-5
,等等。
整数和浮点数在计算机内部存储的方式是不同的,整数运算永远是精确的(除法难道也是精确的?是的!),而浮点数运算则可能会有四舍五入的误差。
字符串
字符串是以单引号'
或双引号"
括起来的任意文本,比如'abc'
,"xyz"
等等。请注意,''
或""
本身只是一种表示方式,不是字符串的一部分,因此,字符串'abc'
只有a
,b
,c
这3个字符。如果'
本身也是一个字符,那就可以用""
括起来,比如"I'm OK"
包含的字符是I
,'
,m
,空格,O
,K
这6个字符。
如果字符串内部既包含'
又包含"
怎么办?可以用转义字符``来标识,比如:
表示的字符串内容是:
转义字符可以转义很多字符,比如`\n`表示换行,`\t`表示制表符,字符
本身也要转义,所以\
表示的字符就是``,可以在Python的交互式命令行用print()
打印字符串看看:
-
>>> print('I'm ok.')
-
I'm ok.
-
>>> print('I'm learning\nPython.')
-
I'm learning
-
Python.
-
>>> print('\\n\')
- ``
- ``
如果字符串里面有很多字符都需要转义,就需要加很多``,为了简化,Python还允许用r''
表示''
内部的字符串默认不转义,可以自己试试:
-
>>> print('\\t\')
-
\
-
>>> print(r'\\t\')
-
\\t\
如果字符串内部有很多换行,用\n
写在一行里不好阅读,为了简化,Python允许用'''...'''
的格式表示多行内容,可以自己试试:
-
>>> print('''line1
-
... line2
-
... line3''')
-
line1
-
line2
-
line3
上面是在交互式命令行内输入,注意在输入多行内容时,提示符由>>>
变为...
,提示你可以接着上一行输入,注意...
是提示符,不是代码的一部分:
当输入完结束符```
和括号)
后,执行该语句并打印结果。
如果写成程序并存为.py
文件,就是:
-
print('''line1
-
line2
-
line3''')
多行字符串'''...'''
还可以在前面加上r
使用
布尔值
布尔值和布尔代数的表示完全一致,一个布尔值只有True
、False
两种值,要么是True
,要么是False
,在Python中,可以直接用True
、False
表示布尔值(请注意大小写),也可以通过布尔运算计算出来:
-
>>> True
-
True
not
运算是非运算,它是一个单目运算符,把True
变成False
,False
变成True
:
-
>>> not True
-
False
空值
空值是Python里一个特殊的值,用None
表示。None
不能理解为0
,因为0
是有意义的,而None
是一个特殊的空值。
此外,Python还提供了列表、字典等多种数据类型,还允许创建自定义数据类型,我们后面会继续讲到。
变量
变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型。
变量在程序中就是用一个变量名表示了,变量名必须是大小写英文、数字和_
的组合,且不能用数字开头,比如:
变量a
是一个整数。
变量t_007
是一个字符串。
变量Answer
是一个布尔值True
。
在Python中,等号=
是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量
这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。
最后,理解变量在计算机内存中的表示也非常重要。当我们写:
时,Python解释器干了两件事情:
- 在内存中创建了一个
'ABC'
的字符串; - 在内存中创建了一个名为
a
的变量,并把它指向'ABC'
。
也可以把一个变量a
赋值给另一个变量b
,这个操作实际上是把变量b
指向变量a
所指向的数据
常量
所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。在Python中,通常用全部大写的变量名表示常量:
但事实上PI
仍然是一个变量,Python根本没有任何机制保证PI
不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量PI
的值,也没人能拦住你。
最后解释一下整数的除法为什么也是精确的。在Python中,有两种除法,一种除法是/
:
-
>>> 10 / 3
-
3.3333333333333335
/
除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:
-
>>> 9 / 3
-
3.0
还有一种除法是//
,称为地板除,两个整数的除法仍然是整数:
-
>>> 10 // 3
-
3
你没有看错,整数的地板除//
永远是整数,即使除不尽。要做精确的除法,使用/
就可以。
因为//
除法只取结果的整数部分,所以Python还提供一个余数运算,可以得到两个整数相除的余数:
-
>>> 10 % 3
-
1
无论整数做//
除法还是取余数,结果永远是整数,所以,整数运算结果永远是精确的。
字符编码
ASCII编码和Unicode编码的区别:ASCII编码是1个字节,而Unicode编码通常是2个字节。
字母A
用ASCII编码是十进制的65
,二进制的01000001
;
字符0
用ASCII编码是十进制的48
,二进制的00110000
,注意字符'0'
和整数0
是不同的;
汉字中
已经超出了ASCII编码的范围,用Unicode编码是十进制的20013
,二进制的01001110 00101101
。
如果统一成Unicode编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。
所以,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”的UTF-8
编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间
UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。
搞清楚了ASCII、Unicode和UTF-8的关系,我们就可以总结一下现在计算机系统通用的字符编码工作方式:
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:
浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:
所以你看到很多网页的源码上会有类似<meta charset="UTF-8" />
的信息,表示该网页正是用的UTF-8编码。
Python的字符串
对于单个字符的编码,Python提供了ord()
函数获取字符的整数表示,chr()
函数把编码转换为对应的字符:
-
>>> ord('A')
-
65
-
>>> ord('中')
-
20013
-
>>> chr(66)
-
'B'
-
>>> chr(25991)
-
'文'
如果知道字符的整数编码,还可以用十六进制这么写str
:
-
>>> '\u4e2d\u6587'
-
'中文'
两种写法完全是等价的。
由于Python的字符串类型是str
,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str
变为以字节为单位的bytes
。
Python对bytes
类型的数据用带b
前缀的单引号或双引号表示:
要注意区分'ABC'
和b'ABC'
,前者是str
,后者虽然内容显示得和前者一样,但bytes
的每个字符都只占用一个字节。
以Unicode表示的str
通过encode()
方法可以编码为指定的bytes
,例如:
-
>>> 'ABC'.encode('ascii')
-
b'ABC'
-
>>> '中文'.encode('utf-8')
-
b'\xe4\xb8\xad\xe6\x96\x87'
-
>>> '中文'.encode('ascii')
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
纯英文的str
可以用ASCII
编码为bytes
,内容是一样的,含有中文的str
可以用UTF-8
编码为bytes
。含有中文的str
无法用ASCII
编码,因为中文编码的范围超过了ASCII
编码的范围,Python会报错。
在bytes
中,无法显示为ASCII字符的字节,用\x##
显示。
反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes
。要把bytes
变为str
,就需要用decode()
方法:
-
>>> b'ABC'.decode('ascii')
-
'ABC'
-
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
-
'中文'
如果bytes
中包含无法解码的字节,decode()
方法会报错:
-
>>> b'\xe4\xb8\xad\xff'.decode('utf-8')
-
Traceback (most recent call last):
-
...
-
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte
如果bytes
中只有一小部分无效的字节,可以传入errors='ignore'
忽略错误的字节:
-
>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
-
'中'
要计算str
包含多少个字符,可以用len()
函数:
-
>>> len('ABC')
-
3
-
>>> len('中文')
-
2
len()
函数计算的是str
的字符数,如果换成bytes
,len()
函数就计算字节数:
-
>>> len(b'ABC')
-
3
-
>>> len(b'\xe4\xb8\xad\xe6\x96\x87')
-
6
-
>>> len('中文'.encode('utf-8'))
-
6
可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
格式化
在Python中,采用的格式化方式和C语言是一致的,用%
实现,举例如下:
-
>>> 'Hello, %s' % 'world'
-
'Hello, world'
-
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
-
'Hi, Michael, you have $1000000.'
你可能猜到了,%
运算符就是用来格式化字符串的。在字符串内部,%s
表示用字符串替换,%d
表示用整数替换,有几个%?
占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?
,括号可以省略。
常见的占位符有:
占位符 | 替换内容 |
%d | 整数 |
%f | 浮点数 |
%s | 字符串 |
%x | 十六进制整数 |
其中,格式化整数和浮点数还可以指定是否补0和整数与小数的位数:
-
print('%2d-%02d' % (3, 1))
-
print('%.2f' % 3.1415926)
如果你不太确定应该用什么,%s
永远起作用,它会把任何数据类型转换为字符串:
-
>>> 'Age: %s. Gender: %s' % (25, True)
-
'Age: 25. Gender: True'
有些时候,字符串里面的%
是一个普通字符怎么办?这个时候就需要转义,用%%
来表示一个%
:
-
>>> 'growth rate: %d %%' % 7
-
'growth rate: 7 %'
format()
另一种格式化字符串的方法是使用字符串的format()
方法,它会用传入的参数依次替换字符串内的占位符{0}
、{1}
……,不过这种方式写起来比%要麻烦得多:
-
>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
-
'Hello, 小明, 成绩提升了 17.1%'
list
Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。
比如,列出班里所有同学的名字,就可以用一个list表示:
-
>>> classmates = ['Michael', 'Bob', 'Tracy']
-
>>> classmates
-
['Michael', 'Bob', 'Tracy']
变量classmates
就是一个list。用len()
函数可以获得list元素的个数:
-
>>> len(classmates)
-
3
用索引来访问list中每一个位置的元素,记得索引是从0
开始的:
-
>>> classmates[0]
-
'Michael'
-
>>> classmates[1]
-
'Bob'
-
>>> classmates[2]
-
'Tracy'
-
>>> classmates[3]
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
IndexError: list index out of range
当索引超出了范围时,Python会报一个IndexError
错误,所以,要确保索引不要越界,记得最后一个元素的索引是len(classmates) - 1
。
如果要取最后一个元素,除了计算索引位置外,还可以用-1
做索引,直接获取最后一个元素:
-
>>> classmates[-1]
-
'Tracy'
以此类推,可以获取倒数第2个、倒数第3个:
-
>>> classmates[-2]
-
'Bob'
-
>>> classmates[-3]
-
'Michael'
-
>>> classmates[-4]
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
IndexError: list index out of range
当然,倒数第4个就越界了。
list是一个可变的有序表,所以,可以往list中追加元素到末尾:
-
>>> classmates.append('Adam')
-
>>> classmates
-
['Michael', 'Bob', 'Tracy', 'Adam']
也可以把元素插入到指定的位置,比如索引号为1
的位置:
-
>>> classmates.insert(1, 'Jack')
-
>>> classmates
-
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']
要删除list末尾的元素,用pop()
方法:
-
>>> classmates.pop()
-
'Adam'
-
>>> classmates
-
['Michael', 'Jack', 'Bob', 'Tracy']
要删除指定位置的元素,用pop(i)
方法,其中i
是索引位置:
-
>>> classmates.pop(1)
-
'Jack'
-
>>> classmates
-
['Michael', 'Bob', 'Tracy']
要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:
-
>>> classmates[1] = 'Sarah'
-
>>> classmates
-
['Michael', 'Sarah', 'Tracy']
list里面的元素的数据类型也可以不同,比如:
list元素也可以是另一个list,比如:
-
>>> s = ['python', 'java', ['asp', 'php'], 'scheme']
-
>>> len(s)
-
4
要注意s
只有4个元素,其中s[2]
又是一个list,如果拆开写就更容易理解了:
-
>>> p = ['asp', 'php']
-
>>> s = ['python', 'java', p, 'scheme']
要拿到'php'
可以写p[1]
或者s[2][1]
,因此s
可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。
如果一个list中一个元素也没有,就是一个空的list,它的长度为0:
-
>>> L = []
-
>>> len(L)
-
0
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)
-
>>> t
-
(1, 2)
如果要定义一个空的tuple,可以写成()
:
-
>>> t = ()
-
>>> t
-
()
但是,要定义一个只有1个元素的tuple,如果你这么定义:
-
>>> t = (1)
-
>>> t
-
1
定义的不是tuple,是1
这个数!这是因为括号()
既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1
。
所以,只有1个元素的tuple定义时必须加一个逗号,
,来消除歧义:
-
>>> t = (1,)
-
>>> t
-
(1,)
Python在显示只有1个元素的tuple时,也会加一个逗号,
,以免你误解成数学计算意义上的括号。
最后来看一个“可变的”tuple:
-
>>> t = ('a', 'b', ['A', 'B'])
-
>>> t[2][0] = 'X'
-
>>> t[2][1] = 'Y'
-
>>> t
-
('a', 'b', ['X', 'Y'])
这个tuple定义的时候有3个元素,分别是'a'
,'b'
和一个list。不是说tuple一旦定义后就不可变了吗?怎么后来又变了?
别急,我们先看看定义的时候tuple包含的3个元素:
当我们把list的元素'A'
和'B'
修改为'X'
和'Y'
后,tuple变为:
表面上看,tuple的元素确实变了,但其实变的不是tuple的元素,而是list的元素。tuple一开始指向的list并没有改成别的list,所以,tuple所谓的“不变”是说,tuple的每个元素,指向永远不变。即指向'a'
,就不能改成指向'b'
,指向一个list,就不能改成指向其他对象,但指向的这个list本身是可变的!
理解了“指向不变”后,要创建一个内容也不变的tuple怎么做?那就必须保证tuple的每一个元素本身也不能变。
条件判断
计算机之所以能做很多自动化的任务,因为它可以自己做条件判断。
比如,输入用户年龄,根据年龄打印不同的内容,在Python程序中,用if
语句实现:
-
age = 20
-
if age >= 18:
-
print('your age is', age)
-
print('adult')
根据Python的缩进规则,如果if
语句判断是True
,就把缩进的两行print语句执行了,否则,什么也不做。
也可以给if
添加一个else
语句,意思是,如果if
判断是False
,不要执行if
的内容,去把else
执行了:
-
age = 3
-
if age >= 18:
-
print('your age is', age)
-
print('adult')
-
else:
-
print('your age is', age)
-
print('teenager')
注意不要少写了冒号:
。
当然上面的判断是很粗略的,完全可以用elif
做更细致的判断:
-
age = 3
-
if age >= 18:
-
print('adult')
-
elif age >= 6:
-
print('teenager')
-
else:
-
print('kid')
elif
是else if
的缩写,完全可以有多个elif
,所以if
语句的完整形式就是:
if
判断条件还可以简写,比如写:
-
if x:
-
print('True')
只要x
是非零数值、非空字符串、非空list等,就判断为True
,否则为False
。
input
最后看一个有问题的条件判断。很多同学会用input()
读取用户的输入,这样可以自己输入,程序运行得更有意思:
-
birth = input('birth: ')
-
if birth < 2000:
-
print('00前')
-
else:
-
print('00后')
输入1982
,结果报错:
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: unorderable types: str() > int()
这是因为input()
返回的数据类型是str
,str
不能直接和整数比较,必须先把str
转换成整数。Python提供了int()
函数来完成这件事情:
-
s = input('birth: ')
-
birth = int(s)
-
if birth < 2000:
-
print('00前')
-
else:
-
print('00后')
再次运行,就可以得到正确地结果。但是,如果输入abc
呢?又会得到一个错误信息:
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
ValueError: invalid literal for int() with base 10: 'abc'
原来int()
函数发现一个字符串并不是合法的数字时就会报错,程序就退出了。
如何检查并捕获程序运行期的错误呢?后面的错误和调试会讲到。
循环
要计算1+2+3,我们可以直接写表达式:
-
>>> 1 + 2 + 3
-
6
要计算1+2+3+...+10,勉强也能写出来。
但是,要计算1+2+3+...+10000,直接写表达式就不可能了。
为了让计算机能计算成千上万次的重复运算,我们就需要循环语句。
Python的循环有两种,一种是for...in循环,依次把list或tuple中的每个元素迭代出来,看例子:
names = ['Michael', 'Bob', 'Tracy']
-
for name in names:
-
print(name)
执行这段代码,会依次打印names
的每一个元素:
Michael
-
Bob
-
Tracy
所以for x in ...
循环就是把每个元素代入变量x
,然后执行缩进块的语句。
再比如我们想计算1-10的整数之和,可以用一个sum
变量做累加:
-
sum = 0
-
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
-
sum = sum + x
-
print(sum)
如果要计算1-100的整数之和,从1写到100有点困难,幸好Python提供一个range()
函数,可以生成一个整数序列,再通过list()
函数可以转换为list。比如range(5)
生成的序列是从0开始小于5的整数:
-
>>> list(range(5))
-
[0, 1, 2, 3, 4]
第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:
-
sum = 0
-
n = 99
-
while n > 0:
-
sum = sum + n
-
n = n - 2
-
print(sum)
在循环内部变量n
不断自减,直到变为-1
时,不再满足while条件,循环退出。
break
在循环中,break
语句可以提前退出循环。例如,本来要循环打印1~100的数字:
n = 1
-
while n <= 100:
-
print(n)
-
n = n + 1
-
print('END')
上面的代码可以打印出1~100。
如果要提前结束循环,可以用break
语句:
-
n = 1
-
while n <= 100:
-
if n > 10: # 当n = 11时,条件满足,执行break语句
-
break # break语句会结束当前循环
-
print(n)
-
n = n + 1
-
print('END')
执行上面的代码可以看到,打印出1~10后,紧接着打印END
,程序结束。
可见break
的作用是提前结束循环。
continue
在循环过程中,也可以通过continue
语句,跳过当前的这次循环,直接开始下一次循环。
-
n = 0
-
while n < 10:
-
n = n + 1
-
print(n)
上面的程序可以打印出1~10。但是,如果我们想只打印奇数,可以用continue
语句跳过某些循环:
n = 0
-
while n < 10:
-
n = n + 1
-
if n % 2 == 0: # 如果n是偶数,执行continue语句
-
continue # continue语句会直接继续下一轮循环,后续的print()语句不会执行
-
print(n)
执行上面的代码可以看到,打印的不再是1~10,而是1,3,5,7,9。
可见continue
的作用是提前结束本轮循环,并直接开始下一轮循环。
有些时候,如果代码写得有问题,会让程序陷入“死循环”,也就是永远循环下去。这时可以用Ctrl+C
退出程序,或者强制结束Python进程。
dict
Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。
举个例子,假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list:
-
names = ['Michael', 'Bob', 'Tracy']
-
scores = [95, 75, 85]
给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,list越长,耗时越长。
如果用dict实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:
-
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
-
>>> d['Michael']
-
95
为什么dict查找速度这么快?因为dict的实现原理和查字典是一样的。假设字典包含了1万个汉字,我们要查某一个字,一个办法是把字典从第一页往后翻,直到找到我们想要的字为止,这种方法就是在list中查找元素的方法,list越大,查找越慢。
第二种方法是先在字典的索引表里(比如部首表)查这个字对应的页码,然后直接翻到该页,找到这个字。无论找哪个字,这种查找速度都非常快,不会随着字典大小的增加而变慢。
dict就是第二种实现方式,给定一个名字,比如'Michael'
,dict在内部就可以直接计算出Michael
对应的存放成绩的“页码”,也就是95
这个数字存放的内存地址,直接取出来,所以速度非常快。
你可以猜到,这种key-value存储方式,在放进去的时候,必须根据key算出value的存放位置,这样,取的时候才能根据key直接拿到value。
把数据放入dict的方法,除了初始化时指定外,还可以通过key放入
-
>>> d['Adam'] = 67
-
>>> d['Adam']
-
67
由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:
-
>>> d['Jack'] = 90
-
>>> d['Jack']
-
90
-
>>> d['Jack'] = 88
-
>>> d['Jack']
-
88
如果key不存在,dict就会报错:
-
>>> d['Thomas']
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
KeyError: 'Thomas'
要避免key不存在的错误,有两种办法,一是通过in
判断key是否存在:
-
>>> 'Thomas' in d
-
False
二是通过dict提供的get()
方法,如果key不存在,可以返回None
,或者自己指定的value:
-
>>> d.get('Thomas')
-
>>> d.get('Thomas', -1)
-
-1
注意:返回None
的时候Python的交互环境不显示结果。
要删除一个key,用pop(key)
方法,对应的value也会从dict中删除:
-
>>> d.pop('Bob')
-
75
-
>>> d
-
{'Michael': 95, 'Tracy': 85}
请务必注意,dict内部存放的顺序和key放入的顺序是没有关系的。
和list比较,dict有以下几个特点:
- 查找和插入的速度极快,不会随着key的增加而变慢;
- 需要占用大量的内存,内存浪费多。
而list相反:
- 查找和插入的时间随着元素的增加而增加;
- 占用空间小,浪费内存很少。
所以,dict是用空间来换取时间的一种方法。
dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象。
这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。
要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:
-
>>> key = [1, 2, 3]
-
>>> d[key] = 'a list'
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: unhashable type: 'list'
set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
要创建一个set,需要提供一个list作为输入集合:
-
>>> s = set([1, 2, 3])
-
>>> 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])
-
>>> s
-
{1, 2, 3}
通过add(key)
方法可以添加元素到set中,可以重复添加,但不会有效果:
-
>>> s.add(4)
-
>>> s
-
{1, 2, 3, 4}
-
>>> s.add(4)
-
>>> s
-
{1, 2, 3, 4}
通过remove(key)
方法可以删除元素:
-
>>> s.remove(4)
-
>>> s
-
{1, 2, 3}
set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:
-
>>> s1 = set([1, 2, 3])
-
>>> s2 = set([2, 3, 4])
-
>>> s1 & s2
-
{2, 3}
-
>>> s1 | s2
-
{1, 2, 3, 4}
set和dict的唯一区别仅在于没有存储对应的value,但是,set的原理和dict一样,所以,同样不可以放入可变对象,因为无法判断两个可变对象是否相等,也就无法保证set内部“不会有重复元素”。试试把list放入set,看看是否会报错。
不可变对象
上面我们讲了,str是不变对象,而list是可变对象。
对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:
-
>>> a = ['c', 'b', 'a']
-
>>> a.sort()
-
>>> a
-
['a', 'b', 'c']
而对于不可变对象,比如str,对str进行操作呢:
-
>>> a = 'abc'
-
>>> a.replace('a', 'A')
-
'Abc'
-
>>> a
-
'abc'
虽然字符串有个replace()
方法,也确实变出了'Abc'
,但变量a
最后仍是'abc'
,应该怎么理解呢?
我们先把代码改成下面这样:
-
>>> a = 'abc'
-
>>> b = a.replace('a', 'A')
-
>>> b
-
'Abc'
-
>>> a
-
'abc'
要始终牢记的是,a
是变量,而'abc'
才是字符串对象!有些时候,我们经常说,对象a
的内容是'abc'
,但其实是指,a
本身是一个变量,它指向的对象的内容才是'abc'
:
当我们调用a.replace('a', 'A')
时,实际上调用方法replace
是作用在字符串对象'abc'
上的,而这个方法虽然名字叫replace
,但却没有改变字符串'abc'
的内容。相反,replace
方法创建了一个新字符串'Abc'
并返回,如果我们用变量b
指向该新字符串,就容易理解了,变量a
仍指向原有的字符串'abc'
,但变量b
却指向新字符串'Abc'
了:
所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。
小结
使用key-value存储结构的dict在Python中非常有用,选择不可变对象作为key很重要,最常用的key是字符串。
tuple虽然是不变对象,但试试把(1, 2, 3)
和(1, [2, 3])
放入dict或set中,并解释结果。
调用函数
Python内置了很多有用的函数,我们可以直接调用。
要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs
,只有一个参数。可以直接从Python的官方网站查看文档:
Built-in Functions — Python 3.10.6 documentation
也可以在交互式命令行通过help(abs)
查看abs
函数的帮助信息。
调用abs
函数:
-
>>> abs(100)
-
100
-
>>> abs(-20)
-
20
-
>>> abs(12.34)
-
12.34
调用函数的时候,如果传入的参数数量不对,会报TypeError
的错误,并且Python会明确地告诉你:abs()
有且仅有1个参数,但给出了两个:
-
>>> abs(1, 2)
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: abs() takes exactly one argument (2 given)
如果传入的参数数量是对的,但参数类型不能被函数所接受,也会报TypeError
的错误,并且给出错误信息:str
是错误的参数类型:
-
>>> abs('a')
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: bad operand type for abs(): 'str'
而max
函数max()
可以接收任意多个参数,并返回最大的那个:
-
>>> max(1, 2)
-
2
-
>>> max(2, 3, 1, -5)
-
3
数据类型转换
Python内置的常用函数还包括数据类型转换函数,比如int()
函数可以把其他数据类型转换为整数:
-
>>> int('123')
-
123
-
>>> int(12.34)
-
12
-
>>> float('12.34')
-
12.34
-
>>> str(1.23)
-
'1.23'
-
>>> str(100)
-
'100'
-
>>> bool(1)
-
True
-
>>> bool('')
-
False
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
-
>>> a = abs # 变量a指向abs函数
-
>>> a(-1) # 所以也可以通过a调用abs函数
-
1
定义函数
在Python中,定义一个函数要使用def
语句,依次写出函数名、括号、括号中的参数和冒号:
,然后,在缩进块中编写函数体,函数的返回值用return
语句返回。
我们以自定义一个求绝对值的my_abs
函数为例:
-
def my_abs(x):
-
if x >= 0:
-
return x
-
else:
-
return -x
请自行测试并调用my_abs
看看返回结果是否正确。
请注意,函数体内部的语句在执行时,一旦执行到return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为None
。return None
可以简写为return
。
在Python交互环境中定义函数时,注意Python会出现...
的提示。函数定义结束后需要按两次回车重新回到>>>
提示符下:
如果你已经把my_abs()
的函数定义保存为abstest.py
文件了,那么,可以在该文件的当前目录下启动Python解释器,用from abstest import my_abs
来导入my_abs()
函数,注意abstest
是文件名(不含.py
扩展名):
import
的用法在后续模块一节中会详细介绍。
空函数
如果想定义一个什么事也不做的空函数,可以用pass
语句:
-
def nop():
-
pass
pass
语句什么都不做,那有什么用?实际上pass
可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass
,让代码能运行起来。
pass
还可以用在其他语句里,比如:
-
if age >= 18:
-
pass
缺少了pass
,代码运行就会有语法错误。
参数检查
调用函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError
:
-
>>> my_abs(1, 2)
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: my_abs() takes 1 positional argument but 2 were given
但是如果参数类型不对,Python解释器就无法帮我们检查。试试my_abs
和内置函数abs
的差别:
-
>>> my_abs('A')
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
File "<stdin>", line 2, in my_abs
-
TypeError: unorderable types: str() >= int()
-
>>> abs('A')
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: bad operand type for abs(): 'str'
当传入了不恰当的参数时,内置函数abs
会检查出参数错误,而我们定义的my_abs
没有参数检查,会导致if
语句出错,出错信息和abs
不一样。所以,这个函数定义不够完善。
让我们修改一下my_abs
的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()
实现:
-
def my_abs(x):
-
if not isinstance(x, (int, float)):
-
raise TypeError('bad operand type')
-
if x >= 0:
-
return x
-
else:
-
return -x
添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
-
>>> my_abs('A')
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
File "<stdin>", line 3, in my_abs
-
TypeError: bad operand type
错误和异常处理将在后续讲到。
返回多个值
函数可以返回多个值吗?答案是肯定的。
比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的新的坐标:
-
import math
-
def move(x, y, step, angle=0):
-
nx = x + step * math.cos(angle)
-
ny = y - step * math.sin(angle)
-
return nx, ny
import math
语句表示导入math
包,并允许后续代码引用math
包里的sin
、cos
等函数。
然后,我们就可以同时获得返回值:
-
>>> x, y = move(100, 100, 60, math.pi / 6)
-
>>> print(x, y)
-
151.96152422706632 70.0
但其实这只是一种假象,Python函数返回的仍然是单一值:
-
>>> r = move(100, 100, 60, math.pi / 6)
-
>>> print(r)
-
(151.96152422706632, 70.0)
原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
小结
定义函数时,需要确定函数名和参数个数;
如果有必要,可以先对参数的数据类型做检查;
函数体内部可以用return
随时返回函数结果;
函数执行完毕也没有return
语句时,自动return None
。
函数可以同时返回多个值,但其实就是一个tuple。
函数的参数
定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
位置参数
我们先写一个计算x2的函数:
-
def power(x):
-
return x * x
对于power(x)
函数,参数x
就是一个位置参数。
当我们调用power
函数时,必须传入有且仅有的一个参数x
:
-
>>> power(5)
-
25
-
>>> power(15)
-
225
现在,如果我们要计算x3怎么办?可以再定义一个power3
函数,但是如果要计算x4、x5……怎么办?我们不可能定义无限多个函数。
你也许想到了,可以把power(x)
修改为power(x, n)
,用来计算xn,说干就干:
-
def power(x, n):
-
s = 1
-
while n > 0:
-
n = n - 1
-
s = s * x
-
return s
对于这个修改后的power(x, n)
函数,可以计算任意n次方:
-
>>> power(5, 2)
-
25
-
>>> power(5, 3)
-
125
修改后的power(x, n)
函数有两个参数:x
和n
,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数x
和n
。
默认参数
新的power(x, n)
函数定义没有问题,但是,旧的调用代码失败了,原因是我们增加了一个参数,导致旧的代码因为缺少一个参数而无法正常调用:
-
>>> power(5)
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: power() missing 1 required positional argument: 'n'
Python的错误信息很明确:调用函数power()
缺少了一个位置参数n
。
这个时候,默认参数就排上用场了。由于我们经常计算x2,所以,完全可以把第二个参数n的默认值设定为2:
-
def power(x, n=2):
-
s = 1
-
while n > 0:
-
n = n - 1
-
s = s * x
-
return s
这样,当我们调用power(5)
时,相当于调用power(5, 2)
:
-
>>> power(5)
-
25
-
>>> power(5, 2)
-
25
而对于n > 2
的其他情况,就必须明确地传入n,比如power(5, 3)
。
从上面的例子可以看出,默认参数可以简化函数的调用。设置默认参数时,有几点要注意:
一是必选参数在前,默认参数在后,否则Python的解释器会报错(思考一下为什么默认参数不能放在必选参数前面);
二是如何设置默认参数。
当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
使用默认参数有什么好处?最大的好处是能降低调用函数的难度。
举个例子,我们写个一年级小学生注册的函数,需要传入name
和gender
两个参数:
-
def enroll(name, gender):
-
print('name:', name)
-
print('gender:', gender)
这样,调用enroll()
函数只需要传入两个参数:
-
>>> enroll('Sarah', 'F')
-
name: Sarah
-
gender: F
如果要继续传入年龄、城市等信息怎么办?这样会使得调用函数的复杂度大大增加。
我们可以把年龄和城市设为默认参数:
-
def enroll(name, gender, age=6, city='Beijing'):
-
print('name:', name)
-
print('gender:', gender)
-
print('age:', age)
-
print('city:', city)
这样,大多数学生注册时不需要提供年龄和城市,只提供必须的两个参数:
-
>>> enroll('Sarah', 'F')
-
name: Sarah
-
gender: F
-
age: 6
-
city: Beijing
只有与默认参数不符的学生才需要提供额外的信息:
-
enroll('Bob', 'M', 7)
-
enroll('Adam', 'M', city='Tianjin')
可见,默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。
有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用enroll('Bob', 'M', 7)
,意思是,除了name
,gender
这两个参数外,最后1个参数应用在参数age
上,city
参数由于没有提供,仍然使用默认值。
也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll('Adam', 'M', city='Tianjin')
,意思是,city
参数用传进去的值,其他默认参数继续使用默认值。
默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个list,添加一个END
再返回:
-
def add_end(L=[]):
-
L.append('END')
-
return L
当你正常调用时,结果似乎不错:
-
>>> add_end([1, 2, 3])
-
[1, 2, 3, 'END']
-
>>> add_end(['x', 'y', 'z'])
-
['x', 'y', 'z', 'END']
当你使用默认参数调用时,一开始结果也是对的:
-
>>> add_end()
-
['END']
但是,再次调用add_end()
时,结果就不对了:
-
>>> add_end()
-
['END', 'END']
-
>>> add_end()
-
['END', 'END', 'END']
很多初学者很疑惑,默认参数是[]
,但是函数似乎每次都“记住了”上次添加了'END'
后的list。
原因解释如下:
Python函数在定义的时候,默认参数L
的值就被计算出来了,即[]
,因为默认参数L
也是一个变量,它指向对象[]
,每次调用该函数,如果改变了L
的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]
了。
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None
这个不变对象来实现:
-
def add_end(L=None):
-
if L is None:
-
L = []
-
L.append('END')
-
return L
现在,无论调用多少次,都不会有问题:
-
>>> add_end()
-
['END']
-
>>> add_end()
-
['END']
为什么要设计str
、None
这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
可变参数
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。
我们以数学题为例子,给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……。
要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:
-
def calc(numbers):
-
sum = 0
-
for n in numbers:
-
sum = sum + n * n
-
return sum
但是调用的时候,需要先组装出一个list或tuple:
-
>>> calc([1, 2, 3])
-
14
-
>>> calc((1, 3, 5, 7))
-
84
如果利用可变参数,调用函数的方式可以简化成这样:
-
>>> calc(1, 2, 3)
-
14
-
>>> calc(1, 3, 5, 7)
-
84
所以,我们把函数的参数改为可变参数:
-
def calc(*numbers):
-
sum = 0
-
for n in numbers:
-
sum = sum + n * n
-
return sum
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*
号。在函数内部,参数numbers
接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
-
>>> calc(1, 2)
-
5
-
>>> calc()
-
0
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
-
>>> nums = [1, 2, 3]
-
>>> calc(nums[0], nums[1], nums[2])
-
14
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*
号,把list或tuple的元素变成可变参数传进去:
-
>>> nums = [1, 2, 3]
-
>>> calc(*nums)
-
14
*nums
表示把nums
这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
-
def person(name, age, **kw):
-
print('name:', name, 'age:', age, 'other:', kw)
函数person
除了必选参数name
和age
外,还接受关键字参数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
这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
-
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
-
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
-
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
当然,上面复杂的调用可以用简化的写法:
-
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
-
>>> person('Jack', 24, **extra)
-
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
**extra
表示把extra
这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw
将获得一个dict,注意kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw
检查。
仍以person()
函数为例,我们希望检查是否有city
和job
参数:
-
def person(name, age, **kw):
-
if 'city' in kw:
-
# 有city参数
-
pass
-
if 'job' in kw:
-
# 有job参数
-
pass
-
print('name:', name, 'age:', age, 'other:', kw)
但是调用者仍可以传入不受限制的关键字参数:
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city
和job
作为关键字参数。这种方式定义的函数如下:
-
def person(name, age, *, city, job):
-
print(name, age, city, job)
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。
调用方式如下:
-
>>> person('Jack', 24, city='Beijing', job='Engineer')
-
Jack 24 Beijing Engineer
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
-
def person(name, age, *args, city, job):
-
print(name, age, args, city, job)
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
-
>>> person('Jack', 24, 'Beijing', 'Engineer')
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
TypeError: person() takes 2 positional arguments but 4 were given
由于调用时缺少参数名city
和job
,Python解释器把这4个参数均视为位置参数,但person()
函数仅接受2个位置参数。
命名关键字参数可以有缺省值,从而简化调用:
-
def person(name, age, *, city='Beijing', job):
-
print(name, age, city, job)
由于命名关键字参数city
具有默认值,调用时,可不传入city
参数:
-
>>> person('Jack', 24, job='Engineer')
-
Jack 24 Beijing Engineer
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数:
-
def person(name, age, city, job):
-
# 缺少 *,city和job被视为位置参数
-
pass
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
-
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)
在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
-
>>> 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,你也可以调用上述函数:
-
>>> 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种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。
练习
以下函数允许计算两个数的乘积,请稍加改造,变成可接收一个或多个数并计算乘积:
Run
小结
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的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*
,否则定义的将是位置参数。
递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n
,用函数fact(n)
表示,可以看出:
于是,fact(n)
用递归的方式写出来就是:
-
def fact(n):
-
if n==1:
-
return 1
-
return n * fact(n - 1)
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000)
:
-
>>> fact(1000)
-
Traceback (most recent call last):
-
File "<stdin>", line 1, in <module>
-
File "<stdin>", line 4, in fact
-
...
-
File "<stdin>", line 4, in fact
-
RuntimeError: maximum recursion depth exceeded in comparison
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
上面的fact(n)
函数由于return n * fact(n - 1)
引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:
-
def fact(n):
-
return fact_iter(n, 1)
-
def fact_iter(num, product):
-
if num == 1:
-
return product
-
return fact_iter(num - 1, num * product)
可以看到,return fact_iter(num - 1, num * product)
仅返回递归函数本身,num - 1
和num * product
在函数调用前就会被计算,不影响函数调用。
fact(5)
对应的fact_iter(5, 1)
的调用如下:
-
===> fact_iter(5, 1)
-
===> fact_iter(4, 5)
-
===> fact_iter(3, 20)
-
===> fact_iter(2, 60)
-
===> fact_iter(1, 120)
-
===> 120
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)
函数改成尾递归方式,也会导致栈溢出。
小结
使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。