python基础语法总结

简介: python基础语法总结

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
print(sum)

#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
o
m
#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'>


image.png


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

a + b 输出结果: HelloPython

image.png

#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] = 'X'
print(t3)

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

95
#5.数据类型---set(集合)
#set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
#set是无序的,重复元素在set中自动被过滤。
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可以处理任意大小的整数,当然包括负整数,在程序中的表示方法和数学上的写法一模一样,例如:1100-80800,等等。


计算机由于使用二进制,所以,有时候用十六进制表示整数比较方便,十六进制用0x前缀和0-9,a-f表示,例如:0xff000xa5b4c3d2,等等。


浮点数


浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,比如,1.23x109和12.3x108是完全相等的。浮点数可以用数学写法,如1.233.14-9.01,等等。但是对于很大或很小的浮点数,就必须用科学计数法表示,把10用e替代,1.23x109就是1.23e9,或者12.3e8,0.000012可以写成1.2e-5,等等。


整数和浮点数在计算机内部存储的方式是不同的,整数运算永远是精确的(除法难道也是精确的?是的!),而浮点数运算则可能会有四舍五入的误差。


字符串


字符串是以单引号'或双引号"括起来的任意文本,比如'abc'"xyz"等等。请注意,''""本身只是一种表示方式,不是字符串的一部分,因此,字符串'abc'只有abc这3个字符。如果'本身也是一个字符,那就可以用""括起来,比如"I'm OK"包含的字符是I'm,空格,OK这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使用


布尔值


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


  1. >>> True
  2. True


not运算是非运算,它是一个单目运算符,把True变成FalseFalse变成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的字符数,如果换成byteslen()函数就计算字节数:


  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')


elifelse 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()返回的数据类型是strstr不能直接和整数比较,必须先把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语句,函数执行完毕后也会返回结果,只是结果为Nonereturn 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包里的sincos等函数。


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

  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)函数有两个参数:xn,这两个参数都是位置参数,调用函数时,传入的两个值按照位置顺序依次赋给参数xn


默认参数


新的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的解释器会报错(思考一下为什么默认参数不能放在必选参数前面);


二是如何设置默认参数。

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


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

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

  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),意思是,除了namegender这两个参数外,最后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']


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


可变参数


在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除了必选参数nameage外,还接受关键字参数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函数里,我们保证能接收到nameage这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。


和可变参数类似,也可以先组装出一个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()函数为例,我们希望检查是否有cityjob参数:

  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)

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

  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


由于调用时缺少参数名cityjob,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 - 1num * 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标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。


目录
相关文章
|
10天前
|
Java 编译器 C语言
Python速成篇(基础语法)上
Python速成篇(基础语法)上
|
1月前
|
Java 程序员 C++
【python】—— 基础语法(二)
【python】—— 基础语法(二)
|
1月前
|
存储 机器学习/深度学习 XML
python基础语法——文件与库
本文基于pycharm编译器,也可以使用Anaconda 里的编译器,将讲解一些python的一些基础语法知识,是对上篇文章的补充.
31 0
|
28天前
|
编译器 测试技术 C++
【Python 基础教程 01 全面介绍】 Python编程基础全攻略:一文掌握Python语法精髓,从C/C++ 角度学习Python的差异
【Python 基础教程 01 全面介绍】 Python编程基础全攻略:一文掌握Python语法精髓,从C/C++ 角度学习Python的差异
157 0
|
10天前
|
存储 监控 Java
Python速成篇(基础语法)下(新年快乐♥)
Python速成篇(基础语法)下(新年快乐♥)
|
16天前
|
Java C语言 C++
【Python】5. 基础语法(3) -- 函数篇
【Python】5. 基础语法(3) -- 函数篇
24 1
|
16天前
|
存储 Java 程序员
【Python】6. 基础语法(4) -- 列表+元组+字典篇
【Python】6. 基础语法(4) -- 列表+元组+字典篇
39 1
|
29天前
|
存储 Python
Python基础语法
Python基础语法
|
1月前
|
数据处理 开发者 Python
|
Java 程序员 C++
【python】—— 基础语法(一)
【python】—— 基础语法(一)

热门文章

最新文章