面向对象程序设计
面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
在Python
中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class
)的概念。
采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student
这种数据类型应该被视为一个对象,这个对象拥有name
和score
这两个属性(Property
)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score
消息,让对象自己把自己的数据打印出来。
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class
)和实例(Instance
)的概念是很自然的。Class
是一种抽象概念,比如我们定义的Class
——Student
,是指学生这个概念,而实例(Instance
)则是一个个具体的Student
,比如,Bart Simpson
和Lisa Simpson
是两个具体的Student
。
所以,面向对象的设计思想是抽象出Class
,根据Class
创建Instance
。
面向对象的抽象程度又比函数要高,因为一个Class
既包含数据,又包含操作数据的方法。
大概是这样的:
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 = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()
类和实例
类是抽象的模板,实例是根据类这一模板创建出来的具体对象,每个对象都拥有相同的方法,但不一定拥有相同的数据。
class Student(object):
#用class定义一个类,原则上类名首字母大写。
#括号里面的东西代表继承自哪个类。
pass
vth = Student()#创建Student类的实例
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__
方法,在创建实例的时候,就把name
,score
等属性绑上去:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
注意到__init__
方法的第一个参数永远是self
,表示创建的实例本身,因此,在__init__
方法内部,就可以把各种属性绑定到self
,因为self
就指向创建的实例本身。
有了__init__
方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__
方法匹配的参数,但self
不需要传,Python
解释器自己会把实例变量传进去。
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self
,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别。
数据封装
面向对象编程的一个重要特点就是数据封装。
可以直接在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))
bart.print_score()#调用
python的面向对象编程可以随意的给对象指定属性=-= 比如这样
class Student(object):
#用class定义一个类,原则上类名首字母大写。
#括号里面的东西代表继承自哪个类。
pass
vth = Student()#创建Student类的实例
ygm = Student()
vth.age = 8
print(vth.age)
print(ygm.age)#注意,这儿运行不出来哦
访问限制
在Class
内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__
,在Python
中,实例的变量名如果以__
开头,就变成了一个私有变量(private
),只有内部可以访问,外部不能访问。
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
但是如果外部代码要获取name
和score
怎么办?可以给Student
类增加get_name
和get_score
这样的方法:
def get_name(self):
return self.__name
def get_score(self):
return self.__score
有些时候,你会看到以一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,看到_
就像看到别人的女朋友,虽然这人还没结婚,但是也不能随便泡人马子=-=
双下划线开头的实例变量也不是一定不能访问,不能直接访问__name
是因为Python
解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量。bart._Student__name
继承和多态
在OOP
程序设计中,当我们定义一个class
的时候,可以从某个现有的class
继承,新的class
称为子类(Subclass
),而被继承的class
称为基类、父类或超类(Base class、Super class
)。
继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial
实现了run()
方法,因此,Dog
和Cat
作为它的子类,什么事也没干,就自动拥有了run()
方法。
继承的第二个好处需要我们对代码做一点改进。你看到了,无论是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...')
当子类和父类都存在相同的run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。这样,我们就获得了继承的另一个好处:多态。
Dog
是从Animal
继承下来的,当我们创建了一个Dog
的实例c
时,我们认为c
的数据类型是Dog
没错,但c
同时也是Animal
也没错,Dog
本来就是Animal
的一种!
所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行。Dog
可以看成Animal
,但Animal
不可以看成Dog
。狗是动物,动物不一定是狗。
多态的好处就是,当我们需要传入Dog
、Cat
、Tortoise
……时,我们只需要接收Animal
类型就可以了,因为Dog
、Cat
、Tortoise
……都是Animal
类型,然后,按照Animal
类型进行操作即可。由于Animal
类型有run()
方法,因此,传入的任意类型,只要是Animal
类或者子类,就会自动调用实际类型的run()
方法,这就是多态的意思。
多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal
的子类时,只要确保run()
方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Animal
子类;
对修改封闭:不需要修改依赖Animal
类型的run_twice()
等函数。
BUT!!!
对于静态语言(例如Java
)来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者它的子类,否则,将无法调用run()
方法。
对于Python
这样的动态语言来说,则不一定需要传入Animal
类型。我们只需要保证传入的对象有一个run()
方法就可以了
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。
获取对象信息
判断对象的类型,用type()
函数。
class Student(object):
pass
vth = Student()
print(type(vth))
如果一个变量指向函数或者类,也可以用type()
判断。
判断基本数据类型可以直接写int
,str
等,但如果要判断一个对象是否是函数怎么办?可以使用types
模块中定义的常量:
import types
def f():
pass
type(abs)==types.BuiltinFunctionType
type(f)==types.FunctionType
type(lambda x: x)==types.LambdaType
type((x for x in range(10)))==types.GeneratorType
我们要判断class
的类型,可以使用isinstance()
函数。
isinstance(h, Husky)
isinstance()
判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
能用type()
判断的基本类型也可以用isinstance()
判断,所以我们判断类型时首选用isinstance()
还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list
或者tuple
isinstance([1, 2, 3], (list, tuple))#这俩都会返回True
isinstance((1, 2, 3), (list, tuple))
dir()
函数返回一个包含字符串的List
.可以获得一个对象的所有属性和方法。
如果你调用len()
函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的__len__()
方法。
len('dsa')#这两种写法是等价的。
'dsa'.__len__()
配合getattr()、setattr()
以及hasattr()
,我们可以直接操作一个对象的状态。
hasattr(obj, 'x') # 有属性'x'吗?
setattr(obj, 'y', 19) # 设置一个属性'y'
getattr(obj, 'y') # 获取属性'y'
如果试图获取不存在的属性,会抛出AttributeError
的错误。
可以传入一个default
参数,如果属性不存在,就返回默认值。
getattr(obj, 'z', 404)
只有在不知道对象信息的时候,我们才会去获取对象信息。如果知道,就直接用。
那学hasattr()
等有什么用呢?一个正确的用法的例子如下:
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None
假设我们希望从文件流fp
中读取图像,我们首先要判断该fp
对象是否存在read
方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()
就派上了用场。请注意,在Python
这类动态语言中,根据鸭子类型,有read()
方法,不代表该fp
对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()
方法返回的是有效的图像数据,就不影响读取图像的功能。
实例属性和类属性
定义一个类属性之后,属性归类所有,但所有实例都可以访问。
类访问类属性: 类名.属性
如果是实例.属性
,就会先看该实例有没有该属性,如果没有,就返回类属性,如果有,就返回实例本身的属性。
- 实例属性属于各个实例所有,互不干扰;
- 类属性属于类所有,所有实例共享一个属性;
- 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。