Python为我们提供了非常完善的基础代码库,覆盖了网络、文件、GUI、数据库、文本等大量内容,被形象地称作“内置电池”。用Python开发,许多功能不必从零编写,直接使用现成的即可。除了内置的库外,Python还有大量的第三方库,也就是别人开发的,供你直接使用的东西。当然,如果你开发的代码通过很好的封装,也可以作为第三方库给别人使用。
首选是网络应用,包括网站、后台服务等等;
其次是许多日常需要的小工具,包括系统管理员需要的脚本任务等等;
另外就是把其他语言开发的程序再包装起来,方便使用。
和C程序相比非常慢,因为Python是解释型语言,你的代码在执行时会一行一行地翻译成CPU能理解的机器码,这个翻译过程非常耗时,所以很慢。而C程序是运行前直接编译成CPU能执行的机器码,所以非常快。
因为是解释性语言,实际上就是发布源代码,这一点跟C语言不同,C语言不用发布源代码,只需要把编译后的机器码(也就是在Windows上常见的xxx.exe文件)发布出去。要从机器码反推出C代码几乎是不可能的。不过,这个缺点仅限于你要编写的软件需要卖给别人挣钱的时候。好消息是目前的互联网时代,靠卖软件授权的商业模式越来越少了,靠网站和移动应用卖服务的模式越来越多了,后一种模式不需要把源码给别人。
.....
当我们编写Python代码时,我们得到的是一个包含Python代码的以
.py
为扩展名的文本文件。要运行代码,就需要Python解释器去执行.py
文件。由于整个Python语言从规范到解释器都是开源的,所以理论上,只要水平够高,任何人都可以编写Python解释器来执行Python代码(当然难度很大)。存在多种Python解释器,如下:
CPython
当我们从Python官方网站下载并安装好Python 3.x后,我们就直接获得了一个官方版本的解释器:CPython。这个解释器是用C语言开发的,所以叫CPython。在命令行下运行python就是启动CPython解释器。
IPython
IPython是基于CPython之上的一个交互式解释器,也就是说,IPython只是在交互方式上有所增强,但是执行Python代码的功能和CPython是完全一样的。好比很多国产浏览器虽然外观不同,但内核其实都是调用了IE。CPython用>>>
作为提示符,而IPython用In [序号]:
作为提示符。
PyPy
PyPy是另一个Python解释器,它的目标是执行速度。PyPy采用JIT技术,对Python代码进行动态编译(注意不是解释),所以可以显著提高Python代码的执行速度。绝大部分Python代码都可以在PyPy下运行,但是PyPy和CPython有一些是不同的,这就导致相同的Python代码在两种解释器下执行可能会有不同的结果。如果你的代码要放到PyPy下执行,就需要了解PyPy和CPython的不同点。
Jython
Jython是运行在Java平台上的Python解释器,可以直接把Python代码编译成Java字节码执行。
IronPython
IronPython和Jython类似,只不过IronPython是运行在微软.Net平台上的Python解释器,可以直接把Python代码编译成.Net的字节码。
小结:
Python的解释器很多,但使用最广泛的还是CPython。如果要和Java或.Net平台交互,最好的办法不是用Jython或IronPython,而是通过网络调用来交互,确保各程序之间的独立性。
在命令行模式下,可以执行
python
进入Python交互式环境,也可以执行python hello.py
运行一个.py文件。执行一个.py
文件只能在命令行模式执行。此外,在命令行模式运行.py
文件和在Python交互式环境下直接运行Python代码有所不同。Python交互式环境会把每一行Python代码的结果自动打印出来,但是,直接运行Python代码却不会。Python交互模式的代码是输入一行,执行一行,而命令行模式下直接运行.py
文件是一次性执行该文件内的所有代码。可见,Python交互模式主要是为了调试Python代码用
的,也便于初学者学习,它不是正式运行Python代码的环境!
用
print()
在括号中加上字符串,就可以向屏幕上输出指定的文字。也可以接受多个字符串,用逗号隔开,依次打印每个字符串,遇到逗号会输出一个空格;也可以打印整数,或者直接计算结果,如print(100 + 200)
可以让用户输入字符串,并存放到一个变量里
python中并没有像c那样用
{}
来包含一个函数体等,而是采用缩进的方式,当语句以冒号结尾时,缩进的语句视为代码块
。但没有规定缩进是几个空格还是Tab。按照约定俗成的惯例,应该始终坚持使用4个空格的缩进,否则在IDE编写代码时,会报错。缩进的坏处就是“复制-粘贴”功能失效了,这是最坑爹的地方。当你重构代码时,粘贴过去的代码必须重新检查缩进是否正确。此外,IDE很难像格式化Java代码那样格式化Python代码。另外,如果在文本编辑器中编写python,最好设置把Tab自动转换为4个空格,确保不混用Tab和空格。
务必注意,Python程序是大小写敏感的,如果写错了大小写,程序会报错。
在c里已经描述的很详细了,这里主要讲述些在python中的独特用法。Python支持多种数据类型,在计算机内部,可以把任何数据都看成一个“对象”,而变量就是在程序中用来指向这些数据对象的,对变量赋值就是把数据和变量给关联起来。
Python可以处理
任意大小的整数
,还包括负整数;对于很大的数,例如10000000000
,很难数清楚0的个数。Python允许在数字中间以_
分隔,因此,还可以写成10_000_000_000
。十六进制数也可以写成0xa1b2_c3d4
。
没啥特别的用法,不过注意Python的浮点数也没有大小限制,但是超出一定范围就直接表示为inf
(无限大)。
python也支持转义功能,特别的,Python还允许用
r''
表示''
内部的字符串默认不转义。如果字符串内部有很多换行,用\n
写在一行里不好阅读,为了简化,Python允许用'''...'''
的格式表示多行内容。
>>> print('''line1
... line2
... line3''')
line1
line2
line3
上面是在交互式命令行内输入,注意在输入多行内容时,提示符由>>>
变为...
,提示你可以接着上一行输入,注意...
是提示符,不是代码的一部分。
因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理,但是要处理中文显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,所以,中国制定了GB2312编码,用来把中文编进去。同理其他语言也采用同样的方式,结果就是,在多语言混合的文本中,显示出来会有乱码。因此出现了Unicode标准,把所有语言都统一到一套编码里,这样就不会再有乱码问题。Unicode标准最常用的是UCS-16编码,用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。
然而,如果写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算,这又造成新的问题,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”
的UTF-8编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母
被编码成1
个字节,汉字
通常是3
个字节,只有很生僻的字符
才会被编码成4-6
个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间
现在计算机系统通用的字符编码工作方式:
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
在最新的Python 3版本中,字符串是以Unicode编码的。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
第一行注释是为了告诉Linux/OS X系统,这是一个Python可执行程序,Windows系统会忽略这个注释;
第二行注释是为了告诉Python解释器,按照UTF-8编码读取源代码。
注意:声明了UTF-8编码并不意味着我们的
.py
文件就是UTF-8编码的,必须并且要确保文本编辑器正在使用UTF-8 without BOM编码
ord()
函数获取字符的整数表示,chr()
函数把编码转换为对应的字符,如:>>> ord('中')
20013
>>> chr(66)
'B'
>>> '\u4e2d\u6587'
'中文'
字节
为单位的bytes
,Python对bytes类型的数据用带b前缀的单引号或双引号表示:x = b'ABC'
,虽然内容显示得和前者一样,但bytes的每个字符都只占用1个字节
。encode()
方法可以编码为指定的bytes,例如:>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
注意:含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。
decode()
方法,如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节,例如:>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'
>>> len(b'ABC')
3
>>> len('中文'.encode('utf-8'))
6
可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。
方法一:和C语言是一致的(包括控制符的高级用法),用%
实现,例如:
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'
如果只有一个%
,括号可以省略。
方法二:format()
方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}……
,不过这种方式写起来比%
要麻烦得多,例如:
>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.1%'
方法三:使用以f开头的字符串,称之为f-string
,它和普通字符串不同之处在于,字符串如果包含{xxx}
,就会以对应的变量替换,例如:
>>> r = 2.5
>>> s = 3.14 * r ** 2
>>> print(f'The area of a circle with radius {r} is {s:.2f}')
The area of a circle with radius 2.5 is 19.62
在Python中,可以直接用True、False表示布尔值(请注意大小写),也可以通过布尔运算计算出来,布尔值可以用
and
、or
和not
运算。
空值是Python里一个特殊的值,用
None
表示。None
不能理解为0,因为0是有意义的,而None
是一个特殊的空值。
list是一种有序的集合,可以随时添加和删除其中的元素(即
可变
)。list里面的元素的数据类型可以不同,元素也可以是另一个list。
定义列表的格式:
列表名 = [元素1, 元素2, 元素3...]
len()
函数获得list元素的个数,注意如果list中含另外一个list或其他含多元素的类型,计算lengh时都看作一个整体,即记作1个元素;另外,如果一个list中一个元素也没有,就是一个空的list,它的长度为0当索引超出了范围时,Python会报一个
IndexError
错误,所以,要确保索引不要越界,记得最后一个元素的索引是len(列表名) - 1
append()
追加元素到末尾:>>> classmates.append('Adam')
>>> classmates
['Michael', 'Bob', 'Tracy', 'Adam']
insert()
把元素插入到指定的位置,比如索引号为1的位置:>>> classmates.insert(1, 'Jack')
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']
pop()
删除list末尾的元素,如果要删除指定位置,括号内指定索引即可:>>> classmates[1] = 'Sarah'
>>> classmates
['Michael', 'Sarah', 'Tracy']
>>> p = ['asp', 'php']
>>> s = ['python', 'java', p, 'scheme']
>>> p[1]
‘php’
>>> s[2][1]
’php‘
tuple和list非常类似,但是tuple一旦初始化就不能修改(即
不可变
,代码更安全,所以如果能用tuple代替list就尽量用tuple)。
定义元组的格式:
元组名 = (元素1, 元素2, 元素3...)
注意:如果只定义1个元素时,如
t = (1)
,这会造成歧义!这是因为()
既可以表示tuple,又可以表示数学公式中的小括号,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。所以为了避免歧义必须再加一个逗号,即:t = (1,)
,这样才会被当作元组。
注意tuple没有append(),insert()这些方法,其他获取元素的方法和list是一样的,即通过索引访问。
不可变
的理解补充tuple所谓的“不变”是说,tuple的每个元素,指向
永远不变,比如:
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])
这个表面上看起来使tuple可变
了,但其实不然,实际上改的只是list中的元素,list是可变
的,但从tuple角度来看,这个位置的元素依然是2向一个lis2,key3:value3,所以是不可变
。
全称dictionary,在其他语言中也称为
map
,使用键-值
(key-value)存储,具有极快的查找速度。
定义字典的格式:
字典名 = [key1:value1, key2:value2, key3:value3]
把数据放入dict的方法,除了初始化时指定外,还可以通过key放入,如:
>>> d['Adam'] = 67
>>> d['Adam']
67
注意:由于一个key只能对应一个value,所以,如果多次对同一个key放入value,后面的值会把前面的值冲掉。
in
:>>> 'Thomas' in d
False
通过dict提供的get()
方法:
>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1
如果key不存在,可以返回None,或者自己指定的value。
pop(key)
方法,对应的value也会从dict中删除,如:>>> d.pop('Bob')
75
>>> d
{'Michael': 95, 'Tracy': 85}
注意正确使用dict非常重要,需要牢记的就是dict的
key
必须是不可变对象
。这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。这个通过key计算位置的算法称为哈希算法(Hash)。
要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key:
和list比较,dict有以下几个特点:
空间来换取时间
的一种方法。和dict类似,也是一组key的集合,但不存储value。在set中,不能有重复的key。
要创建一个set,需要提供一个list作为输入集合,如:
>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}
注意set中的list只是表示说有这几个元素,而顺序实际上是无序的,另外从上面可看到重复元素被自动过滤了。
add(key)
方法可以添加元素到set中,可以重复添加,但不会有效果;remove(key)
方法可以删除元素;>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}
比如,str是不变对象,而list是可变对象,对于可变对象,直接操作则内部的内容是会变化的,而对于不可变对象,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。例子如下:
>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'
当我们调用a.replace('a', 'A')
时,实际上调用方法replace是作用在字符串对象'abc'
上的,而这个方法虽然名字叫replace,但却没有改变字符串'abc'的内容。相反,replace方法创建了一个新字符串'Abc'并返回,如果我们用变量b指向该新字符串,就容易理解了,变量a仍指向原有的字符串'abc',但变量b却指向新字符串'Abc'
了。
变量的命名规则和c一样。与c、java等静态语言(在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错)不同,python是
动态语言
(变量本身类型不固定的语言),和静态语言相比,动态语言更灵活。
例:
a = 'ABC'
此时,Python解释器干了两件事情:
对变量赋值a = b是把变量a指向真正的对象,该对象是变量b所指向的。随后对变量b的赋值不影响变量a的指向。
所谓常量就是不能变的变量,在Python中,通常用
全部大写
的变量名表示常量。
在Python中,有两种除法:
/
//
%
做取余运算,可以得到两个整数相除的余数完整形式如下:
if <条件判断1>:
<执行1>
elif <条件判断2>:
<执行2>
elif <条件判断3>:
<执行3>
else:
<执行4>
从上往下
判断,如果在某个判断上是True,把该判断对应的语句执行后,就忽略掉剩下的elif和else。if x:
print('True')
只要x是非零数值、非空字符串、非空list等,就判断为True,否则为False。
birth = input('birth: ')
if birth < 2000:
print('00前')
else:
print('00后')
输入1982
,结果报错TypeError: unorderable types: str() > int()
,这是因为input()
返回的数据类型是str,str不能直接和整数比较,必须先把str转换成整数,可以用函数int()
来实现,即把获取输入的代码部分改成:
s = input('birth: ')
birth = int(s)
但是注意如果输入abc
,即使用int()
也会报错,因为abc
转换成int后并不是一个合法的数字。
当我们用if判断时,会写很长一串代码,可读性较差。如果要针对某个变量匹配若干种情况,使用
match
语句更好,类似于c语言中的switch
格式如下:
match <匹配变量名>:
case <匹配值1>:
<执行1>
case <匹配值2>:
<执行2>
case <匹配值3>:
<执行3>
case _: # _表示匹配到其他任何情况
<执行4>
age = 15
match age:
case x if x < 10:
print(f'< 10 years old: {x}')
case 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18:
print('11~18 years old.')
case _:
print('not sure.')
第一个表示当age < 10
成立时匹配,且赋值给变量x;第三个匹配多个值。
args = ['gcc', 'hello.c']
存储# -*- coding: utf-8 -*-
args = ['gcc', 'hello.c', 'world.c']
match args:
# 如果仅出现gcc,报错:
case ['gcc']:
print('gcc: missing source file(s).')
# 出现gcc,且至少指定了一个文件:
case ['gcc', file1, *files]:
print('gcc compile: ' + file1 + ', ' + ', '.join(files))
# 仅出现clean:
case ['clean']:
print('clean')
case _:
print('invalid command.')
第一个case ['gcc']
表示列表仅有'gcc'一个字符串,没有指定文件名,报错;
第二个case ['gcc', file1, *files]
表示列表第一个字符串是'gcc',第二个字符串绑定到变量file1
,后面的任意个字符串绑定到*files
;
第三个case ['clean']
表示列表仅有'clean'一个字符串;
最后一个case _
表示其他所有情况。
格式:
names = ... // 定义元组或列表等
for x in names:
[语句]
就是把每个在names里的元素代入变量x进行遍历,然后执行缩进块的语句
格式:
while [条件]:
[语句]
只要条件满足,就不断循环,条件不满足时退出循环
另外,和c语言一样的,同样有break和continue用于跳过循环,且原理相同,不再赘述
基本上所有的高级语言都支持函数,Python也不例外。Python不但能非常灵活地定义函数,而且本身内置了很多有用的函数,可以直接调用。
要调用python内置的函数,需要知道函数的名称和参数,可以直接从Python的官方网站查看文档,也可以在交互式命令行通过help(函数名)查看函数的帮助信息。
TypeError
的错误;TypeError
的错误;别名
,如:>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1
格式:
def 函数名(参数):
[语句]
return 返回值
请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回,如果没有return语句,函数执行完毕后也会返回结果,只是结果为
None
。return None
可以简写为return
。另外,在Python交互环境中定义函数时,注意Python会出现...
的提示。函数定义结束后需要按两次回车
重新回到>>>
提示符下。
def nop():
pass
pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。注意pass也可以用在其他语句中,不仅仅是函数中。
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
对参数类型做检查,只允许整数和浮点数类型的参数。
tuple
,例如:import math
def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
然后,我们就可以同时获得返回值:
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0
但其实这只是一种假象,Python函数返回的仍然是单一值:
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)
Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数等,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
例如:
写一个函数计算任意数的n次方:
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
调用:
>>> power(5, 2)
25
>>> power(5, 3)
125
这里的power(x, n)
函数有两个参数:x和n,这两个参数都是位置参数
,调用函数时,传入的两个值按照位置顺序依次赋给参数x和n
默认参数可以降低函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。
举个例子,我们写个一年级小学生注册的函数,需要传入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('Adam', 'M', city='Tianjin')
注意:
enroll('Bob', 'M', 7)
,意思是,除了name,gender这两个参数外,最后1个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。enroll('Adam', 'M', city='Tianjin')
,意思是,city参数用传进去的值,其他默认参数继续使用默认值。定义默认参数要牢记一点:默认参数必须指向
不变对象
(如None)!(因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有)
顾名思义,可变参数就是传入的参数个数是可变的。可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。
定义时如下:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
仅仅是在参数前加了*
,其中numbers是指代一个参数个数不确定的list或tuple等。
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
此时*nums
表示把nums这个list的所有元素作为可变参数传进去。
关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。关键字参数可以扩展函数的功能(比如正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。)
定义时如下:
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数,也可以传入任意个数的关键字参数:
>>> 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'}
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
>>> 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。
如果要限制关键字参数的名字,就可以用命名关键字参数。
例如,只接收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() missing 2 required keyword-only arguments: 'city' and 'job'
由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args
,但缺少命名关键字参数导致报错。
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
由于命名关键字参数city具有默认值,调用时,可不传入city参数。
要特别注意,如果没有可变参数,就必须加一个
*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数!
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
如果一个函数在内部调用自身本身,这个函数就是递归函数。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
尾递归
优化,例子如下:def fact(n):
if n==1:
return 1
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语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。
在Python中,代码不是越多越好,而是越少越好。代码不是越复杂越好,而是越简单越好。高级特性的出现,可以简化大部分复杂代码。
比如取前N个元素,也就是索引为0-(N-1)的元素,可以用循环:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> r = []
>>> n = 3
>>> for i in range(n):
... r.append(L[i])
...
>>> r
['Michael', 'Sarah', 'Tracy']
对这种经常取指定索引范围的操作,用循环十分繁琐,因此,Python提供了切片(Slice)操作符,能大大简化这种操作:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
L[0:3]
表示,从索引0开始取,直到索引3为止,但不包括索引3。
L[:3]
;>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[-2:-1]
['Bob']
记住倒数第一个元素的索引是
-1
!!!
>>> L = list(range(100))
>>> L
[0, 1, 2, 3, ..., 99]
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
[:]
就可以原样复制一个list;>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为
迭代
。Python中,迭代是通过for ... in来完成的,而很多语言比如C语言,迭代list是通过下标完成的。
>>> 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()
collections.abc
模块的Iterable
类型判断,例如:>>> from collections.abc import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False
enumerate
函数可以把一个list变成索引-元素
对,这样就可以在for循环中同时迭代索引和元素本身,例如:>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C
列表生成式即
List Comprehensions
,是Python内置的非常简单却强大的可以用来创建list的生成式。
list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
可以用list(range(1, 11))
[1x1, 2x2, 3x3, ..., 10x10]
可以用[x * x for x in range(1, 11)]
x * x
放到前面,后面跟for循环,就可以把list创建出来。[x * x for x in range(1, 11) if x % 2 == 0]
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']
>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']
>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
if..else
时的问题:在一个列表生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else。
在Python中,这种
一边循环一边计算
的机制,称为生成器:generator。要创建一个generator,有很多种方法。
[]
改成()
,例如:>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>
next()
,就计算出下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误,但这样很繁琐,更正确的方法是使用for循环,因为generator也是可迭代对象,例如:>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1
4
9
16
25
36
49
64
81
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
),斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
注意,赋值语句:
a, b = b, a + b
相当于:
t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]
但不必显式写出临时变量t就可以赋值。
fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator,只需稍加修改即可成为一个generator:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
如果一个函数定义中包含
yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator,注意和普通函数的执行流程不一样,变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行,例子如下:
def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)
调用:
>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
这里的odd
不是普通函数,而是generator函数
,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)
就报错。
请务必注意:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator!
比如上面的例子如果调用方式如下,就不再输出我们预期的结果:
>>> next(odd())
step 1
1
>>> next(odd())
step 1
1
>>> next(odd())
step 1
1
原因在于上述代码实际上创建了3个完全独立的generator,对3个generator分别调用next()当然每个都会返回第一个值。因此正确写法应是创建一个generator对象,然后不断对这一个generator对象调用next()。
同样的,把函数改成generator函数后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:
调用时:
>>> for n in fib(6):
... print(n)
...
1
1
2
3
5
8
>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done
我们已经知道,可以直接作用于for循环的数据类型有以下几种:集合数据类型(list、tuple、dict、set、str等)和generator(包括生成器和带yield的generator函数),这些可以直接作用于for循环的对象统称为
可迭代对象
:Iterable,可以被next()函数调用并不断返回下一个值的对象称为迭代器
:Iterator.
>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance(100, Iterable)
False
>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
生成器都是Iterator对象,但list、dict、str等虽然是Iterable,却不是Iterator。
这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是
惰性
的,只有在需要返回下一个数据时它才会计算。Iterator甚至可以表示一个无限大的数据流
,例如全体自然数。而使用list是永远不可能存储全体自然数的。
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
Python的for循环本质上就是通过不断调用next()函数实现的!
例如:
for x in [1, 2, 3, 4, 5]:
pass
完全等价于:
# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
try:
# 获得下一个值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循环
break
越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。函数式编程就是一种
抽象程度很高
的编程范式,纯粹的函数式编程语言编写的函数没有变量
,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用
。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言
。
>>> f = abs
>>> f
<built-in function abs>
>>> f = abs
>>> f(-10)
10
并且注意到变量指向函数后,可以直接用变量来调用该函数
>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
把abs指向10后,就无法通过abs(-10)
调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10!要恢复该函数除非重启python交互环境。
注意:由于这里的abs函数实际上是定义在
builtins
模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
。
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
def add(x, y, f):
return f(x) + f(y)
当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,根据函数定义,我们可以推导计算过程为:
x = -5
y = 6
f = abs
f(x) + f(y) ==> abs(-5) + abs(6) ==> 11
return 11
关于map和reduce的理解可以参考论文
map()
函数接收两个参数,一个是函数,一个是Iterable
,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
例如:
>>> def f(x):
... return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的
f(x)=x2
,还可以计算任意复杂的函数,只需要修改函数的定义即可,很方便。
比如,把这个list所有数字转为字符串:
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce把一个函数作用在一个序列
[x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
比如写一个把str转换为int的函数:
from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
def fn(x, y):
return x * 10 + y
def char2num(s):
return DIGITS[s]
return reduce(fn, map(char2num, s))
filter()
的作用是从一个序列中筛出符合条件的元素,也接收一个函数和一个序列,和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
例如,在一个list中,删掉偶数,只保留奇数,可以这么写:
def is_odd(n):
return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]
注意到filter()函数返回的是一个Iterator,也就是一个惰性序列(只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素),所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。
可以接收一个key函数来实现自定义的排序,key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。用sorted()排序的关键在于实现一个
映射函数
。
例如按绝对值大小排序:
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]
忽略大小写,按照字母序排序
这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。我们给sorted传入key函数,即可实现忽略大小写的排序:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True
:
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']
从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
比如要实现一个可变参数的求和,如果不需要立刻求和,而是在后面的代码中,根据需要再计算,可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数以及其所在的地址:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
调用函数f时,才真正计算求和的结果:
>>> f()
再注意一点,当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False
f1()和f2()的调用结果互不影响。
注意到上述例子中,返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,这种情况被称为
闭包
。
返回闭包时牢记一点:返回函数不要引用任何
循环变量
,或者后续会发生变化的变量!
例如:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了,预期结果应该是1、4、9,但实际结果是9、9、9,原因就在于返回的函数引用了变量i
,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果都为9。
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
这时的结果就是预期结果,缺点是代码较长,可利用lambda函数缩短代码。
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常,但是,如果对外层变量赋值,由于Python解释器会把该变量当作函数的局部变量,它会报错,此时就需要用nonlocal
声明它不是局部变量,例子如下:
def inc():
x = 0
def fn():
nonlocal x
x = x + 1
return x
return fn
f = inc()
print(f()) # 1
print(f()) # 2
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数,同样,也可以把匿名函数作为返回值返回。
例如,计算f(x)=x2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:
>>> list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
通过对比可以看出,匿名函数lambda x: x * x
实际上就是:
def f(x):
return x * x
关键字lambda
表示匿名函数,冒号前面的x表示函数参数。匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
在代码运行期间动态增加功能的方式,称之为
装饰器
(Decorator),本质上,decorator就是一个返回函数的高阶函数
例如:
>>> def now():
... print('2015-3-25')
...
>>> f = now
>>> f()
2015-3-25
现在,假设我们要增强now()
函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()
函数的定义,首先要定义一个能打印日志的decorator,可以定义如下:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。使用时,要借助Python的@语法,把decorator置于函数的定义处:
@log
def now():
print('2015-3-25')
调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
>>> now()
call now():
2015-3-25
如果decorator本身需要传入参数,那就需要编写一个返回decorator
的高阶函数,写出来会更复杂。比如,要自定义log的文本:
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
此时使用如下:
@log('execute')
def now():
print('2015-3-25')
和最初的两层嵌套的decorator相比,这里是3层嵌套,效果是这样的:
>>> now = log('execute')(now)
分析下,首先执行log('execute')
,返回的是decorator
函数,再调用返回的函数,参数是now函数,返回值最终是wrapper
函数。
注意!需要把原始函数的
__name__
等属性复制到decorator里定义的函数中,否则,有些依赖函数签名的代码执行就会出错。
比如上面例子的wrapper
函数,经过decorator装饰之后的函数,它们的__name__
已经从原来的'now'变成了'wrapper',因此需要补充定义如下:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
另外,在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过
继承
和组合
来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持
decorator。Python的decorator可以用函数实现,也可以用类实现
。
通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。
以int()
函数为例,该函数默认按十进制转换,如果传入base参数,就可以做N进制的转换,假设要转换大量的二进制字符串,每次都传入int(x, base=2)
非常麻烦,于是,我们想到,可以定义一个int2()
的函数,默认把base=2
传进去:
def int2(x, base=2):
return int(x, base)
functools.partial
就是帮助我们创建一个偏函数
的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1000000', base=10)
1000000
所以,简单总结偏函数的作用就是,当函数的参数个数太多,需要简化时,偏函数可以把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
即Object Oriented Programming,简称
OOP
,是一种程序设计思想。OOP把对象
作为程序的基本单元,一个对象包含了数据
和操作数据的函数
。面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类
(Class)的概念。给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法
(Method)。
类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
定义类的格式:
class 大写开头的类名(object):
pass
这里的(object)
表示该类是从哪个类继承下来的,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
类名+()
实现的;__init__
方法来实现,如下:class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意到
__init__
方法的第一个参数永远是self
,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
__init__
方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__
方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去,如下:>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'
定义类后,每个由该类创建的实例就拥有所有该类中的数据,我们可以通过函数来访问这些数据,但既然实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在类的内部定义
访问数据的函数
,这样,就把“数据”给封装起来了。这些封装数据的函数和类本身是关联起来的,我们称之为类的方法
。这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节,这是封装的好处。
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('%s: %s' % (self.name, self.score))
调用:
>>> bart.print_score()
Bart Simpson: 59
__
,在Python中,实例的变量名如果以__
开头,就变成了一个私有变量
(private),只有内部可以访问,外部不能访问。class Student(object):
...
def get_score(self):
return self.__score
def set_score(self, score):
self.__score = score
这种方式可以对参数做检查,避免传入无效的参数
__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量。总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。不过不同版本的Python解释器可能会把
__name
改成不同的变量名。
当我们定义一个class的时候,可以从某个现有的class继承,新的class称为
子类
(Subclass),而被继承的class称为基类、父类或超类
(Base class、Super class)。继承最大的好处是子类获得了父类的全部功能。
例子如下:
父类:
class Animal(object):
def run(self):
print('Animal is running...')
子类:
class Dog(Animal):
pass
class Cat(Animal):
pass
调用:
dog = Dog()
dog.run()
cat = Cat()
cat.run()
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
当子类和父类都存在相同的方法A时,我们说,子类的A覆盖了父类的A,在代码运行的时候,总是会调用子类的A。这样,我们就获得了继承的另一个好处:多态。
def run_twice(animal):
animal.run()
animal.run()
当我们传入Animal的实例时,run_twice()就打印出:
>>> run_twice(Animal())
Animal is running...
Animal is running...
Dog、Cat同理;而当我们再定义一个Tortoise
类型,也从Animal
派生:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
调用run_twice()
时,传入Tortoise
的实例,发现也可以打印出:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
会发现,即使新增子类,不必对run_twice()
做任何修改
实际上,任何依赖父类Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。因此,多态的好处就是,当我们需要传入各种子类时,只需要接收它们的父类类型即可,然后按照父类类型进行操作。
通过多重继承,一个子类就可以同时获得多个父类的所有功能。
假设我们要实现以下4种动物:
Dog - 狗狗;
Bat - 蝙蝠;
Parrot - 鹦鹉;
Ostrich - 鸵鸟。
以狗狗和鹦鹉为例,两者既可以以哺乳动物和鸟类区别开,也可以以能跑和能飞区别开,也就是说可以有多层次的父类,再细分下去将是指数式的爆炸增长,这时就可以采用多重继承。例如:
首先,主要的类层次仍按照哺乳类和鸟类设计:
class Animal(object):
pass
# 大类:
class Mammal(Animal):
pass
class Bird(Animal):
pass
# 各种动物:
class Dog(Mammal):
pass
class Bat(Mammal):
pass
class Parrot(Bird):
pass
class Ostrich(Bird):
pass
现在,我们要给动物再加上Runnable和Flyable的功能,只需要先定义好Runnable和Flyable的类:
class Runnable(object):
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog:
class Dog(Mammal, Runnable):
pass
上面的多重继承设计就叫
MixIn
,MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。为了更好地区分开来,可以给多重继承的父类名字加上后缀MixIn
。
比如,编写一个多进程模式的TCP服务,定义如下:
class MyTCPServer(TCPServer, ForkingMixIn):
pass
编写一个多线程模式的UDP服务,定义如下:
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn:
class MyTCPServer(TCPServer, CoroutineMixIn):
pass
这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
这就是多态真正的威力:调用方只管调用,不管细节,无需确切地知道它的子类型,从上述例子,当我们新增一种
Animal
的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的,这就是著名的开闭原则
,具体内容如下:
以上述例子为例,对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法;而对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了,这就是动态语言的
鸭子类型
(即一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子),它并不要求严格的继承体系。
可以使用
type()
函数来判断对象的类型,括号中的参数是需要判断的对象。
>>> import types
>>> def fn():
... pass
...
>>> type(fn)==types.FunctionType
True
>>> type(abs)==types.BuiltinFunctionType
True
>>> type(lambda x: x)==types.LambdaType
True
>>> type((x for x in range(10)))==types.GeneratorType
True
对于class的
继承
关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()
函数。isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上(比如判断a是Dog类型,Animals是dog的父类,因此a也是Animals类型)。
>>> isinstance([1, 2, 3], (list, tuple))
True
总是优先使用isinstance()判断类型,可以将指定类型及其子类“一网打尽”。
如果要获得一个对象的所有属性和方法,可以使用
dir()
函数,它返回一个包含字符串的list。
比如,获得一个str对象的所有属性和方法:
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
类似
__xxx__
的属性和方法在Python中都是有特殊用途的,比如__len__
方法返回长度,调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以两者其实是等价的。
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()
紧接着,可以测试该对象的属性:
>>> hasattr(obj, 'x') # 有属性'x'吗?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有属性'y'吗?
False
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') # 有属性'y'吗?
True
>>> getattr(obj, 'y') # 获取属性'y'
19
>>> obj.y # 获取属性'y'
19
如果试图获取不存在的属性,会抛出AttributeError的错误;
可以传入一个default参数,如果属性不存在,就返回默认值,如:
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404
要注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息,比如可以直接写:
sum = obj.x + obj.y
,就不要写:sum = getattr(obj, 'x') + getattr(obj, 'y')
。
例如:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。
请注意,在Python这类动态语言中,根据鸭子类型,有read()方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能。
由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性的方法是通过实例变量,或者通过self变量。
例如:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
class Student(object):
name = 'Student'
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。测试如下:
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael,
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
因此在编写程序的时候,千万不要对实例属性和类属性使用相同的名字。
正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以动态给该实例绑定任何属性和方法,动态绑定允许我们在
程序运行的过程中
动态给class加上功能(也就是说即使类和实例中没有定义),这就是动态语言的灵活性。
例子如下:
定义class:
class Student(object):
pass
>>> s = Student()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael
>>> def set_age(self, age): # 定义一个函数作为实例方法
... self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
>>> s.set_age(25) # 调用实例方法
>>> s.age # 测试结果
25
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的
__slots__
变量,来限制该class中实例能添加的属性。
例如,定义时:
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
创建实例并绑定属性:
>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
试图绑定score得到AttributeError
的错误。
要注意,
__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以随便改数据,按照常规写法,可以做如下处理:
class Student(object):
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
但是又略显复杂,这个时候其实可以用装饰器@property
。
@property
装饰器就是负责把一个方法变成属性调用的。@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
修改上述例子如下:
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
上面把一个getter方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器@score.setter
,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作。调用如下:
>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2015 - self._birth
上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。
要特别注意:属性的方法名不要和实例变量重名。
例如,以下的代码是错误的:
class Student(object):
# 方法名称和实例变量均为birth:
@property
def birth(self):
return self.birth
这是因为调用s.birth
时,首先转换为方法调用,在执行return self.birth
时,又视为访问self的属性,于是又转换为方法调用,造成无限递归,最终导致栈溢出报错RecursionError
。
看到类似
__slots__
这种形如__xxx__
的变量或者函数名就要注意,这些在Python中是有特殊用途的,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。
__str__
:>>> class Student(object):
... def __init__(self, name):
... self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>
但是这样的结果不够美观,也不是我们想要的,可以利用__str__
定制如下:
>>> class Student(object):
... def __init__(self, name):
... self.name = name
... def __str__(self):
... return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)
这样不仅美观而且容易看出实例内部重要的数据。
但是如果是在python命令交互窗口中,直接敲变量而不是print来打印,则依然显示最初的不美观结果,因为直接显示变量调用的不是__str__(),而是__repr__()
,两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,repr()是为调试服务的。解决办法是再定义一个__repr__()。但是通常__str__()和__repr__()代码都是一样的,所以可以:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
__iter__
:如果一个类想被用于
for ... in
循环,类似list或tuple那样,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值,直到遇到StopIteration
错误时退出循环。
我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b
def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 100000: # 退出循环的条件
raise StopIteration()
return self.a # 返回下一个值
现在,试试把Fib实例作用于for循环:
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
...
46368
75025