目录

​python基础语法​

​输出​

​输入​

​数据类型​

​变量​

​常量​

​字符编码​

​Python的字符串​

​格式化​

​tuple​

​条件判断​

​ input​

​循环​

​dict​

​set​

​不可变对象​

​小结​

​调用函数​

​数据类型转换​

​定义函数​

​空函数​

​参数检查​

​返回多个值​

​小结​

​函数的参数​

​位置参数​

​默认参数​

​可变参数​

​关键字参数​

​命名关键字参数​

​参数组合​

​练习​

​小结​

​递归函数​

​小结​


python基础语法

#1.基础操作
age = 20 # 声明一个变量age 用来存储一个数字 20
1+1 # 基础数学加法
print('Hello World!') # 打印Hello World!


#2.条件判断if
if 1 == 2: # 如果 if 跟随的条件为 那么不执行属于if 的语句,然后寻找 else
print("假的")
else: # 寻找到 else 之后 执行属于else中的语句
print("1==2是假的")


#3.循环操作---for
for i in range(5):
print(i)


#3.循环操作---while
sum = 0
n = 99
while n > 0:
sum = sum + n
n = n - 1


#4.break、continue、pass
#break语句可以跳出 for while 的循环体
n = 1
while n <= 100:
if n > 10:
break
print(n)
n += 1


#continue语句跳过当前循环,直接进行下一轮循环
n = 1
while n < 10:
n = n + 1
if n % 2 == 0:
continue
print(n)


#pass是空语句,一般用做占位语句,不做任何事情
for letter in 'Room':
if letter == 'o':
pass
print('pass')
print(letter)


R
pass
o
pass
#5.数据类型---Number(数字)
#Python支持int, float, complex三种不同的数字类型
a = 3
b = 3.14
c = 3 + 4j
print(type(a), type(b), type(c))


<class 'int'> <class 'float'> <class 'complex'>

python基础语法总结_Python


#5.数据类型---String(字符串)
#支持字符串拼接、截取等多种运算
a = "Hello"
b = "Python"
print("a + b 输出结果:", a + b)
#print("a[1:4] 输出结果:", a[1:4])


a + b

python基础语法总结_ico_02


#5.数据类型---List(列表)
#列表是写在方括号 [] 之间、用逗号分隔开的元素列表。
#列表索引值以 0 为开始值,-1 为从末尾的开始位置。
list = ['abcd', 786 , 2.23, 'runoob', 70.2]
print(list[1:3])

#tinylist = [123, 'runoob']
#print(list + tinylist)


[786, 2.23]

#tuple与list类似,不同之处在于tuple的元素不能修改。tuple写在小括号里,元素之间用逗号隔开。
#元组的元素不可变,但可以包含可变对象,如list。

#5.数据类型---Tuple(元组)
#tuple与list类似,不同之处在于tuple的元素不能修改。tuple写在小括号里,元素之间用逗号隔开。
#元组的元素不可变,但可以包含可变对象,如list。
t1 = ('abcd', 786 , 2.23, 'runoob', 70.2)
t2 = (1, )
t3 = ('a', 'b', ['A', 'B'])
t3[2][0]


('a', 'b', ['X', 'B'])
#5.数据类型---dict(字典)
#字典是无序的对象集合,使用键-值(key-value)存储,具有极快的查找速度。
#键(key)必须使用不可变类型。
#同一个字典中,键(key)必须是唯一的。
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
print(d['Michael'])


95
#5.---set
#setdictkeyvaluekeysetkey
#setset
s = set([1, 1, 2, 2, 3, 3])
print(s)


{1, 2, 3}

输出

用​​print()​​​在括号中加上字符串,就可以向屏幕上输出指定的文字。比如输出​​'hello, world'​​,用代码实现如下:

>>> print('hello, world')


​print()​​函数也可以接受多个字符串,用逗号“,”隔开,就可以连成一串输出:

  1. ​>>> print('The quick brown fox', 'jumps over', 'the lazy dog')​
  2. ​The quick brown fox jumps over the lazy dog​

​print()​​会依次打印每个字符串,遇到逗号“,”会输出一个空格

​print()​​也可以打印整数,或者计算结果:

  1. ​>>> print(300)​
  2. ​300​
  3. ​>>> print(100 + 200)​
  4. ​300​

因此,我们可以把计算​​100 + 200​​的结果打印得更漂亮一点:

  1. ​>>> print('100 + 200 =', 100 + 200)​
  2. ​100 + 200 = 300​

输入

Python提供了一个​​input()​​,可以让用户输入字符串,并存放到一个变量里。比如输入用户的名字:

  1. ​>>> name = input()​
  2. ​Michael​

当你输入​​name = input()​​并按下回车后,Python交互式命令行就在等待你的输入了。这时,你可以输入任意字符,然后按回车后完成输入。

输入完成后,不会有任何提示,Python交互式命令行又回到​​>>>​​​状态了。那我们刚才输入的内容到哪去了?答案是存放到​​name​​​变量里了。可以直接输入​​name​​查看变量内容:

  1. ​>>> name​
  2. ​'Michael'​

结合输入输出

  1. ​name = input()​
  2. ​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个字符。

如果字符串内部既包含​​'​​​又包含​​"​​怎么办?可以用转义字符``来标识,比如:

'I'm "OK"!'


表示的字符串内容是:

I'm "OK"!


转义字符​​可以转义很多字符,比如`\n`表示换行,`\t`表示制表符,字符​​​本身也要转义,所以​​\​​​表示的字符就是``,可以在Python的交互式命令行用​​print()​​打印字符串看看:

  1. ​>>> print('I'm ok.')​
  2. ​I'm ok.​
  3. ​>>> print('I'm learning\nPython.')​
  4. ​I'm learning​
  5. ​Python.​
  6. ​>>> print('\\n\')​
  7. ``
  8. ``

如果字符串里面有很多字符都需要转义,就需要加很多``,为了简化,Python还允许用r''表示​''​内部的字符串默认不转义,可以自己试试:

  1. ​>>> print('\\t\')​
  2. ​\ ​
  3. ​>>> print(r'\\t\')​
  4. ​\\t\​

如果字符串内部有很多换行,用​​\n​​写在一行里不好阅读,为了简化,Python允许用'''...'''的格式表示多行内容,可以自己试试:

  1. ​>>> print('''line1​
  2. ​... line2​
  3. ​... line3''')​
  4. ​line1​
  5. ​line2​
  6. ​line3​

上面是在交互式命令行内输入,注意在输入多行内容时,提示符由​​>>>​​​变为​​...​​​,提示你可以接着上一行输入,注意​​...​​是提示符,不是代码的一部分:

┌────────────────────────────────────────────────────────┐
│Command Prompt - python _ x
├────────────────────────────────────────────────────────┤
>>> print('''line1 │
│... line2 │
│... line3''')
│line1
│line2
│line3

>>> _



└────────────────────────────────────────────────────────┘


当输入完结束符​​```​​​和括号​​)​​后,执行该语句并打印结果。

如果写成程序并存为​​.py​​文件,就是:

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

多行字符串'''...'''还可以在前面加上​r​使用

布尔值

布尔值和布尔代数的表示完全一致,一个布尔值只有​​True​​​、​​False​​​两种值,要么是​​True​​​,要么是​​False​​​,在Python中,可以直接用​​True​​​、​​False​​表示布尔值(请注意大小写),也可以通过布尔运算计算出来:

  1. ​>>> True​
  2. ​True​

​not​​​运算是非运算,它是一个单目运算符,把​​True​​​变成​​False​​​,​​False​​​变成​​True​​:

  1. ​>>> not True​
  2. ​False​

空值

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

此外,Python还提供了列表、字典等多种数据类型,还允许创建自定义数据类型,我们后面会继续讲到。

变量

变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型。

变量在程序中就是用一个变量名表示了,变量名必须是大小写英文、数字和​​_​​的组合,且不能用数字开头,比如:

a = 1


变量​​a​​是一个整数。

t_007 = 'T007'


变量​​t_007​​是一个字符串。

Answer = True


变量​​Answer​​​是一个布尔值​​True​​。

在Python中,等号​​=​​是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量

这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。

最后,理解变量在计算机内存中的表示也非常重要。当我们写:

a = 'ABC'


时,Python解释器干了两件事情:

  1. 在内存中创建了一个​​'ABC'​​的字符串;
  2. 在内存中创建了一个名为​​a​​​的变量,并把它指向​​'ABC'​​。

也可以把一个变量​​a​​​赋值给另一个变量​​b​​​,这个操作实际上是把变量​​b​​​指向变量​​a​​所指向的数据

常量

所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。在Python中,通常用全部大写的变量名表示常量:

PI = 3.14159265359


但事实上​​PI​​​仍然是一个变量,Python根本没有任何机制保证​​PI​​​不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改变变量​​PI​​的值,也没人能拦住你。

最后解释一下整数的除法为什么也是精确的。在Python中,有两种除法,一种除法是/

  1. ​>>> 10 / 3​
  2. ​3.3333333333333335​

/除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:

  1. ​>>> 9 / 3​
  2. ​3.0​

还有一种除法是//,称为地板除,两个整数的除法仍然是整数:

  1. ​>>> 10 // 3​
  2. ​3​

你没有看错,整数的地板除//永远是整数,即使除不尽。要做精确的除法,使用​/​就可以。

因为//除法只取结果的整数部分,所以Python还提供一个余数运算,可以得到两个整数相除的余数:

  1. ​>>> 10 % 3​
  2. ​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()​​函数把编码转换为对应的字符:

  1. ​>>> ord('A')​
  2. ​65​
  3. ​>>> ord('中')​
  4. ​20013​
  5. ​>>> chr(66)​
  6. ​'B'​
  7. ​>>> chr(25991)​
  8. ​'文'​

如果知道字符的整数编码,还可以用十六进制这么写​​str​​:

  1. ​>>> '\u4e2d\u6587'​
  2. ​'中文'​

两种写法完全是等价的。

由于Python的字符串类型是​​str​​​,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把​​str​​​变为以字节为单位的​​bytes​​。

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

x = b'ABC'


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

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

  1. ​>>> 'ABC'.encode('ascii')​
  2. ​b'ABC'​
  3. ​>>> '中文'.encode('utf-8')​
  4. ​b'\xe4\xb8\xad\xe6\x96\x87'​
  5. ​>>> '中文'.encode('ascii')​
  6. ​Traceback (most recent call last):​
  7. ​File "<stdin>", line 1, in <module>​
  8. ​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()​​方法:

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

如果​​bytes​​​中包含无法解码的字节,​​decode()​​方法会报错:

  1. ​>>> b'\xe4\xb8\xad\xff'.decode('utf-8')​
  2. ​Traceback (most recent call last):​
  3. ​...​
  4. ​UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte​

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

  1. ​>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')​
  2. ​'中'​

要计算str包含多少个字符,可以用​len()​函数:

  1. ​>>> len('ABC')​
  2. ​3​
  3. ​>>> len('中文')​
  4. ​2​

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

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

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

格式化

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

  1. ​>>> 'Hello, %s' % 'world'​
  2. ​'Hello, world'​
  3. ​>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)​
  4. ​'Hi, Michael, you have $1000000.'​

你可能猜到了,%运算符就是用来格式化字符串的。在字符串内部,​%s​表示用字符串替换,​%d​表示用整数替换,有几个​%?​占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个​%?​,括号可以省略。

常见的占位符有:

占位符

替换内容

%d

整数

%f

浮点数

%s

字符串

%x

十六进制整数

其中,格式化整数和浮点数还可以指定是否补0和整数与小数的位数:

  1. ​print('%2d-%02d' % (3, 1))​
  2. ​print('%.2f' % 3.1415926)​

如果你不太确定应该用什么,%s永远起作用,它会把任何数据类型转换为字符串:

  1. ​>>> 'Age: %s. Gender: %s' % (25, True)​
  2. ​'Age: 25. Gender: True'​

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

  1. ​>>> 'growth rate: %d %%' % 7​
  2. ​'growth rate: 7 %'​

format()

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

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

list

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

比如,列出班里所有同学的名字,就可以用一个list表示:

  1. ​>>> classmates = ['Michael', 'Bob', 'Tracy']​
  2. ​>>> classmates​
  3. ​['Michael', 'Bob', 'Tracy']​

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

  1. ​>>> len(classmates)​
  2. ​3​

用索引来访问list中每一个位置的元素,记得索引是从​​0​​开始的:

  1. ​>>> classmates[0]​
  2. ​'Michael'​
  3. ​>>> classmates[1]​
  4. ​'Bob'​
  5. ​>>> classmates[2]​
  6. ​'Tracy'​
  7. ​>>> classmates[3]​
  8. ​Traceback (most recent call last):​
  9. ​File "<stdin>", line 1, in <module>​
  10. ​IndexError: list index out of range​

当索引超出了范围时,Python会报一个​​IndexError​​​错误,所以,要确保索引不要越界,记得最后一个元素的索引是​​len(classmates) - 1​​。

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

  1. ​>>> classmates[-1]​
  2. ​'Tracy'​

以此类推,可以获取倒数第2个、倒数第3个:

  1. ​>>> classmates[-2]​
  2. ​'Bob'​
  3. ​>>> classmates[-3]​
  4. ​'Michael'​
  5. ​>>> classmates[-4]​
  6. ​Traceback (most recent call last):​
  7. ​File "<stdin>", line 1, in <module>​
  8. ​IndexError: list index out of range​

当然,倒数第4个就越界了。

list是一个可变的有序表,所以,可以往list中追加元素到末尾:

  1. ​>>> classmates.append('Adam')​
  2. ​>>> classmates​
  3. ​['Michael', 'Bob', 'Tracy', 'Adam']​

也可以把元素插入到指定的位置,比如索引号为​​1​​的位置:

  1. ​>>> classmates.insert(1, 'Jack')​
  2. ​>>> classmates​
  3. ​['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']​

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

  1. ​>>> classmates.pop()​
  2. ​'Adam'​
  3. ​>>> classmates​
  4. ​['Michael', 'Jack', 'Bob', 'Tracy']​

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

  1. ​>>> classmates.pop(1)​
  2. ​'Jack'​
  3. ​>>> classmates​
  4. ​['Michael', 'Bob', 'Tracy']​

要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:

  1. ​>>> classmates[1] = 'Sarah'​
  2. ​>>> classmates​
  3. ​['Michael', 'Sarah', 'Tracy']​

list里面的元素的数据类型也可以不同,比如:

>>> L = ['Apple', 123, True]


list元素也可以是另一个list,比如:

  1. ​>>> s = ['python', 'java', ['asp', 'php'], 'scheme']​
  2. ​>>> len(s)​
  3. ​4​

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

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

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

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

  1. ​>>> L = []​
  2. ​>>> len(L)​
  3. ​0​

tuple

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

>>> 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)​
  2. ​>>> t​
  3. ​(1, 2)​

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

  1. ​>>> t = ()​
  2. ​>>> t​
  3. ​()​

但是,要定义一个只有1个元素的tuple,如果你这么定义:

  1. ​>>> t = (1)​
  2. ​>>> t​
  3. ​1​

定义的不是tuple,是​​1​​​这个数!这是因为括号​​()​​​既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是​​1​​。

所以,只有1个元素的tuple定义时必须加一个逗号​​,​​,来消除歧义:

  1. ​>>> t = (1,)​
  2. ​>>> t​
  3. ​(1,)​

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

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

  1. ​>>> t = ('a', 'b', ['A', 'B'])​
  2. ​>>> t[2][0] = 'X'​
  3. ​>>> t[2][1] = 'Y'​
  4. ​>>> t​
  5. ​('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​​语句实现:

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

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

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

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

注意不要少写了冒号:

当然上面的判断是很粗略的,完全可以用elif做更细致的判断:

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

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

​if​​判断条件还可以简写,比如写:

  1. ​if x:​
  2. ​print('True')​

只要​​x​​​是非零数值、非空字符串、非空list等,就判断为​​True​​​,否则为​​False​​。

 input

最后看一个有问题的条件判断。很多同学会用​​input()​​读取用户的输入,这样可以自己输入,程序运行得更有意思:

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

输入​​1982​​,结果报错:

  1. ​Traceback (most recent call last):​
  2. ​File "<stdin>", line 1, in <module>​
  3. ​TypeError: unorderable types: str() > int()​

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

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

再次运行,就可以得到正确地结果。但是,如果输入​​abc​​呢?又会得到一个错误信息:

  1. ​Traceback (most recent call last):​
  2. ​File "<stdin>", line 1, in <module>​
  3. ​ValueError: invalid literal for int() with base 10: 'abc'​

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

如何检查并捕获程序运行期的错误呢?后面的错误和调试会讲到。

循环

要计算1+2+3,我们可以直接写表达式:

  1. ​>>> 1 + 2 + 3​
  2. ​6​

要计算1+2+3+...+10,勉强也能写出来。

但是,要计算1+2+3+...+10000,直接写表达式就不可能了。

为了让计算机能计算成千上万次的重复运算,我们就需要循环语句。

Python的循环有两种,一种是for...in循环,依次把list或tuple中的每个元素迭代出来,看例子:
​​​names = ['Michael', 'Bob', 'Tracy']​

  1. ​for name in names:​
  2. ​print(name)​

执行这段代码,会依次打印​​names​​​的每一个元素:
​​​Michael​

  1. ​Bob​
  2. ​Tracy​

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

再比如我们想计算1-10的整数之和,可以用一个​​sum​​变量做累加: 

  1. ​sum = 0​
  2. ​for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:​
  3. ​sum = sum + x​
  4. ​print(sum)​

如果要计算1-100的整数之和,从1写到100有点困难,幸好Python提供一个​​range()​​​函数,可以生成一个整数序列,再通过​​list()​​​函数可以转换为list。比如​​range(5)​​生成的序列是从0开始小于5的整数:

  1. ​>>> list(range(5))​
  2. ​[0, 1, 2, 3, 4]​

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

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

在循环内部变量​​n​​​不断自减,直到变为​​-1​​时,不再满足while条件,循环退出。

break

在循环中,​​break​​​语句可以提前退出循环。例如,本来要循环打印1~100的数字:
​​​n = 1​

  1. ​while n <= 100:​
  2. ​print(n)​
  3. ​n = n + 1​
  4. ​print('END')​

上面的代码可以打印出1~100。

如果要提前结束循环,可以用​​break​​语句:

  1. ​n = 1​
  2. ​while n <= 100:​
  3. ​if n > 10: # 当n = 11时,条件满足,执行break语句​
  4. ​break # break语句会结束当前循环​
  5. ​print(n)​
  6. ​n = n + 1​
  7. ​print('END')​

执行上面的代码可以看到,打印出1~10后,紧接着打印​​END​​,程序结束。

可见​​break​​的作用是提前结束循环。

continue

在循环过程中,也可以通过​​continue​​语句,跳过当前的这次循环,直接开始下一次循环。

  1. ​n = 0​
  2. ​while n < 10:​
  3. ​n = n + 1​
  4. ​print(n)​

上面的程序可以打印出1~10。但是,如果我们想只打印奇数,可以用​​continue​​​语句跳过某些循环:
​​​n = 0​

  1. ​while n < 10:​
  2. ​n = n + 1​
  3. ​if n % 2 == 0: # 如果n是偶数,执行continue语句​
  4. ​continue # continue语句会直接继续下一轮循环,后续的print()语句不会执行​
  5. ​print(n)​

执行上面的代码可以看到,打印的不再是1~10,而是1,3,5,7,9。

可见​​continue​​的作用是提前结束本轮循环,并直接开始下一轮循环。

有些时候,如果代码写得有问题,会让程序陷入“死循环”,也就是永远循环下去。这时可以用​​Ctrl+C​​退出程序,或者强制结束Python进程。

dict

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

举个例子,假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list:

  1. ​names = ['Michael', 'Bob', 'Tracy']​
  2. ​scores = [95, 75, 85]​

给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,list越长,耗时越长。

如果用dict实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:

  1. ​>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}​
  2. ​>>> d['Michael']​
  3. ​95​

为什么dict查找速度这么快?因为dict的实现原理和查字典是一样的。假设字典包含了1万个汉字,我们要查某一个字,一个办法是把字典从第一页往后翻,直到找到我们想要的字为止,这种方法就是在list中查找元素的方法,list越大,查找越慢。

第二种方法是先在字典的索引表里(比如部首表)查这个字对应的页码,然后直接翻到该页,找到这个字。无论找哪个字,这种查找速度都非常快,不会随着字典大小的增加而变慢。

dict就是第二种实现方式,给定一个名字,比如​​'Michael'​​​,dict在内部就可以直接计算出​​Michael​​​对应的存放成绩的“页码”,也就是​​95​​这个数字存放的内存地址,直接取出来,所以速度非常快。

你可以猜到,这种key-value存储方式,在放进去的时候,必须根据key算出value的存放位置,这样,取的时候才能根据key直接拿到value。

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

  1. ​>>> d['Adam'] = 67​
  2. ​>>> d['Adam']​
  3. ​67​

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:

  1. ​>>> d['Jack'] = 90​
  2. ​>>> d['Jack']​
  3. ​90​
  4. ​>>> d['Jack'] = 88​
  5. ​>>> d['Jack']​
  6. ​88​

如果key不存在,dict就会报错:

  1. ​>>> d['Thomas']​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​KeyError: 'Thomas'​

要避免key不存在的错误,有两种办法,一是通过​​in​​判断key是否存在:

  1. ​>>> 'Thomas' in d​
  2. ​False​

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

  1. ​>>> d.get('Thomas')​
  2. ​>>> d.get('Thomas', -1)​
  3. ​-1​

注意:返回​​None​​的时候Python的交互环境不显示结果。

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

  1. ​>>> d.pop('Bob')​
  2. ​75​
  3. ​>>> d​
  4. ​{'Michael': 95, 'Tracy': 85}​

请务必注意,dict内部存放的顺序和key放入的顺序是没有关系的。

和list比较,dict有以下几个特点:

  1. 查找和插入的速度极快,不会随着key的增加而变慢;
  2. 需要占用大量的内存,内存浪费多。

而list相反:

  1. 查找和插入的时间随着元素的增加而增加;
  2. 占用空间小,浪费内存很少。

所以,dict是用空间来换取时间的一种方法。

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

这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。

要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:

  1. ​>>> key = [1, 2, 3]​
  2. ​>>> d[key] = 'a list'​
  3. ​Traceback (most recent call last):​
  4. ​File "<stdin>", line 1, in <module>​
  5. ​TypeError: unhashable type: 'list'​

set

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

要创建一个set,需要提供一个list作为输入集合:

  1. ​>>> s = set([1, 2, 3])​
  2. ​>>> s​
  3. ​{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])​
  2. ​>>> s​
  3. ​{1, 2, 3}​

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

  1. ​>>> s.add(4)​
  2. ​>>> s​
  3. ​{1, 2, 3, 4}​
  4. ​>>> s.add(4)​
  5. ​>>> s​
  6. ​{1, 2, 3, 4}​

通过​​remove(key)​​方法可以删除元素:

  1. ​>>> s.remove(4)​
  2. ​>>> s​
  3. ​{1, 2, 3}​

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

  1. ​>>> s1 = set([1, 2, 3])​
  2. ​>>> s2 = set([2, 3, 4])​
  3. ​>>> s1 & s2​
  4. ​{2, 3}​
  5. ​>>> s1 | s2​
  6. ​{1, 2, 3, 4}​

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

不可变对象

上面我们讲了,str是不变对象,而list是可变对象。

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

  1. ​>>> a = ['c', 'b', 'a']​
  2. ​>>> a.sort()​
  3. ​>>> a​
  4. ​['a', 'b', 'c']​

而对于不可变对象,比如str,对str进行操作呢:


  1. ​>>> a = 'abc'​
  2. ​>>> a.replace('a', 'A')​
  3. ​'Abc'​
  4. ​>>> a​
  5. ​'abc'​

虽然字符串有个​​replace()​​​方法,也确实变出了​​'Abc'​​​,但变量​​a​​​最后仍是​​'abc'​​,应该怎么理解呢?

我们先把代码改成下面这样:

  1. ​>>> a = 'abc'​
  2. ​>>> b = a.replace('a', 'A')​
  3. ​>>> b​
  4. ​'Abc'​
  5. ​>>> a​
  6. ​'abc'​

要始终牢记的是,​​a​​​是变量,而​​'abc'​​​才是字符串对象!有些时候,我们经常说,对象​​a​​​的内容是​​'abc'​​​,但其实是指,​​a​​​本身是一个变量,它指向的对象的内容才是​​'abc'​​:

┌───┐                  ┌───────┐
│ a │─────────────────>│ 'abc'


当我们调用​​a.replace('a', 'A')​​​时,实际上调用方法​​replace​​​是作用在字符串对象​​'abc'​​​上的,而这个方法虽然名字叫​​replace​​​,但却没有改变字符串​​'abc'​​​的内容。相反,​​replace​​​方法创建了一个新字符串​​'Abc'​​​并返回,如果我们用变量​​b​​​指向该新字符串,就容易理解了,变量​​a​​​仍指向原有的字符串​​'abc'​​​,但变量​​b​​​却指向新字符串​​'Abc'​​了:

┌───┐                  ┌───────┐
│ 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​​函数:

  1. ​>>> abs(100)​
  2. ​100​
  3. ​>>> abs(-20)​
  4. ​20​
  5. ​>>> abs(12.34)​
  6. ​12.34​

调用函数的时候,如果传入的参数数量不对,会报​​TypeError​​​的错误,并且Python会明确地告诉你:​​abs()​​有且仅有1个参数,但给出了两个:

  1. ​>>> abs(1, 2)​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​TypeError: abs() takes exactly one argument (2 given)​

如果传入的参数数量是对的,但参数类型不能被函数所接受,也会报​​TypeError​​​的错误,并且给出错误信息:​​str​​是错误的参数类型:

  1. ​>>> abs('a')​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​TypeError: bad operand type for abs(): 'str'​

而​​max​​​函数​​max()​​可以接收任意多个参数,并返回最大的那个:

  1. ​>>> max(1, 2)​
  2. ​2​
  3. ​>>> max(2, 3, 1, -5)​
  4. ​3​

数据类型转换

Python内置的常用函数还包括数据类型转换函数,比如​​int()​​函数可以把其他数据类型转换为整数:

  1. ​>>> int('123')​
  2. ​123​
  3. ​>>> int(12.34)​
  4. ​12​
  5. ​>>> float('12.34')​
  6. ​12.34​
  7. ​>>> str(1.23)​
  8. ​'1.23'​
  9. ​>>> str(100)​
  10. ​'100'​
  11. ​>>> bool(1)​
  12. ​True​
  13. ​>>> bool('')​
  14. ​False​

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

  1. ​>>> a = abs # 变量a指向abs函数​
  2. ​>>> a(-1) # 所以也可以通过a调用abs函数​
  3. ​1​

定义函数

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

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

  1. ​def my_abs(x):​
  2. ​if x >= 0:​
  3. ​return x​
  4. ​else:​
  5. ​return -x​

请自行测试并调用​​my_abs​​看看返回结果是否正确。

请注意,函数体内部的语句在执行时,一旦执行到​​return​​时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。

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

在Python交互环境中定义函数时,注意Python会出现​​...​​​的提示。函数定义结束后需要按两次回车重新回到​​>>>​​提示符下:

┌────────────────────────────────────────────────────────┐
│Command Prompt - python - □ x │
├────────────────────────────────────────────────────────┤
│>>> def my_abs(x): │
│... if x >= 0: │
│... return x │
│... else: │
│... return -x │
│... │
│>>> my_abs(-9) │
│9


如果你已经把​​my_abs()​​​的函数定义保存为​​abstest.py​​​文件了,那么,可以在该文件的当前目录下启动Python解释器,用​​from abstest import my_abs​​​来导入​​my_abs()​​​函数,注意​​abstest​​​是文件名(不含​​.py​​扩展名):

┌────────────────────────────────────────────────────────┐
│Command Prompt - python - x
├────────────────────────────────────────────────────────┤
>>> from abstest import my_abs
>>> my_abs(-9)
│9


​import​​​的用法在后续​​模块​​一节中会详细介绍。

空函数

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


  1. ​def nop():​
  2. ​pass​

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

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


  1. ​if age >= 18:​
  2. ​pass​

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

参数检查

调用函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出​​TypeError​​:


  1. ​>>> my_abs(1, 2)​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​TypeError: my_abs() takes 1 positional argument but 2 were given​

但是如果参数类型不对,Python解释器就无法帮我们检查。试试​​my_abs​​​和内置函数​​abs​​的差别:


  1. ​>>> my_abs('A')​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​File "<stdin>", line 2, in my_abs​
  5. ​TypeError: unorderable types: str() >= int()​
  6. ​>>> abs('A')​
  7. ​Traceback (most recent call last):​
  8. ​File "<stdin>", line 1, in <module>​
  9. ​TypeError: bad operand type for abs(): 'str'​

当传入了不恰当的参数时,内置函数​​abs​​​会检查出参数错误,而我们定义的​​my_abs​​​没有参数检查,会导致​​if​​​语句出错,出错信息和​​abs​​不一样。所以,这个函数定义不够完善。

让我们修改一下​​my_abs​​​的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数​​isinstance()​​实现:


  1. ​def my_abs(x):​
  2. ​if not isinstance(x, (int, float)):​
  3. ​raise TypeError('bad operand type')​
  4. ​if x >= 0:​
  5. ​return x​
  6. ​else:​
  7. ​return -x​

添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:


  1. ​>>> my_abs('A')​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​File "<stdin>", line 3, in my_abs​
  5. ​TypeError: bad operand type​

错误和异常处理将在后续讲到。

返回多个值

函数可以返回多个值吗?答案是肯定的。

比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的新的坐标:


  1. ​import math​

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

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

然后,我们就可以同时获得返回值:


  1. ​>>> x, y = move(100, 100, 60, math.pi / 6)​
  2. ​>>> print(x, y)​
  3. ​151.96152422706632 70.0​

但其实这只是一种假象,Python函数返回的仍然是单一值:


  1. ​>>> r = move(100, 100, 60, math.pi / 6)​
  2. ​>>> print(r)​
  3. ​(151.96152422706632, 70.0)​

原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

小结

定义函数时,需要确定函数名和参数个数;

如果有必要,可以先对参数的数据类型做检查;

函数体内部可以用​​return​​随时返回函数结果;

函数执行完毕也没有​​return​​​语句时,自动​​return None​​。

函数可以同时返回多个值,但其实就是一个tuple。

函数的参数

定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。

Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。

位置参数

我们先写一个计算x2的函数:


  1. ​def power(x):​
  2. ​return x * x​

对于​​power(x)​​​函数,参数​​x​​就是一个位置参数。

当我们调用​​power​​​函数时,必须传入有且仅有的一个参数​​x​​:


  1. ​>>> power(5)​
  2. ​25​
  3. ​>>> power(15)​
  4. ​225​

现在,如果我们要计算x3怎么办?可以再定义一个​​power3​​函数,但是如果要计算x4、x5……怎么办?我们不可能定义无限多个函数。

你也许想到了,可以把​​power(x)​​​修改为​​power(x, n)​​,用来计算xn,说干就干:


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

对于这个修改后的​​power(x, n)​​函数,可以计算任意n次方:


  1. ​>>> power(5, 2)​
  2. ​25​
  3. ​>>> power(5, 3)​
  4. ​125​

修改后的​​power(x, n)​​​函数有两个参数:​​x​​​和​​n​​​,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数​​x​​​和​​n​​。

默认参数

新的​​power(x, n)​​函数定义没有问题,但是,旧的调用代码失败了,原因是我们增加了一个参数,导致旧的代码因为缺少一个参数而无法正常调用:


  1. ​>>> power(5)​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​TypeError: power() missing 1 required positional argument: 'n'​

Python的错误信息很明确:调用函数​​power()​​​缺少了一个位置参数​​n​​。

这个时候,默认参数就排上用场了。由于我们经常计算x2,所以,完全可以把第二个参数n的默认值设定为2:


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

这样,当我们调用​​power(5)​​​时,相当于调用​​power(5, 2)​​:


  1. ​>>> power(5)​
  2. ​25​
  3. ​>>> power(5, 2)​
  4. ​25​

而对于​​n > 2​​​的其他情况,就必须明确地传入n,比如​​power(5, 3)​​。

从上面的例子可以看出,默认参数可以简化函数的调用。设置默认参数时,有几点要注意:

一是必选参数在前,默认参数在后,否则Python的解释器会报错(思考一下为什么默认参数不能放在必选参数前面);

二是如何设置默认参数。

当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

使用默认参数有什么好处?最大的好处是能降低调用函数的难度。

举个例子,我们写个一年级小学生注册的函数,需要传入​​name​​​和​​gender​​两个参数:


  1. ​def enroll(name, gender):​
  2. ​print('name:', name)​
  3. ​print('gender:', gender)​

这样,调用​​enroll()​​函数只需要传入两个参数:


  1. ​>>> enroll('Sarah', 'F')​
  2. ​name: Sarah​
  3. ​gender: F​

如果要继续传入年龄、城市等信息怎么办?这样会使得调用函数的复杂度大大增加。

我们可以把年龄和城市设为默认参数:


  1. ​def enroll(name, gender, age=6, city='Beijing'):​
  2. ​print('name:', name)​
  3. ​print('gender:', gender)​
  4. ​print('age:', age)​
  5. ​print('city:', city)​

这样,大多数学生注册时不需要提供年龄和城市,只提供必须的两个参数:


  1. ​>>> enroll('Sarah', 'F')​
  2. ​name: Sarah​
  3. ​gender: F​
  4. ​age: 6​
  5. ​city: Beijing​

只有与默认参数不符的学生才需要提供额外的信息:


  1. ​enroll('Bob', 'M', 7)​
  2. ​enroll('Adam', 'M', city='Tianjin')​

可见,默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。

有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用​​enroll('Bob', 'M', 7)​​​,意思是,除了​​name​​​,​​gender​​​这两个参数外,最后1个参数应用在参数​​age​​​上,​​city​​参数由于没有提供,仍然使用默认值。

也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用​​enroll('Adam', 'M', city='Tianjin')​​​,意思是,​​city​​参数用传进去的值,其他默认参数继续使用默认值。

默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:

先定义一个函数,传入一个list,添加一个​​END​​再返回:


  1. ​def add_end(L=[]):​
  2. ​L.append('END')​
  3. ​return L​

当你正常调用时,结果似乎不错:


  1. ​>>> add_end([1, 2, 3])​
  2. ​[1, 2, 3, 'END']​
  3. ​>>> add_end(['x', 'y', 'z'])​
  4. ​['x', 'y', 'z', 'END']​

当你使用默认参数调用时,一开始结果也是对的:


  1. ​>>> add_end()​
  2. ​['END']​

但是,再次调用​​add_end()​​时,结果就不对了:


  1. ​>>> add_end()​
  2. ​['END', 'END']​
  3. ​>>> add_end()​
  4. ​['END', 'END', 'END']​

很多初学者很疑惑,默认参数是​​[]​​​,但是函数似乎每次都“记住了”上次添加了​​'END'​​后的list。

原因解释如下:

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

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

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


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

现在,无论调用多少次,都不会有问题:


  1. ​>>> add_end()​
  2. ​['END']​
  3. ​>>> add_end()​
  4. ​['END']​

为什么要设计​​str​​​、​​None​​这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数

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

我们以数学题为例子,给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……。

要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:


  1. ​def calc(numbers):​
  2. ​sum = 0​
  3. ​for n in numbers:​
  4. ​sum = sum + n * n​
  5. ​return sum​

但是调用的时候,需要先组装出一个list或tuple:


  1. ​>>> calc([1, 2, 3])​
  2. ​14​
  3. ​>>> calc((1, 3, 5, 7))​
  4. ​84​

如果利用可变参数,调用函数的方式可以简化成这样:


  1. ​>>> calc(1, 2, 3)​
  2. ​14​
  3. ​>>> calc(1, 3, 5, 7)​
  4. ​84​

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


  1. ​def calc(*numbers):​
  2. ​sum = 0​
  3. ​for n in numbers:​
  4. ​sum = sum + n * n​
  5. ​return sum​

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


  1. ​>>> calc(1, 2)​
  2. ​5​
  3. ​>>> calc()​
  4. ​0​

如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:


  1. ​>>> nums = [1, 2, 3]​
  2. ​>>> calc(nums[0], nums[1], nums[2])​
  3. ​14​

这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个​​*​​号,把list或tuple的元素变成可变参数传进去:


  1. ​>>> nums = [1, 2, 3]​
  2. ​>>> calc(*nums)​
  3. ​14​

​*nums​​​表示把​​nums​​这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。

关键字参数

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


  1. ​def person(name, age, **kw):​
  2. ​print('name:', name, 'age:', age, 'other:', kw)​

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


  1. ​>>> person('Michael', 30)​
  2. ​name: Michael age: 30 other: {}​

也可以传入任意个数的关键字参数:


  1. ​>>> person('Bob', 35, city='Beijing')​
  2. ​name: Bob age: 35 other: {'city': 'Beijing'}​
  3. ​>>> person('Adam', 45, gender='M', job='Engineer')​
  4. ​name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}​

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

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


  1. ​>>> extra = {'city': 'Beijing', 'job': 'Engineer'}​
  2. ​>>> person('Jack', 24, city=extra['city'], job=extra['job'])​
  3. ​name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}​

当然,上面复杂的调用可以用简化的写法:


  1. ​>>> extra = {'city': 'Beijing', 'job': 'Engineer'}​
  2. ​>>> person('Jack', 24, **extra)​
  3. ​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​​参数:


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

但是调用者仍可以传入不受限制的关键字参数:

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


如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收​​city​​​和​​job​​作为关键字参数。这种方式定义的函数如下:


  1. ​def person(name, age, *, city, job):​
  2. ​print(name, age, city, job)​

和关键字参数​​**kw​​​不同,命名关键字参数需要一个特殊分隔符​​*​​​,​​*​​后面的参数被视为命名关键字参数。

调用方式如下:


  1. ​>>> person('Jack', 24, city='Beijing', job='Engineer')​
  2. ​Jack 24 Beijing Engineer​

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符​​*​​了:


  1. ​def person(name, age, *args, city, job):​
  2. ​print(name, age, args, city, job)​

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:


  1. ​>>> person('Jack', 24, 'Beijing', 'Engineer')​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​TypeError: person() takes 2 positional arguments but 4 were given​

由于调用时缺少参数名​​city​​​和​​job​​​,Python解释器把这4个参数均视为位置参数,但​​person()​​函数仅接受2个位置参数。

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


  1. ​def person(name, age, *, city='Beijing', job):​
  2. ​print(name, age, city, job)​

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


  1. ​>>> person('Jack', 24, job='Engineer')​
  2. ​Jack 24 Beijing Engineer​

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


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

参数组合

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

比如定义一个函数,包含上述若干种参数:


  1. ​def f1(a, b, c=0, *args, **kw):​
  2. ​print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)​

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

在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。


  1. ​>>> f1(1, 2)​
  2. ​a = 1 b = 2 c = 0 args = () kw = {}​
  3. ​>>> f1(1, 2, c=3)​
  4. ​a = 1 b = 2 c = 3 args = () kw = {}​
  5. ​>>> f1(1, 2, 3, 'a', 'b')​
  6. ​a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}​
  7. ​>>> f1(1, 2, 3, 'a', 'b', x=99)​
  8. ​a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}​
  9. ​>>> f2(1, 2, d=99, ext=None)​
  10. ​a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}​

最神奇的是通过一个tuple和dict,你也可以调用上述函数:


  1. ​>>> args = (1, 2, 3, 4)​
  2. ​>>> kw = {'d': 99, 'x': '#'}​
  3. ​>>> f1(*args, **kw)​
  4. ​a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}​
  5. ​>>> args = (1, 2, 3)​
  6. ​>>> kw = {'d': 88, 'x': '#'}​
  7. ​>>> f2(*args, **kw)​
  8. ​a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}​

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

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

练习

以下函数允许计算两个数的乘积,请稍加改造,变成可接收一个或多个数并计算乘积:

# -*- coding: utf-8 -*-


# 测试
print('product(5) =', product(5))
print('product(5, 6) =', product(5, 6))
print('product(5, 6, 7) =', product(5, 6, 7))
print('product(5, 6, 7, 9) =', product(5, 6, 7, 9))
if product(5) != 5:
print('测试失败!')
elif product(5, 6) != 30:
print('测试失败!')
elif product(5, 6, 7) != 210:
print('测试失败!')
elif product(5, 6, 7, 9) != 1890:
print('测试失败!')
else:
try:
product()
print('测试失败!')
except TypeError:
print('测试成功!')


 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)​​用递归的方式写出来就是:


  1. ​def fact(n):​
  2. ​if n==1:​
  3. ​return 1​
  4. ​return n * fact(n - 1)​

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试​​fact(1000)​​:


  1. ​>>> fact(1000)​
  2. ​Traceback (most recent call last):​
  3. ​File "<stdin>", line 1, in <module>​
  4. ​File "<stdin>", line 4, in fact​
  5. ​...​
  6. ​File "<stdin>", line 4, in fact​
  7. ​RuntimeError: maximum recursion depth exceeded in comparison​

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

上面的​​fact(n)​​​函数由于​​return n * fact(n - 1)​​引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:


  1. ​def fact(n):​
  2. ​return fact_iter(n, 1)​

  3. ​def fact_iter(num, product):​
  4. ​if num == 1:​
  5. ​return product​
  6. ​return fact_iter(num - 1, num * product)​

可以看到,​​return fact_iter(num - 1, num * product)​​​仅返回递归函数本身,​​num - 1​​​和​​num * product​​在函数调用前就会被计算,不影响函数调用。

​fact(5)​​​对应的​​fact_iter(5, 1)​​的调用如下:


  1. ​===> fact_iter(5, 1)​
  2. ​===> fact_iter(4, 5)​
  3. ​===> fact_iter(3, 20)​
  4. ​===> fact_iter(2, 60)​
  5. ​===> fact_iter(1, 120)​
  6. ​===> 120​

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的​​fact(n)​​函数改成尾递归方式,也会导致栈溢出。

小结

使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。

针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。

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