python文件开头
关于脚本第一行的#!/usr/bin/python3 的解释: 脚本语言的第一行只对Linux、Unix用户适用,用来指定脚本用什么解释器执行。有这句的加上执行权限后,可以直接用./
执行,不然会报错,因为找不到python解释器。#!/usr/bin/python3 是告诉操作系统执行这个脚本的时候,调用/usr/bin下的python3 解释器。#!/usr/bin/env python3 这种用法是为了防止操作系统用户没有将python3 装在默认的/usr/bin目录下。当系统看到这一行的时候,首先会到env设置里查找python3 的安装路径,再调用对应路径下的解释器程序完成操作。
#!/usr/bin/python3 相当于写死了python路径。
#!/usr/bin/env python3 会去环境设置寻找python目录,可以增强代码可移植性,推荐这种写法。
分成两种情况:
(1)如果.py文件开头写了#!/usr/bin/python3
,执行时使用:
./script.py
(2)如果.py文件开头不写,执行时使用:
python3 script.py
注意:python3默认使用utf-8编码,可以不用在开头加
#coding=utf-8
直接输出中文。
一、标识符
- 由数字、字母、下划线、汉字组成
- 不能用数字开头
- 字母区分大小写
二、缩进与代码块
- python使用缩进来表示代码块,不需要使用大括号{ }
- 统一代码块的语句必须包含相同的缩进格数(一般默认4个空格)
- 一行写不开:
- 用反斜杠(\)来实现多行语句的连接,使用续行符需要注意两点:续行符后不能有空格、续行符后必须直接换行。
- 在[ ],{ } 或 ( ) 中的多行语句,不需要用反斜杠(\)
total = item_one + \
item_two + \
item_three
total = ['item_one', 'item_two', 'item_three',
'item_four', 'item_five']
三、注释
- 单行注释以
#
开头 - 多行注释用三个单引号
'''
或三个双引号"""
将注释括起
四、变量与数据类型
- 变量不需要声明,每个变量在使用前都必须赋值,变量赋值后,该变量才会被创建。
- 在python中,变量就是变量,它没有类型,我们所说的“类型”是变量所指的内存中对象的类型。
- 多变量赋值:
a = b = c = 1
a, b, c = 1, 2, "runboob"
注意,第二种同步赋值语句会同时运算等号右边的所有表达式,并一次性将右侧表达式的结果分别赋值给等号左侧的对应变量。
数据类型(六种)
1. 数字
- 包括int、float、bool、complex几个小类型
- 可以用
type(变量名)
查看具体是哪种小类型 - 数字类型转换
int(x)
,float(x)
,complex(x,y)
2. 字符串
-
用单引号
'
或双引号"
括起来 -
字符串可以用
+
运算符连接在一起,用*
运算符重复 -
两种索引方式:
1. 从左往右以0开始
2. 从右往左以-1开始注意:截取的时候是前闭后开的
-
python使用反斜杠
\
转义特殊字符,如果你不想让反斜杠发生转义,可以在字符串前面添加一个r
,表示原始字符串:
>>> print('Ru\noob')
Ru
oob
>>> print(r'Ru\noob')
Ru\noob
>>>
3. 元组(元组中数据不可修改)
- 元组属于集合类的数据结构实现,元组中元素可以不相同,支持数字、字符串甚至列表。
- 元组写在圆括号()之间,元素之间用逗号隔开
- 和字符串一样,元组可以被索引和截取(左闭右开)
- 元组中的元素值不能修改,但我们可以对元组进行连接组合
- 元组可以用
+
运算符连接,用*
运算符重复 - 构造包含0个或1个元素的元组比较特殊,所以有一些额外的语法规则:
tup1 = () # 空元组
tup2 = (20,) # 一个元素,需要在元素后添加逗号
- 元组中的元素值是不允许删除的,但我们可以使用del语句来删除整个元组,如下实例:
#!/usr/bin/python3
tup = ('Google', 'Runoob', 1997, 2000)
print (tup)
del tup
print ("删除后的元组 tup : ")
print (tup)
4. 列表
- 列表属于集合类的数据结构实现,列表中的元素类型可以不同,支持数字、字符串、甚至列表
- 列表写在方括号[ ]之间,元素之间用逗号隔开
- 和字符串一样,列表可以被索引和截取(前闭后开)
- 列表可以用
+
运算符连接,用*
运算符重复 - 列表函数&方法:
len(list) #列表元素个数
max(list)、min(list) #返回列表元素最大最小值
list.insert(index,obj) #将对象插入列表
list.append(obj) #在列表末尾添加新对象
list.remove(obj) #移除列表中某个值的第一个匹配项
list.count(obj) #统计某元素在列表中的出现次数
list.extend(seq) #在列表末尾一次性追加另一个序列中的多个值(用新列表扩展源列表)
- 列表生成式:
>>>L=list(range(100))
>>>L
[0, 1, 2, 3, ..., 99]
列表生成式可以用一行语句替代循环
>>> [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']
三层和三层以上的循环就很少用到了。
>>> [x for x in range(1, 11) if x % 2 == 0]
[2, 4, 6, 8, 10]
>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
可见,在一个列表生成式中,for
前面的if ... else
是表达式,而for
后面的if
是过滤条件,不能带else
。
5. 集合
- 集合是一个无序的不重复元素序列(会自动去重)
- 可以使用大括号{ }或者
set()
函数创建集合 - 创建空集合必须用
set()
,因为{ }用来创建一个字典 - 集合的基本操作:
1.添加元素s.add(x)
或s.update(x)
2.移除元素s.remove(x)
或s.discard(x)
3.计算集合中元素个数len(s)
4.清空集合s.clear
>>> # 下面展示两个集合间的运算.
...
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b # 集合a中包含而集合b中不包含的元素
{'r', 'd', 'b'}
>>> a | b # 集合a或b中包含的所有元素
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b # 集合a和b中都包含了的元素
{'a', 'c'}
>>> a ^ b # 不同时包含于a和b的元素
{'r', 'd', 'b', 'm', 'z', 'l'}
6. 字典
- 列表是有序的对象集合,字典是无序的对象集合。
- 两者之间的区别是:字典中的元素是通过键来存取,而不是通过偏移
- 字典用{ }标识,他是无序的
键:值
的集合 - 键必须使用不可变类型,在同一字典中键必须是唯一的
- 修改字典
向字典添加新内容的方法是增加新的键/值对,修改或删除已有键/值对如下实例:
#!/usr/bin/python3
dict = {'Name': 'Runoob', 'Age': 7, 'Class': 'First'}
dict['Age'] = 8 # 更新 Age
dict['School'] = "菜鸟教程" # 添加信息
- 删除字典元素
能删单一的元素也能清空字典,清空只需一项操作。
显示删除一个字典用del命令,如下实例:
#!/usr/bin/python3
dict = {'Name': 'Runoob', 'Age': 7, 'Class': 'First'}
del dict['Name'] # 删除键 'Name'
dict.clear() # 清空字典
del dict # 删除字典
五、运算符
- 算术运算符
+、-、*、/、%(取余)、**(幂)、//(向下取接近商的整数)
- 比较运算符
==、!=、<、>、>=、<=
- 位运算符
&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移运算符)、>>(右移运算符)
- 逻辑运算符
and(与)、or(或)、not(非)
六、条件控制结构
- python没有switch-case结构,只有if-elif-else结构
- 代码执行过程:
- if结构为:
if 条件1: #注意冒号
执行语句1 #用缩进划分语句块
elif 条件2: #注意不是else if
执行语句2
elif 条件3:
执行语句3
else:
执行语句4
最终都要执行的语句
七、循环结构
while 判断条件: #注意冒号
执行语句 #用缩进划分语句块
for 变量 in 序列: #序列常使用range()函数
执行语句
- while和for循环都可搭配else使用,它在穷尽列表(for循环)或条件变为false(while循环)导致循环终止时被执行,它与循环结构后的普通执行语句的区别是:循环结构被break终止时,不执行else子句
- break和continue的用法同C语言相同
- python没有do…while结构
- for循环里,同时引用了两个变量,在python里是很常见的,比如下面的代码:
>>>for x,y in [(1,1),(2,4),(3,9)]:
print(x,y)
1 1
2 4
3 9
八、pass语句
python中pass语句是空语句,他不做任何事情,一般用做占位语句,是为了保持程序结构的完整性。
九、函数
- 基本结构
def 函数名(参数列表):
函数体
- 参数传递:
1)关键字参数
使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值。
def f(a,b):
print(a)
print(b)
f(b=4,a=25) #指定关键字参数
2)默认参数
def f(name,age=25):
print(name)
print(age)
f(kitty,21)
f(Liming) #没传的参数使用默认值
3)不定长参数
该函数能处理比当初声明时更多的参数
加了*
号的参数会以元组形式导入,存放所有未命名的参数
def f(a,*b):
.
.
.
f(3,8,9,10) #其中后三个参数会以(8,9,10)形式的元组传入
加**
号的参数会议字典的形式传入
def f(a,**b):
.
.
.
f(3,k=1,q=7) #后两个参数会以{'k':1,'q':7}形式的字典传入
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收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)
- 函数名
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1
- 参数类型检查
让我们修改一下my_abs的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance()实现:
def my_abs(x):
if not isinstance(x, (int, float)):
raise TypeError('bad operand type')
if x >= 0:
return x
else:
return -x
添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误
十、模块
模块是一个包含所有你定义的函数和变量的文件,其后缀名是.py
。模块可以被别的程序引入,以使用该模块的函数等功能。这也是使用python标准库的方法。(引用Python库时,使用import <功能库名称>
使用时采用<功能库名称>.<函数名称>()
方式调用具体功能)
- 导入模块
import support 调用时support.func() from support import func 调用时直接func()
-
运行本模块
if __name__ == '__main__': test() #该语句只有在运行本模块时才会运行
当我们在命令行运行XXX模块文件时,python解释器把一个特殊变量
__name__
置为__main__
,而如果在其他地方导入XXX模块时,if
判断将失败。在一个模块中,写在该判断语句下面的可执行语句与其他位置的可执行语句在执行本模块时都会执行,区别在本模块被其它模块导入时,写在其他位置的可执行语句会被自动执行,而写在该判断语句下面的可执行语句不会执行。因此,这种if
测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。 -
包
sound/ 顶层包 __init__.py 初始化 sound 包 formats/ 文件格式转换子包 __init__.py wavread.py wavwrite.py aiffread.py aiffwrite.py auread.py auwrite.py ... effects/ 声音效果子包 __init__.py echo.py surround.py reverse.py ... filters/ filters 子包 __init__.py equalizer.py vocoder.py karaoke.py ...
在导入在一个包的时候,Python 会根据 sys.path 中的目录来寻找这个包中包含的子目录。
目录只有包含一个叫做 __init__.py 的文件才会被认作是一个包,主要是为了避免一些滥俗的名字(比如叫做 string)不小心的影响搜索路径中的有效模块。
最简单的情况,放一个空的 :file:__init__.py就可以了。当然这个文件中也可以包含一些初始化代码或者为(将在后面介绍的) __all__变量赋值。
__all__ = ["echo", "surround", "reverse"]
这表示当你使用from sound.effects import *这种用法时,你只会导入包里面这三个子模块。
-
作用域
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过
_
前缀来实现的。正常的函数和变量名是公开的(public),可以被直接引用,比如:
abc
,x123
,PI
等;类似
__xxx__
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊变量,hello
模块定义的文档注释也可以用特殊变量__doc__
访问,我们自己的变量一般不要用这种变量名;类似
_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc
,__abc
等; -
模块搜索路径
当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错:
>>> import mymodule Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: No module named mymodule 默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中:
>>> import sys >>> sys.path ['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', ..., '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']
十一、输出格式化
print('{1} and {0}'.format(变量1,变量2))
{ }及里面的字符(格式化字段)会被format里的参数替换
{ }外面的原样输出
{ }里的数字用于指向传入对象在format的位置
print("...%d.....%s"%(变量1,变量2))
十二、读写文件
open()用于返回一个file对象
open("path/filename","mode")
mode决定打开文件的模式
r只读,r+读写,指针放文件头
w只写,从头写,原有内容被删除
w+读写,从头写,原有内容被删除
a追加写,a+追加读写
f=open("......",".....")
str=f.read()
f.write(".....")
f.close()
十三、异常
try:
执行代码
except ValueEror/TypeError/NameError:
如果在执行try子句过程中发生了异常,且异常的类型和except后的名称相符,那么对应的except子句将会执行
else:
else子句必须放在所有except子句之后,else子句将在try子句没有发生任何异常的时候执行
finally:
finally子句是不管有没有异常都将最终执行的子句
十四、全局变量、局部变量
只有在类(class)、函数(def)中定义的变量才是局部变量。
在模块(.py)中直接声明的变量,及在逻辑代码块中(try…except、for/while、if…elif…else)定义的变量为全局变量。
#!/usr/bin/python3
total = 0 # 这是一个全局变量
# 可写函数说明
def sum( arg1, arg2 ):
#返回2个参数的和."
total = arg1 + arg2 # total在这里是局部变量.
print ("函数内是局部变量 : ", total)
return total
#调用sum函数
sum( 10, 20 )
print ("函数外是全局变量 : ", total)
以上实例输出结果:
函数内是局部变量 : 30
函数外是全局变量 : 0
要想在函数内部改全局变量,total前加global
十五、程序调试
启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。我们先准备好程序:
# err.py
s = '0'
n = int(s)
print(10 / n)
然后启动:
$ python -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'
以参数-m pdb
启动后,pdb定位到下一步要执行的代码-> s = '0'
。输入命令l
来查看代码:
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)
输入命令n
可以单步执行代码:
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
-> print(10 / n)
任何时候都可以输入命令p 变量名
来查看变量:
(Pdb) p s
'0'
(Pdb) p n
0
输入命令q结束调试,退出程序:
(Pdb) q
这种通过pdb在命令行调试的方法理论上是万能的,但实在是太麻烦了,如果有一千行代码,要运行到第999行得敲多少命令啊。还好,我们还有另一种调试方法。
pdb.set_trace()
这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb
,然后,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点:
# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)
运行代码,程序会自动在pdb.set_trace()
暂停并进入pdb调试环境,可以用命令p
查看变量,或者用命令c
继续运行:
$ python err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
File "err.py", line 7, in <module>
print(10 / n)
ZeroDivisionError: division by zero
补充内容:面向对象编程
参考链接:Python廖雪峰教程之面向对象编程
面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示:
std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
def print_score(std):
print('%s: %s' % (std['name'], std['score']))
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来。
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))
给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)。面向对象的程序写出来就像这样:
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class是一种抽象概念,比如我们定义的Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的Student,比如,Bart Simpson和Lisa Simpson是两个具体的Student。
面向对象的抽象程度比函数要高,一个Class既包含数据,又包含操作数据的方法。
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
仍以Student类为例,在Python中,定义类是通过class关键字:
class Student(object):
pass
class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:
>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>
可以看到,变量bart指向的就是一个Student的实例,后面的0x10a67a590是内存地址,每个object的地址都不一样,而Student本身则是一个类。
可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name,score等属性绑上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意:特殊方法“init”前后分别有两个下划线!!!
注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。
有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。
可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法:
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))
要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入:
>>> bart.print_score()
Bart Simpson: 59
这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出name和score,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name、score属性:
>>> bart = Student('Bart Simpson', 59)
>>> bart.score
59
>>> bart.score = 99
>>> bart.score
99
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:
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))
改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name和实例变量.__score了:
>>> bart = Student('Bart Simpson', 59)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法:
class Student(object):
...
def get_name(self):
return self.__name
def get_score(self):
return self.__score
如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
class Student(object):
...
def set_score(self, score):
self.__score = score
最后注意下面的这种错误写法:
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'
表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量。不信试试:
>>> bart.get_name() # get_name()内部返回self.__name
'Bart Simpson'
在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:
class Animal(object):
def run(self):
print('Animal is running...')
当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:
class Dog(Animal):
pass
class Cat(Animal):
pass
对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。Cat和Dog类似。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,Dog和Cat作为它的子类,什么事也没干,就自动拥有了run()方法:
dog = Dog()
dog.run()
cat = Cat()
cat.run()
运行结果如下:
Animal is running...
Animal is running...
当然,也可以对子类增加一些方法,比如Dog类:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
继承的第二个好处需要我们对代码做一点改进。你看到了,无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running…,符合逻辑的做法是分别显示Dog is running…和Cat is running…,因此,对Dog和Cat类改进如下:
class Dog(Animal):
def run(self):
print('Dog is running...')
class Cat(Animal):
def run(self):
print('Cat is running...')
再次运行,结果如下:
Dog is running...
Cat is running...
当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。
在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行:
>>> b = Animal()
>>> isinstance(b, Dog)
False
Dog可以看成Animal,但Animal不可以看成Dog。
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
def run_twice(animal):
animal.run()
animal.run()
当我们传入Animal的实例时,run_twice()就打印出:
>>> run_twice(Animal())
Animal is running...
Animal is running...
当我们传入Dog的实例时,run_twice()就打印出:
>>> run_twice(Dog())
Dog is running...
Dog is running...
当我们传入Cat的实例时,run_twice()就打印出:
>>> run_twice(Cat())
Cat is running...
Cat is running...
看上去没啥意思,但是仔细想想,现在,如果我们再定义一个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...
你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。
给实例绑定属性的方法是通过实例变量,或者通过self变量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
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
从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。