面向对象编程
6.1 类和实例
class后面紧接类名,通常是以大写字母为开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
class Student(object):
pass
可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:
>>> class Student(object):
... pass
>>> bart = Student()
>>> bart.name = 'Bart.Simpson'
>>> bart.name
'Bart.Simpson'
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name,scope等属性绑上去:
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 Simon', 59)
>>> bart.score
59
>>> bart.name
'Bart Simon'
数据封装
可以直接在类的内部定义访问数据的函数,这样,就把“数据”封装起来了。这些封装数据的函数是和类本身是关联起来的,称之为类的方法。
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 = Student('Kevin', 99)
>>> bart.print_score()
Kevin: 99
这样的话,我们从外部看Student类,就只需要知道,创建实例需要给出类的属性name和score,而如何打印,则是在Student类的内部定义的。
封装的另一个优点在于,可以给类增加新的方法。比如下例给Person类增加新的判别身材的方法:
class Person(object):
def __init__(self, name, sex, height,weight):
self.sex = sex
self.height = height
self.name = name
self.weight = weight
def print_sex_height(self):
print('%s: %s, %s' %(self.name, self.sex, self.height))
def figure(self):
if self.weight > 140:
print('Too fat, you should take more exsrcise.')
elif self.weight <110:
print('Too thin, you should be extra mindful of getting the right nourishment.')
else:
print('You have a good figure, keep it.')
同样的,新增的figure方法可以直接在实例变量上调用,不需要知道内部实现细节。
>>> baby = Person('zhengning', 'female', 162, 98)
>>> baby.print_sex_height()
zhengning: female, 162
>>> baby.figure()
Too thin, you should be extra mindful of getting the right nourishment.
小结
- 类是创建实例的模板,而实例则是一个一个具体的对象,每个实例拥有的数据都互相独立,互不影响。
- 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据。
- 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
- 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。
>>> baby = Person('zhengning', 'female', 162, 98)
>>> kevin = Person('wukaiwen', 'male', 165, 126)
>>> baby.age = 8
>>> baby.age
8
>>> kevin.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'age'
6.2 访问限制
在class内部,可以通过定义属性和方法,而外部代码则可以通过直接实例变量的方法来操作数据。这样,就隐藏了内部的复杂逻辑。但是这样,则存在一个内部属性易被修改的问题,即外部代码能通过实例的方法来修改类的属性。
>>> kevin = Person('wukaiwen', 'male', 165, 126)
>>> kevin.weight
126
>>> kevin.weight = 130
>>> kevin.weight
130
如果要让内部属性不被外部访问,可以在属性的名称前加上两个下划线。在Python中,实例的变量如果在类的内部定义时以__开头,就变成了一个私有变量(private),只有内部可以访问,而外部不能访问:
class Person(object):
def __init__(self, name, sex, height, weight):
self.__sex = sex
self.__name = name
self.__height = height
self.__weight = weight
改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__height及.__height了:
>>>zhengning = Person('zhengning', 'female', 162, 98)
>>>zhengning.__height
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-6-1abc5119b3fc>", line 1, in <module>
zhengning.__height
AttributeError: 'Person' object has no attribute '__height'
这样保证了外部代码不能随意修改对象内部的状态,即通过访问限制的保护,代码更加健壮。
如果要获取类内部的私有属性的话,可以给Person类增加诸如get_name和get_sex这样的方法:
class Person(object):
...
def get_name(self):
return self.__name
def get_sex(self):
return self.__sex
此时,调用方法获取实例的姓名和性别及输出如下:
>>>baby = Person('zhengning', 'female', 162, 98)
>>>baby.get_name()
'zhengning'
>>>baby.get_sex()
'female'
如果又要允许外部代码修改height和weight的话,可以再给Person类增加set_height和set_weight方法:
class Person(object):
...
def set_name(self):
self.__name = name
def set_sex(self):
self.__sex = sex
此时,调用方法获取实例的姓名和性别及输出如下:
>>>from test1 import *
>>>baby = Person('zhengning', 'female', 162, 98)
>>>baby.get_height()
162
>>>baby.set_height(165)
>>>baby.get_height()
165
这里我们就需要考虑一个问题了,即最开始直接通过修改实例的属性kevin.weight = 130即可修改属性值,那么为什么要单独定义一个方法来修改属性呢?这是因为在方法中,可以对参数做检查,避免传入无效的参数。
class Person(object):
...
def set_weight(self, weight):
if 0 <= weight <= 200:
self.__weight = weight
else:
raise ValueError('Invalid weight.')
使用情况如下:
>>>from test1 import *
>>>baby = Person('zhengning', 'female', 162, 210)
>>>baby.get_weight()
210
>>> baby.set_weight(102)
>>>baby.get_weight()
102
>>>baby.set_weight(220)
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-7-038c5fd42666>", line 1, in <module>
baby.set_weight(220)
File "G:/PyCharm/PycharmProjects/Python_Liaoxuefeng/chapter06\test1.py", line 25, in set_weight
raise ValueError('Invalid weight.')
ValueError: Invalid weight.
上面说了这么多,我们再来考虑这么一个问题:双下划线开头的实例是不是一定不能从外部访问呢?其实也不是。不能直接访问的原因在于Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:
>>>zhengning._Person__height = 163
>>>zhengning._Person__height
163
6.3 继承和多态
在面向对象的程序设计(OPP)中,当我们定义一个类class时,可以从某个现有的已定义的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Fruit的class,有一个run()方法可以直接打印:
class Fruit(object):
def taste(self):
print('Fruit is delicious.')
当我们需要编写Apple类和Pear类时,就可以直接从Fruit类继承:
class Apple(Fruit):
pass
class Pear(Fruit):
pass
对于Apple类和Pear类而言,Fruit类就是它们的父类,而它们就是Fruit类的子类。
继承有什么好处呢?最大的好处是子类获得了父类的全部功能。上述中,由于Fruit类定义了taste方法,因此,Apple和Pear作为它的子类,什么事也没干,就自动获得了taste()方法:
>>>from test2 import *
>>>apple = Apple()
>>>apple.taste()
Fruit is delicious.
>>>pear = Pear()
>>>pear.taste()
Fruit is delicious.
当然,也可以直接对子类增加一些方法,比如在子类Apple中:
class Apple(Fruit):
def color(self):
print('The apple is red.')
继承的第二个好处是:多态。即当子类和父类都存在相同的方法时,子类的方法会覆盖父类的方法:
class Apple(Fruit):
def taste(self):
print('Fruit is delicious.')
class Pear(Fruit):
def taste(self):
print('Fruit is delicious.')
再次运行,结果如下:
>>>from test2 import *
>>>apple = Apple()
>>>apple.taste()
The apple is delicious.
>>>pear = Pear()
>>>pear.taste()
The pear is delicious.
要理解什么是多态,我们首先要对数据类型再做一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们自己定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:
aList = list() # a是list类型
fruit = Fruit() # b是Fruit类型
apple = Apple() # c是Apple类型
判断一个变量是否是某个类型可以用isinstance()判断:
>>>isinstance(apple, Apple)
True
现在,我们再思考一个关于继承的问题:顾名思义,如果一个实例的数据类型是某个子类例如Aplle类型,那么它是否也属于该类的父类Fruit类型呢?答案是肯定的:
>>>isinstance(apple, Fruit)
True
那么多态的好处在哪里呢?我们需要再编写一个函数,这个函数接受一个Fruit类型的变量:
def taste_twice(fruit):
fruit.taste()
fruit.taste()
当我们传入Fruit的实例时,taste_twice()就打印出:
>>>taste_twice(Fruit())
Fruit is delicious.
Fruit is delicious.
当我们传入Apple实例时,taste_twice()就打印出:
>>>taste_twice(Apple())
The apple is delicious.
The apple is delicious.
在这种情况下,如果我们再定义一个Orange类型,也从Fruit派生出来:
class Orange(Fruit):
def taste(self):
print('The orange is delicious.')
当我们调用taste_twice()时,传入Orange的实例:
>>>taste_twice(Orange())
The orange is delicious.
The orange is delicious.
这时,我们发现,新增一个Fruit的子类Orange,而不必对taste_twice()做任何修改。实际上,任何以Fruit作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
静态语言 vs 动态语言
对于静态语言而言,如果需要传入Fruit类型,则传入的对象必须是Fruit类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Fruit类型,我们只需要保证传入的对象有一个run()就可以了:
class Timer(object):
def taste(self):
print('Start...')
调用结果如下:
>>>taste_twice(Timer())
Start...
Start...
小结
- 继承可以把父类的所有功能都直接拿过来,而不必直接定义属性和方法。
- 多态则使得子类可以新增自己特有的方法,也可以把父类不合适的方法覆盖重写,只需从新定义和父类相同的方法。
6.4 获取对象信息
当我们拿到一个对象的引用时,可以用哪些方法来知道这个对象是什么类型呢?
使用type()
首先,我们使用type()函数来判断对象类型,基本类型都可以通过type()来判断:
>>>type(123)
int
>>>type('str')
str
如果一个变量指向函数或者类,也可以用type()判断:
>>>type(abs)
builtin_function_or_method
通过上述几个例子,可以看出type()函数返回的类型是对象所应的class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:
>>>type(123) type(456)
True
>>>type(123) int
True
>>>type('123') type('abc') # 注意123加了引号
True
>>>type(123) type('abc')
False
判断基本类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:
>>>import types
>>>def fcn():
pass
>>>type(fcn) 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
使用instance()
对于class的继承关系来说,使用type()就不太方便。我们要判断class类型的时候,可以使用instance()函数,这在上节使用过。
此外,还可以判断一个变量是否是某些类型中的一种:
>>>isinstance([1, 2, 3], (list, tuple))
True
>>>isinstance((1, 2, 3), (list, tuple))
True
这里需要注意几个问题:
- 能用type()判断的基本类型也可以使用isinstance()判断
- 可以在if判断语句中,同时使用多个type()和isinstance()来进行逻辑运算
使用dir()
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
>>>dir('zhengning')
Out[22]:
['__add__',
'__class__',
'__contains__',
'__len__',
...
'translate',
'upper',
'zfill']
类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你试图调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以下面的代码是等价的:
>>>len('zhengning')
9
>>>'zhengning'.__len__()
9
我们自己写的类,也想用len(myObj)的话,就自己写一个__len__方法:
class MyObj(object):
def __len__(self):
return 10
>>>baby = MyObj()
>>>len(baby)
100
剩下的就都是普通属性或方法,比如upper()返回大写的字符串:
>>>'zhengning'.upper()
'ZHENGNING'
上述只说明了如何把属性和方法列出来,其实配合getattr()、setattr()、hasattr(),我们可以直接操作一个对象的状态:
class MyObj(object):
def __init__(self):
self.name = 'zhengning'
def love(self):
print('I love u.')
>>>from test3 import *
>>>baby = MyObj()
>>>hasattr(baby, name) # 属性name需要加引号,否则会报错
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-4-e82eb9214bf5>", line 1, in <module>
hasattr(baby, name)
NameError: name 'name' is not defined
>>>hasattr(baby, 'name') # 检查是否有属性'name'
True
>>>baby.name # 获取属性'name'
'zhengning'
>>>hasattr(baby, 'age') # 检查是否有属性'age'
False
>>>setattr(baby, 'age', 25) # 设置一个属性'age'
>>>hasattr(baby, 'age')
True
>>>getattr(baby, 'age') # 获取属性'age'
25
>>>baby.age
25
如果试图获取不存在的属性,会抛出AttributeError的错误:
>>>getattr(baby, 'height') # 获取baby的属性'height',没有该属性则报错
Traceback (most recent call last):
File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-14-15ec7e1fc4a1>", line 1, in <module>
getattr(baby, 'height')
AttributeError: 'MyObj' object has no attribute 'height'
也可以获得对象的方法:
>>>hasattr(baby, 'love') # 检查对象baby有方法'love'
True
>>>getattr(baby, 'love') # 获取对象baby的方法'love'
<bound method MyObj.love of <test3.MyObj object at 0x000002074210F6A0>>
>>>myLove = getattr(baby, 'love') # 获取方法'love'并赋值给变量myLove
>>>myLove # myLove指向baby.love
<bound method MyObj.love of <test3.MyObj object at 0x000002074210F6A0>>
>>>myLove() # 调用myLove()与调用baby.love()是一样的
I love u.
小结
通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。但需要注意的是,只有在不知道对象信息的时候,我们才会取获取信息。比如,如果可以直接写:
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()就派上了用场。
6.5 实例属性和类属性
由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性有两种方法,即通过实例变量或者self变量:
class Person(object):
def __init__(self, name): # 通过self变量创建属性
self.name = name
>>>baby = Person('zhengning')
>>>baby.name
'zhengning'
>>>baby.age = 25 # 通过实例变量创建属性
那么,Person类本身怎么绑定属性呢?可以直接在class中定义属性,这种属性是类属性,归Person类所有:
class Person(object):
race = 'Asian'
def __init__(self, name):
self.name = name
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到:
>>>from test3 import * # 程序保存在test3.py文件中
>>>baby = Person('zhengning') # 创建实例baby
>>>baby.race
'Asian'
>>>print(baby.race) # 打印race属性,因为实例并没有race属性,所以会基础查找class的name属性。注意和上一条语句进行对比
Asian
>>>print(Person.race) # 打印类的race属性
Asian
>>>baby.race = 'Han' # 给实例绑定race属性
>>>print(baby.race) # 由于实例属性的优先级比类属性高,因此,它会屏蔽掉类的race属性
Han
>>>print(Person.race) # 但是类属性并未消失,仍然可以用左边这种方式访问
Asian
>>>del baby.race # 如果删除实例的race属性
>>>print(baby.race) # 再次调用baby.race,由于实例的race属性没有找到,类的race属性就显示出来了
Asian
从上面的例子可以看出,在编写程序的时候,千万不能把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性。但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。