学习廖雪峰python学习笔记
- 了解python
免费开源
- python应用领域
- python版本
- python3.7
- project管理
- 智能提示
- 语法高亮
- 代码跳转
- 解释代码
- 框架和库
- 新建项目:
- 新建文件书写代码:
- 文件运行:
- pycharm界面外观设置:file-settings/sefault settings
- 项目管理:
# 单行注释 print("hello word") print('hello python') # 及简单的内容 # 单行注释 """ 多行注释 多行注释 """python学习部分''' 多行注释 多行注释 '''
- 由数字、字母、下划线组成
- 不能使用数字开头
- 不能使用内置关键字
- 严格区分大小写
- 见名知义
- 大驼峰:即每个单词首字母大写,例如MyName
- 小驼峰:第二个和第二个以后的单词首字母大写,例如myName
- 下划线:my_name
# 定义变量 my_name ='tom' print(my_name) # 定义变量 存储变量 学习笔记 schoolName ='学习笔记' print(schoolName)b、认识bug
- 打断点
- debug调试
- 程序执行完,内存变量终结
- 数值
folat(浮点型)
- 布尔型
- str
- list
- typle(元组)
- set(集合)
- dict(字典)
- 空值
空值是Python里一个特殊的值,用None表示。None不能理解为0,因为0是有意义的,而None是一个特殊的空值。
num1 = 1 num2 = 1.1 a='hello word' b=True # list列表类型 c=[10,10,19] # tuple----元组类型 d = (10,3,5) # set----集合 e = {13,34,56} # dict---字典---键值对 f = {'name':'tom','age':18} #type(num1) print(type(num1)) print(type(num2)) print(type(a)) print(type(b)) print(type(c)) print(type(d)) print(type(e)) print(type(f)) print(f)1.3
a = 'ABC'
Python解释器干了两件事情:
-
在内存中创建了一个'ABC'的字符串;
- 在内存中创建了一个名为a的变量,并把它指向'ABC'。
所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。在Python中,通常用全部大写的变量名表示常量:
python有两种除法# -*- coding: utf-8 -*-
2、字符串和编码我们已经讲过了,字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。
因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),如果要表示更大的整数,就必须用更多的字节。比如两个字节可以表示的最大整数是65535,4个字节可以表示的最大整数是4294967295。
由于计算机是美国人发明的,因此,最早只有127个字符被编码到计算机里,也就是大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码,比如大写字母A的编码是65,小写字母z的编码是122。
但是要处理中文显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,所以,中国制定了GB2312编码,用来把中文编进去。
你可以想得到的是,全世界有上百种语言,日本把日文编到Shift_JIS里,韩国把韩文编到Euc-kr里,各国有各国的标准,就会不可避免地出现冲突,结果就是,在多语言混合的文本中,显示出来会有乱码。
因此,Unicode应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。
Unicode标准也在不断发展,但最常用的是用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。现代操作系统和大多数编程语言都直接支持Unicode。
现在,捋一捋ASCII编码和Unicode编码的区别:ASCII编码是1个字节,而Unicode编码通常是2个字节。
字母A用ASCII编码是十进制的65,二进制的01000001;
字符0用ASCII编码是十进制的48,二进制的00110000,注意字符'0'和整数0是不同的;
汉字中已经超出了ASCII编码的范围,用Unicode编码是十进制的20013,二进制的01001110 00101101。
你可以猜测,如果把ASCII编码的A用Unicode编码,只需要在前面补0就可以,因此,A的Unicode编码是00000000 01000001。
新的问题又出现了:如果统一成Unicode编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。
所以,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”的UTF-8编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间:
字符 | ASCII | Unicode | UTF-8 |
---|---|---|---|
A | 01000001 | 00000000 01000001 | 01000001 |
中 | x | 01001110 00101101 | 11100100 10111000 10101101 |
从上面的表格还可以发现,UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。
搞清楚了ASCII、Unicode和UTF-8的关系,我们就可以总结一下现在计算机系统通用的字符编码工作方式:
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:
浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:
- 对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:
chr()编码转字符
- 在网络传输过程中要把str转换成以字节为单位的bytes
- Python对bytes类型的数据用带b前缀的单引号或双引号表示:
- X = b"ABC"
- 要注意区分'ABC'和b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。
- 以Unicode表示的str通过encode()方法可以编码为指定的bytes
- 纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes。含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。
- 在bytes中,无法显示为ASCII字符的字节,用\x##显示。
- 反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:
- 如果bytes中包含无法解码的字节,decode()方***报错:
- 如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节:
print(ord("Q")) print(chr(25991)) print('\u4e2d\u6587') x = '中文' x.encode('utf-8') print(x)y = b'ABC' y.decode("ascii") print(y)y = b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8') print(y)y = b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore') print(y)
- 要计算str包含多少个字符,可以用len()函数:
- len()函数计算的是str的字符数,如果换成bytes,len()函数就计算字节数:
-
n1=len('ABC') n2=len(b'ABC') n3=len("中文") n4=len('中文'.encode('utf-8')) print(n1,n2,n3,n4)
- 可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
在操作字符串时,我们经常遇到str和bytes的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8编码对str和bytes进行转换。
#!/usr/bin/env python3 # -*- coding: utf-8 -*-
- 格式化
%运算符就是用来格式化字符串的。在字符串内部,%s表示用字符串替换,%d表示用整数替换,有几个%?占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?,括号可以省略。
常见的占位符有:
占位符 | 替换内容 |
---|---|
%d | 整数 |
%f | 浮点数 |
%s | 字符串 |
%x | 十六进制整数 |
其中,格式化整数和浮点数还可以指定是否补0和整数与小数的位数:
- 如果你不太确定应该用什么,%s永远起作用,它会把任何数据类型转换为字符串:
- 有些时候,字符串里面的%是一个普通字符怎么办?这个时候就需要转义,用%%来表示一个%:
- 另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}……,不过这种方式写起来比%要麻烦得多:
>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125) 'Hello, 小明, 成绩提升了 17.1%'
list
Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。
- 当索引超出了范围时,Python会报一个IndexError错误,所以,要确保索引不要越界,记得最后一个元素的索引是len(classmates) - 1。如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素:
classmates[-1]
- list是一个可变的有序表,所以,可以往list中追加元素到末尾:
classmates.append('Adam')
- 也可以把元素插入到指定的位置,比如索引号为1的位置:
classmates.insert(1, 'Jack')
- 要删除list末尾的元素,用pop()方法:
classmates.pop()
- 要删除指定位置的元素,用pop(i)方法,其中i是索引位置:
- 要把某个元素替换成别的元素,可以直接赋值给对应的索引位置:
classmates[1] = 'Sarah'
- list里面的元素的数据类型也可以不同,比如:
classmate=['jf','hf','hjfi',True,12313]
- list元素也可以是另一个list,比如:
p = ['asp', 'php'] s = ['python', 'java', p, 'scheme']
- 要拿到'php'可以写p[1]或者s[2][1],因此s可以看成是一个二维数组,类似的还有三维、四维……数组,不过很少用到。
classmate=['jf','hf','hjfi',True,12313] print(classmate) print(classmate[-1]) classmate.insert(1,'我是') print(classmate) classmate.pop(2) print(classmate) classmate[1]="alex" print(classmate)tuple
因为tuple不可变,所以代码更安全。如果可能,能用tuple代替list就尽量用tuple。
tuple的陷阱:当你定义一个tuple时,在定义的时候,tuple的元素就必须被确定下来,比如:
>>> t = (1, 2) >>> t (1, 2)
定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。
所以,只有1个元素的tuple定义时必须加一个逗号,,来消除歧义:
>>> t = (1,) >>> t (1,)可变tuple,所指向的对象也不变
x=('a','b',['A','B']) x[2][0]='X' x[2][1]='Y' print(x)4、条件判断和循环
if <条件判断1>: <执行1> elif <条件判断2>: <执行2> elif <条件判断3>: <执行3> else: <执行4>5、循环
Python提供一个range()函数# -*- coding: utf-8 -*-
namep = [34,1,23,454]
for x in namep:
print(x)
sums=0 for x in range(101): sums=sums+x print(sums)break提前结束循环
sum = 0 n = 99 while n > 0: sum = sum + n n = n - 2 print(sum)
G = {'王':34,'流':34} print(G['王'])
G['w']= 99 print(G['w'])
要避免key不存在的错误,有两种办法,一是通过in判断key是否存在:
>>> 'Thomas' in d False
二是通过dict提供的get()方法,如果key不存在,可以返回None,或者自己指定的value:
>>> d.get('Thomas') >>> d.get('Thomas', -1) -1
注意:返回None的时候Python的交互环境不显示结果。
d['Jack'] = 90 G['w']= 99 print(G['w']) ret='ree' in G print(ret) z=d.get("zzz") z=d.get('zzz',90) print(z)
和list比较,dict有以下几个特点:
- 查找和插入的速度极快,不会随着key的增加而变慢;
- 需要占用大量的内存,内存浪费多。
而list相反:
- 查找和插入的时间随着元素的增加而增加;
- 占用空间小,浪费内存很少。
dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象。
这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。
要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:
setset和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。
s = set([1, 1, 2, 2, 3, 3])
>>> s1 = set([1, 2, 3]) >>> s2 = set([2, 3, 4]) >>> s1 & s2 {2, 3} >>> s1 | s2 {1, 2, 3, 4}sort()排序
数据类型转换
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函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
# -*- coding: utf-8 -*- n1 = 255 n2 = 1000 print(hex(n1)) print(hex(n2))
def myChange(x): if x>0: return x else: return -x print(myChange(-990))
请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。
空函数
如果想定义一个什么事也不做的空函数,可以用pass语句:
def nop(): pass
pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。
pass还可以用在其他语句里,比如:
if age >= 18: pass
缺少了pass,代码运行就会有语法错误。
数据类型检查可以用内置函数isinstance()实现:def my_abs(x): if not isinstance(x, (int, float)): raise TypeError('bad operand type') if x >= 0: return x else: return -x print(my_abs('E'))import math语句表示导入math包,并允许后续代码引用math包里的sin、cos等函数。
def quadratic(a, b, c): d=b*b-4*a*c if d>0: x1=(-b+math.sqrt(b*b-4*a*c))/(2*a) x2=(-b-math.sqrt(b*b-4*a*c))/(2*a) return x1, x2 elif d==0: x1 = (-b + math.sqrt(b * b - 4 * a * c)) / (2 * a) return x1 else: return print(quadratic(34,2,3))
小结
- 定义函数时,需要确定函数名和参数个数;
- 如果有必要,可以先对参数的数据类型做检查;
- 函数体内部可以用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)
但是调用者仍可以传入不受限制的关键字参数:
>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收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)的形式调用它,无论它的参数是如何定义的。
练习
以下函数允许计算两个数的乘积,请稍加改造,变成可接收一个或多个数并计算乘积:
# -*- coding: utf-8 -*-
- 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的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
- 命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
- 定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000):
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,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
可以通过切片轻松取出某一段数列。比如前10个数:
>>> L[:10] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
后10个数:
>>> L[-10:] [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
前11-20个数:
>>> L[10:20] [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
前10个数,每两个取一个:
>>> L[:10:2] [0, 2, 4, 6, 8]
所有数,每5个取一个:
>>> L[::5] [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
甚至什么都不写,只写[:]就可以原样复制一个list:
>>> L[:] [0, 1, 2, 3, ..., 99]
tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:
>>> (0, 1, 2, 3, 4, 5)[:3] (0, 1, 2)
字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
>>> 'ABCDEFG'[:3] 'ABC' >>> 'ABCDEFG'[::2] 'ACEG'
strs[1:]#倒着取 练习:利用切片操作,实现一个trim()函数,去除字符串首尾的空格,注意不要调用str的strip()方法:
如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)。
在Python中,迭代是通过for ... in来完成的,而很多语言比如C语言,迭代list是通过下标完成的,比如Java代码:
for (i=0; i<list.length; i++) { n = list[i];
}
可以看出,Python的for循环抽象程度要高于C的for循环,因为Python的for循环不仅可以用在list或tuple上,还可以作用在其他可迭代对象上。
list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:
>>> d = {'a': 1, 'b': 2, 'c': 3} >>> for key in d: ... print(key) ... a c b
因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。
默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。
由于字符串也是可迭代对象,因此,也可以作用于for循环:
>>> for ch in 'ABC': ... print(ch) ... A B C
所以,当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型。
那么,如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:
>>> from collections import Iterable >>> isinstance('abc', Iterable) # str是否可迭代 True >>> isinstance([1,2,3], Iterable) # list是否可迭代 True >>> isinstance(123, Iterable) # 整数是否可迭代 False
最后一个小问题,如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:
>>> for i, value in enumerate(['A', 'B', 'C']): ... print(i, value) ... 0 A 1 B 2 C
上面的for循环里,同时引用了两个变量,在Python里是很常见的,比如下面的代码:
>>> for x, y in [(1, 1), (2, 4), (3, 9)]: ... print(x, y) ... 1 1 2 4 3 9
练习
请使用迭代查找一个list中最小和最大值,并返回一个tuple:
# -*- coding: utf-8 -*-
列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。
举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11)):
>>> list(range(1, 11)) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环:
>>> L = [] >>> for x in range(1, 11): ... L.append(x * x) ... >>> L [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:
>>> [x * x for x in range(1, 11)] [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来,十分有用,多写几次,很快就可以熟悉这种语法。
for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:
>>> [x * x for x in range(1, 11) if x % 2 == 0] [4, 16, 36, 64, 100]
还可以使用两层循环,可以生成全排列:
>>> [m + n for m in 'ABC' for n in 'XYZ'] ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
三层和三层以上的循环就很少用到了。
运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:
>>> import os # 导入os模块,模块的概念后面讲到 >>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录 ['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
for循环其实可以同时使用两个甚至多个变量,比如dict的items()可以同时迭代key和value:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' } >>> for k, v in d.items(): ... print(k, '=', v) ... y = B x = A z = C
因此,列表生成式也可以使用两个变量来生成list:
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' } >>> [k + '=' + v for k, v in d.items()] ['y=B', 'x=A', 'z=C']
最后把一个list中所有的字符串变成小写:
>>> L = ['Hello', 'World', 'IBM', 'Apple'] >>> [s.lower() for s in L] ['hello', 'world', 'ibm', 'apple']
if ... else
使用列表生成式的时候,有些童鞋经常搞不清楚if...else的用法。
例如,以下代码正常输出偶数:
>>> [x for x in range(1, 11) if x % 2 == 0] [2, 4, 6, 8, 10]
但是,我们不能在最后的if加上else:
>>> [x for x in range(1, 11) if x % 2 == 0 else 0] File "<stdin>", line 1 [x for x in range(1, 11) if x % 2 == 0 else 0] ^ SyntaxError: invalid syntax
这是因为跟在for后面的if是一个筛选条件,不能带else,否则如何筛选?
另一些童鞋发现把if写在for前面必须加else,否则报错:
>>> [x if x % 2 == 0 for x in range(1, 11)] File "<stdin>", line 1 [x if x % 2 == 0 for x in range(1, 11)] ^ SyntaxError: invalid syntax
这是因为for前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据x计算出结果,因为缺少else,必须加上else:
>>> [x if x % 2 == 0 else -x for x in range(1, 11)] [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
上述for前面的表达式x if x % 2 == 0 else -x才能根据x计算出确定的结果。
可见,在一个列表生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else。
练习
如果list中既包含字符串,又包含整数,由于非字符串类型没有lower()方法,所以列表生成式会报错:
>>> L = ['Hello', 'World', 18, 'Apple', None] >>> [s.lower() for s in L] Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <listcomp> AttributeError: 'int' object has no attribute 'lower'
使用内建的isinstance函数可以判断一个变量是不是字符串:
>>> x = 'abc' >>> y = 123 >>> isinstance(x, str) True >>> isinstance(y, str) False
请修改列表生成式,通过添加if语句保证列表生成式能正确地执行:
# -*- coding: utf-8 -*-