文章目录
- Object Oriented Programming(面向对象编程),简称OOP,OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数;
- 面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行,为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度;
- 而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接受其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递;
- 在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
;面向对象的抽象程度又比函数要高,因为一个Class
既包含数据,又包含操作数据的方法。 - 数据封装、继承和多态是面向对象的三大特点;
1. 类(class)和实例(instance)
- 类和实例是面向对象中最重要的概念,类是抽象出来的模板,比如Student类,而实例是根据类创建出来的一个个具体的’对象’,每个对象都拥有相同的方法,但各自的数据可能不同;
- 在Python中,定义类是通过
class
关键字,class
后面紧接的是类名,即Student
,类名通常以大写字母开头,紧接着是(object)
,表示该类是从那个类继承下来的,通常没有合适的继承类,就使用object
类,这是所有类最终都会继承的类
class Student(object):
pass
- 定义好了
Student
类,就可以根据Student
类创建出Student
的实例,创建实例是通过类名+()实现的; - 变量
bart
指向的就是一个Student
的实例,后面的0x000002A5BF8D7E80
是内存地址,每个object的地址都不一样,而Student
本身就是一个类;
>>> bart = Student()
>>> bart
<Student object at 0x000002A5BF8D7E80>
>>> Student
<class 'Student'>
- 也可以自由的给一个实例变量绑定属性,如,给实例
bart
绑定一个name
属性:
#
>>> bart.name = 'ABC'
>>> bart.name
'ABC'
# 通过定义一个特殊的__init__方法,在创建实例时,就把name, score等属性绑上去
class Student(object):
def __init__(self, name, score): # __init__方法的第一个参数永远是self,表示创建的实例本身
self.name = name # 因此在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身
self.score = score
# 有了__init__方法,在创建实例的时候,必须传入与__init__方法匹配的参数,但self不用传,Python解释器会自己把实例变量传进去
>>> bart = Student('ABC', 59)
>>> bart.name
'ABC'
>>> bart.score
59
- 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量
self
,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。
1.1 数据封装
- 在上面的Student类中,每个实例拥有各自的
name
和score
这些数据,可以通过函数来访问这些数据:
# 打印一个学生的成绩:
>>> def print_score(std):
... print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
ABC: 59
- 既然
Student
实例本身就拥有这些数据,就没必要在外面的函数访问,可以直接在Student
类内部定义访问数据的函数,这样就把数据给封装起来了,这些封装数据的函数和Student
类本身是关联的,我们称之为类的方法:
# 在pycharm中将此程序打包为class_instance.py
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))
# 先将类导入
>>> from class_instance import Student
# 要调用一个方法,只需要在实例上直接调用,除了self不用传递,其他参数正常传入:
>>> bart = Student('ABC', 59) # 创建一个实例
>>> bart.print_score()
ABC: 59
- 从外部看
Student
类,就只需知道,创建实例需要给出name
和score
,而如何打印,都是Student
类内部定义的,这样被封装起来的,数据和逻辑,调用就非常简单; - 同时另外一个好处就是可以给
Student
类增加新的方法,比如get_grade
:
# 在pycharm中将程序存为class_instance.py
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 90:
return 'A'
elif self.score >= 60:
return 'B'
else:
return 'C'
# 在命令行中导入Student类
>>> from class_instance import Student
>>> lisa = Student('Lisa', 90)
>>> bart = Student('Bart', 59)
>>> print(lisa.name, lisa.get_grade())
>>> print(bart.name, bart.get_grade())
Lisa A
Bart C
- 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然他们都是同一个类的不同实例,但拥有的变量名称都可能不同:
>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8
>>> bart.age
8
>>> lisa.age
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'
2. 访问限制
- 从前面Student类的定义来看,外部代码依然可以自由的修改一个实例的
name
和score
属性:
from class_instance import Student
bart = Student('ABC', 59)
bart.score
59
bart.score = 99
bart.score
99
- 在属性名称前加两个下划线
__
,此实例变量就变成了私有变量(private),只有内部可以访问,外部不可以访问:
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('ABC', 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
方法:
calss Student(object):
...
def set_score(self, score):
self.score = score
- 那原先那种
bart.score = 99
也可以修改,为什么要这样操作,这是因为在方法中,可以对参数做检查,避免传入无效的参数:
class Student(object):
...
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')
- 注意在Python中,变量名类似
__xxx__
的,也就是以双下划线开头,结尾的,是特殊变量,不是private变量,可以直接访问,所以,不能用这种命名变量名; - 有些时候,看到以一个下划线开头的实例变量名,如
_name
,这种的实例变量虽然是可以外部访问,但是一般是视为私有变量,不要随意访问; - 双下划綫开头的实例变量是不是一定不能在外部访问呢?其实也不是,不能直接访问是因为python解释器对外把
__name
变量变改成了_Student__name
,所以,可以通过_Student__name
来访问__name
变量;
>>> bart._Student__name
'ABC'
- 但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把
__name
改成不同的变量名;
3. 继承和多态
3.1 继承
- 在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`就是他的子类;
- 继承最大的好处就是子类获得了父类的全部功能,由于
Animal
实现了run()
方法,因此,作为子类的Dog
和Cat
就自动拥有了run()
方法:
dog = Dog()
dog.run()
cat = Cat()
cat.run()
# 运行结果:
Animal is running...
Animal is running...
- 同时,可以对子类增加一些方法,如:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
- 继承的另外一个好处需要我们对代码做一下改进:
# 当run()的时候,分别显示Dog is running...和Cat is running...
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...
- 当子类和父类都存在相同的
running
方法时,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
,这样我们获得了继承的另外一个好处:多态
3.2 多态
- 首先对数据类型做一点说明:当我们定义一个class时,实际上就定义了一种数据类型,我们自定义的数据类型和Python自带的数据类型没什么两样:
a = list() # a 是list类型
b = Animal() # b 是Animal类型
c = Dog() # c 是Dog类型
# 用isinstance()判断变量是某个类型:
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
# 试试这个
>>> isinstance(c, Animal)
True
- 这样看来,
c
不仅是Dog
,还是Animal
,这是因为Dog是从Animal继承下来的。所以在继承关系中,如果一个实例的数据类型是某个子类,那他的数据类型也可以被看做父类,但是反过来不行:
>>> b = Animal()
>>> isinstance(b, Dog)
False
- 再通过一个函数来理解多态的好处:
# 编写一个函数,这个函数接受一个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()
方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则: -
对扩展开放:允许新增
Animal
子类; -
对修改封闭:不需要修改依赖
Animal
类型的run_twice()
等函数。 -
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类
object
,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:
3.3 静态语言VS动态语言
- 对于静态语言(java)来说,如果传入
Animal
类型,则传入的对象必须是Animal
类型或者他的子类,否则,无法调用run()
方法; - 对于像python这种的动态语言,则不一定需要传入
Animal
类型,我们只需要保证传入的对象有一个run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
- Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个
read()
方法,返回其内容。但是,许多对象,只要有read()
方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()
方法的对象。
4. 获取对象信息
- 当我们拿到一个对象的引用时,如何知道这个对象是什么类型,有哪些方法呢?
4.1 使用type()
# 使用type()函数判断基本类型
type(123)
<class 'int'>
type('str')
<class 'str'>
type(None)
<class 'NoneType'>
# 如果一个变量指向函数或者类,同样可以判断
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
# type()函数返回对应的class类型,如:在if语句中判断,比较两个变量的type类型是否相同:
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False
# 判断基本数据类型可以直接写int,str等,但如果判断一个对象是否是函数时,就需要用到types模块中定义的常量:
>>> 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
4.2 使用isinstance()
# 对class的继承关系来说,使用type()很不方便,所以我们可以使用isinstance()函数:
# 回顾上次的例子,如果继承关系是:
object -> Animal -> Dog -> Husky
>>> a = Animal() # 先创建3种类型的对象
>>> b = Dog()
>>> c = Husky()
>>> isinstance(c, Husky)
True
>>> isinstance(c, Dog) # c自身虽是Husky类型,但是Husky是从Dog继承下来的,所以c也是Dog类型,
True
>>> isinstance(c, Animal) # 换句话说,isinstance判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上
True
# 能用type()判断的基本类型也可用isinstance()判断:
>>> isinstance('a', str)
True
# 同时可以判断一个变量是否是某些类型中的一种:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance([1, 2, 3], list)
True
>>> isinstance((1, 2, 3), (list, tuple))
True
>>> isinstance((1, 2, 3), tuple)
True
- 总是优先使用isinstance()判断类型,可以将指定类型及其子类‘一网打尽’
4.3 使用dir()
# 获得一个对象的所有属性,使用dir()函数,返回一个包含字符串的list:
>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] # 类似__xxx__的属性和方法都是有特殊用途的,比如__len__方法返回长度;还有lower()返回小写的字符串;
# 在python中你调用len()函数去获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法:
>>> len('ABC') # 这两种方法是等价的
3
>>> 'ABC'.__len__()
3
# 自己写的类,如果想用len(myObj),就自己写个__len__()方法:
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog = MyDog()
>>> len(dog)
100
# 仅仅把属性和方法列出来是不够的,配合`getattr()`、`setattr()`、hasattr(),我们可以直接操作一个对象的状态:
>>> 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
>>> obj.y
19
>>> getattr(obj, 'y') # 获取属性‘y’
19
>>> getattr(obj, 'z') # 获取不存在的属性,会抛出错误:
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
>>> getattr(obj, 'z', 404) # 传入一个default参数,若属性不存在,返回默认值
404
>>> hasattr(obj, 'power') # 获得对象的方法,有属性‘power’吗
True
>>> getattr(obj, 'power') # 获取属性‘power’
<bound method MyObject.power of <__main__.MyObject object at 0x00000281D577BB70>>
>>> fn = getattr(obj, 'power') # 获取属性‘power’并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x00000281D577BB70>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81
4.4 小结
- 注意只有在不知道对象信息的时候,我们才回去获得对象信息:
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对象是否有read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。
hasattr()
就派上了用场。 - 请注意,在Python这类动态语言中,根据鸭子类型,有
read()
方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()
方法返回的是有效的图像数据,就不影响读取图像的功能。
5. 实例属性和类属性
- 因为python是动态语言,所以根据类创建的实例可以任意绑定属性
# 给实例绑定属性的方法时通过实例变量,或通过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'
...
>>> 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
- 从这个例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同名字的名称,访问到的将是类属性;
5.1 练习
- 为了统计学生人数,可以给Student类增加一个类属性,每创建一个实例,该属性自动增加:
class Student(object):
count = 0
def __init__(self, name):
self.name = name
Student.count = Student.count + 1
if Student.count != 0:
print('测试失败!')
else:
bart = Student('Bart')
if Student.count != 1:
print('测试失败!')
else:
lisa = Student('Bart')
if Student.count != 2:
print('测试失败!')
else:
print('Students:', Student.count)
print('测试通过!')
Run
Students: 2
测试通过!