面向对象编程概念(OOP)
面向过程是编年史: 按照时间先后顺序, 一条一条的记录发生的事情.
面向对象是记传史: 按照每个王侯将相的维度, 记录这个人是谁, 有哪些特点, 做过哪些事情
所以史记其实是中国古代的一个非常成功的应用了 OOP 思想的著作
OOP是一种编程思想. 所谓 “思想”, 指的是解决一类问题时,
使用的一系列的方法手段, 一套组合拳.
我们看看这套组合拳都有哪些招式
抽象
抽象是指对现实世界问题和实体的本质表现, 行为, 特征进行建模;
抽象的反义词是 "具体“
面向对象的本质是:
把我们现实生活中的一些东西变成计算机访问代码
这个过程就是抽象过程
举例:
class student 整个学生类,一个学生类想要让其代表一个学生
现实生活中的学生的属性:姓名 年龄 老师 父母 职业
根据实际情况选择我们关注的信息,你就表示什么信息
提取你想要的核心信息
抽象和具体的差异:信息量的多少
抽象的本质, 是抓住我们重点关注的主体, 而忽略一些我们不需要关注的细节;
写程序也是一样, 我们不可能把一个现实事物所有的信息都在程序中表示出来,
而是只表示我们需要用到的
类和实例
实例: 就是对象
- 类是施工图纸, 里面有房子的重要信息(户型, 面积, 朝向, 层高等等).
- 实例是造好的房子, 房子造好了, 才能住进去, 才能娶到媳妇.
如果房子烂尾了, 美好生活就只是在YY~ - 通过同一张图纸可以建造出N个相同格局的房子,
那么这N个实例就都是属于同一个类(回忆下我们的type函数). - 通过同一张图纸可以建造出不同的房子, 每一个房子都有一个房号,
5号楼1单元101这种, 其实就是实例的id(回忆下我们的id函数). - 这N个房子里, 住的人也不相同, 比如101是我住,
隔壁102住的是老王. 这个就是实例的值不同
总结:
对象就是类的实例, 是一个客观事物的抽象.
把多个对象按一定的规则归一归类, 就成为了类.
Python中的对象包含三个部分: id, 类型, 值
封装/接口 (接口:广义的接口)
1.封装描述了对数据/信息进行隐藏的观念, 对象的使用者只能通过接口来访问/修改对象的数据.
2.封装的好处是减轻调用者的使用负担, 调用者只需要知道接口如何使用, 而不需要知道对象的具体实现.
3.封装这个概念当然不是只存在于面向对象思想中. 回忆计算机网络协议栈的分层结构, 也是为了达到封装的效果.
只要通话双方能听懂对方的语言, 就可以正常使用电话通话. 而不需要理解电话的任何实现细节
封装涉及两种不同的角色:
1.类的实现者
2.类的调用者(使用者)
封装的含义:
让调用者不必知道实现者是实现类的细节,
只要知道类怎么使用就行了(类提供的接口)
为什么要封装? <<代码大全>>
<<代码大全>>开篇就在讨论这件事情,
开篇第一章:软件开发的本质
本质叫管理程序的复杂程度,不能让程序太复杂,
复杂-> 没办法维护->代码写不下去了->就会出现问题
如何管理代码的复杂程度?
封装
把很多不必要的细节隐藏起来,然后让使用者更加简单
组合/包含
多个小的类组合成一个大的类, 来解决一个相对复杂的问题.
组合表示 has-a 语义
例如, 我们用代码表示一个 “学校” , 那么可以先创建一个学校类.
学校类中又包含了教师类, 学生类, 行政人员类, 后勤人员类等.
组合的目的是代码重用
包含:(has-a)
实现一个类的过程中,用其他类的示例来作为这个类的成员
继承:(is-a)
继承表示的是一个什么东西,包含表示的是有一个什么东西
派生/继承-
1.基于一个现有的类, 通过继承这个类创建一个新的类.
2.如果基于类A用继承的方式创建了类B,
我们就将这个过程成为类A派生出了类B.
类A称为父类(或超类), 类B称为子类.
3.子类对象包含了父类对象的所有属性和操作.
4.继承表示了 is-a 这样的语义
继承的目的还是代码重用
多态
多态是OOP中比较难理解的概念, 我们先看一个 花木兰替父从军 的例子
花木兰替父(父亲名叫花弧)从军, 我们先创建这么两个类
父类对象:
数据:
姓名: 花弧
性别: 男
年龄: 60
方法
冲锋杀敌(花弧精通降龙十八掌)
子类对象:
数据:
姓名: 花木兰
性别: 女
年龄: 28
方法
冲锋杀敌(木兰精通打狗棒法)
涂脂抹粉(这个是子类具备的方法)
1.木兰先去军营报道, 将军的花名册上写着花弧的名字. 相当于是一个父类对象的引用. 而木兰替父从军, 相当
于父类引用指向一个子类的对象.
2.两军交战了, 将军发号施令: 花弧听令, 冲锋杀敌! 将军是对象的使用者, 将军通过父类的引用, 让子类执
行"冲锋杀敌"这个动作, 实际上阵杀敌的是木兰而不是花弧, 因此木兰肯定是按照自己熟悉的武功招式来杀敌.
3.将军并不知道 “花弧” 这个引用是父类的还是子类的, 将军只知道这个类有冲锋杀敌的方法, 而不知道这个
“花弧” 其实还有涂脂抹粉的动作
4.战争结束, 木兰衣锦还乡, 这时候亲朋好友都知道木兰的底细, 使用"花木兰"这个子类对象的引用来称呼. 这
时, 木兰就可以涂脂抹粉了
总结一下:
1.本质上讲 多态是在运行时能够自动的识别出对象的类型, 并使用对应类型的方法来完成一定的动作.
2.多态也是一种 “信息隐藏”, 让调用者了解的信息越少, 那么使用起来负担就越低.
3.刚才我们说的封装, 只是让调用者不知道这个类里面的实现; 而我们的多态, 是让调用者连这个类究竟是哪
个类都不需要关心
1.多态对我们简化代码有非常大的意义. 我们先看一段C语言代码. 这个代码表示的是某品牌路由器检查所有网口状态的代码.
2.每一种型号的路由器的端口数目/端口类型可能都不同, 因此每一个或几个这样的类型必须提供一个专门的函数
switch(board_type) {
case Board2304:
case Board2306:
CheckPortStatus1();
break;
case Board3300:
case Board3302:
CheckPortSatus2();
break;
case Board5300:
CheckPortStatus3();
break;
case Board6300:
CheckPortStatus4();
break;
...
}
每新增一种型号, 都意味着需要将这些 switch-case 语句都修改一遍. 而且每个地方都要判定所有的板类型.
如果使用多态机制, 每一个新的类型就新增一种子类, 只需要在需要的地方使用父类的对象调用即可
board.CheckPortStatus();
自省/反射
自省和反射是一个意思. 顾名思义, 就是认清自己是谁. 指对象在程序运行时能获取到一些自己的信息
1.dir/id/type/name/doc 这些内置属性和方法就是自省的具体体现.
2.其实C++中也有typeid这样的方法来进行一部分的"自省"的功能. 只不过是和C++的设计理念背道而驰, 因
此不推荐使用
回忆我们之前的dir函数
def Hello():
print('hello')
print(dir(Hello))
# 执行结果
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__',
'__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__',
'__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure',
'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
print(Hello.func_name)
# 执行结果
Hello
dir 可以把一个对象都包含哪些信息,所有的主要特征,都能获取到
属性和方法
创建类
使用class关键字创建一个类. 创建类的语法和创建函数很像
创建一个函数
def FunctionName(arg):
'function doc string' # 文档字符串
function_suite # 函数体
创建一个类
class ClassName(object):
'class doc string' # 文档字符串
class_suite # 类体
1.创建一个类, 实际上是创建了一个自定制的类型. 其实Python中, 类和类型是统一的. 而不像很多其他编程
语言那样, 区分内置类型和"类类型".
2.类的定义语句一般出现在模块的顶级缩进; 但是Python并不强制要求. 类的定义也可以出现在类/函数, 或者
其他语句块中. 当然作用域也就不一样了
延伸:
创建一个列表
a = []
等价于:
a = list()
这里的list也是一个类,通过这样的方式我们可以创建列表
这个类的示例,也就是一个列表对象
python内置垃圾回收机制,所以创建完一个对象之后
不用了也不用手动释放,都会有解释器自己回收
()你当前的这个类继承自那个副类
也可以不写这个括号
如果你不写,默认继承:object类
python 支持多继承所以括号内可以几个,用逗号隔开
类里面要有的东西:
属性(成员变量)
方法(成员函数)
注意:
类里面的def 函数的参数必须带一个self
并且 self 是第一个参数
可以写成this,但是不推荐
class Test:
"""
这是一个用来测试的类
"""
def Print(self):
"""
这是一个方法(java) 成员函数(C++) 实例方法(python)
self必须是第一个参数,表示当前这个实例的引用,类似于this
"""
print('hehe')
如何创建对象
Test 表示类名,加上一个括号表示创建一个具体的实例
t 实例的对象的引用
python中不用写new
t = Test() #常见
t.Print()
hehe
t 就是实例,这个实例我们默认传到print这个第一个参数里面
很少写成下面这样
t.Print() 等价于 Test.Print(t)
将t当作 self 这个参数 传递进去
class Test:
"""
这是一个用来测试的类
"""
# 实例属性在不同方法之间都可以分别创建
def Init(self):
self.x = 10
def Print(self):
"""
这是一个方法(java) 成员函数(C++) 实例方法(python)
self必须是第一个参数,表示当前这个示例的引用,类似于this
"""
# 实例属性必须在示例方法中创建
print('hehe',self.x)
t = Test() #常见
t.Init()
t.Print()
hehe 10
如果 t.Init() 没有调用
报错,x 找不到
问题:我们每次都要调用这个Init()会不会太复杂
类似于构造函数,我们创建一个特殊的函数
就会在类被实例化过程中,自动被调用
修改后:
class Test:
def __init__(self):
"""
通常我们把一个类的实例属性都写到__init__里面
"""
self.x = 10
def Print(self):
print('hehe',self.x)
t = Test()
t.Print()
hehe 10
和其他函数一样,这个也可以传参数
同理支持函数的一切功能
class Test:
def __init__(self,num):
"""
通常我们把一个类的实例属性都写到__init__里面
"""
self.x = num
def Print(self):
print('hehe',self.x)
t = Test(11)
t.Print()
hehe 11
除了成员变量和成员函数,还有一些情况
静态成员函数和静态成员变量
class Test:
# 构建一个静态的成员变量 静态属性,类属性
val = 10
def __init__(self,num):
self.x = num
def Print(self):
print('hehe',self.x)
print(Test.val)
10
class Test:
val = 10
def __init__(self,num):
self.x = num
def Print(self):
print('hehe',self.x)
def StaticPrint():
print('hehe')
StaticPrint = staticmethod(StaticPrint)
Test.StaticPrint()
hehe
staticmethod 是一个函数,本质上是一个类,视为一个函数
类似于放函数
修改,美化:
class Test:
val = 10
def __init__(self,num):
self.x = num
def Print(self):
print('hehe',self.x)
# 装饰器
@staticmethod
def StaticPrint():
print('hehe')
Test.StaticPrint()
新问题: 我们知道静态函数可以直接访问静态变量–不能
class Test:
val = 10
def __init__(self,num):
self.x = num
def Print(self):
print('hehe',self.x)
@staticmethod
def StaticPrint():
print('hehe',val)
Test.StaticPrint()
NameError: name 'val' is not defined
修改:
class Test:
val = 10
def __init__(self,num):
self.x = num
def Print(self):
print('hehe',self.x)
@staticmethod
def StaticPrint():
"""
静态方法 != C++静态成员函数
此时无法访问静态成员变量(类属性)
"""
print('hehe')
# 能将类的实例扩展过来
@classmethod
def ClassPrint(cls):
"""
类方法 == C++静态成员函数
cls表示当前类
"""
print('hehe',cls.val)
print(Test.val)
Test.ClassPrint()
10
hehe 10
总结:
python进行定义类的时候要考虑:
1.实例属性(非静态成员变量)
2.实例方法(非静态成员函数)
3.类属性(静态成员变量)
4.类方法(静态成员函数)
5.静态方法
属性的访问权限
学过C++/Java的同学们知道, 使用关键字public/private/protected等关键字, 可以对属性的访问权限进行控制. 这也是
“封装” 思想的一种具体体现.
Python中使用双下划线前缀( __ )来表示私有成员
class Test:
def __init__(self):
self.__x = 10
t = Test()
print(t.__x)
AttributeError: 'Test' object has no attribute '__x'
访问不到,为什么?
他被当成了私有,只有你内部才能使用
类之间的关系
在设计一个程序时, 需要设计多个有一定关系的类.
而这种类和类之间的关系, 最主要有两种. 组合和继承
组合
就是让一个类里面包含其他类的实例
class Student:
pass
class Teacher:
pass
组合
class School:
def __init__(self) :
self.students = [Student(),Student()]
self.teachers = [Teacher(),Teacher()]
继承:
class Cat:
def __init__(self):
self.name = '嘿 '
self.type = '猫'
def Speak(self):
print('喵')
class AshortCat(Cat):
# 子类中如果出现和父类同名的函数或者变量
# 就会相互覆盖,尤其是需要注意 __init__
# def __init__(self):
# Cat.__init__(self)
# 或者:
def __init__(self):
super().__init__(self)
cat = AshortCat()
cat.Speak()
print(cat.name)
# 喵
# 嘿
理解Python的多态
Python中的多态和继承没有直接关系
C++里面的多态,你必须要有一个副类的指针,或者副类的引用
他指向一个子类的实例,然后调用副类里面的某个虚函数,
然后他自动调用到子类的方法
def add(x,y):
return x+y
print(add(10,20))
print(add('hallo','world'))
print(add([1,2,3,],[4,5,6]))
30
halloworld
[1, 2, 3, 4, 5, 6]
多态本质是封装的更进一步
封装指的是调用你这不必知道使用的对象内部的细节
多态值得是调用者连这个使用的类是啥都不需要知道,
只要知道这个类支持某个操作就行了
类和实例的常用内建函数
issubclass
判定一个类是不是另外一个类的子类.
class Parent(object):
pass
class Child(Parent):
pass
print issubclass(Child, Parent)
# 执行结果
True
isinstance
判定一个对象是不是一个类的实例.
class C(object):
pass
c = C()
print(isinstance(c, C))
# 执行结果
True
hasattr, getattr, setattr, delattr
用来对一个类/对象的属性进行操作和判定
class Test:
val = 10
print(getattr(Test,'val')) #10
删除
print(delattr(Test,'val')) #10
print(Test.val) #AttributeError: type object 'Test' has no attribute 'val'
class C(object):
x = 100
print hasattr(C, 'x')
print getattr(C, 'x')
setattr(C, 'x', 200)
print(C.x)
delattr(C, 'x')
# 执行结果
True
100
200
dir
查看类/对象的所有属性和方法
class C(object):
pass
print(dir(C))
# 执行结果
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__',
'__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
vars
和dir类似. dir只是返回了有哪些属性, 但是没显示属性的值. vars将值也取了出来
class C(object):
pass
print vars(C)
# 执行结果
{'__dict__': <attribute '__dict__' of 'C' objects>, '__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc_None}
super
获取到当前类/对象的父类
super本质也是一个工厂函数, 创建了一个父类的对象
使用特殊方法定制类
特殊方法的作用
我们前面学的 init 只是其中一个特殊方法. 其实类还有很多特殊方法, 通过这些特殊方法, 可以完成
运算符重载
模拟标准类型
class Time:
def __init__(self, hour,min):
self.hour = hour
self.min = min
t = Time(13,30)
print(t)
<__main__.Time object at 0x000002790F051760>
打印的是这个对象的他的一些属性信息
修改:
class Time:
def __init__(self, hour,min):
self.hour = hour
self.min = min
# 这个函数返回一个字符串,相当于描述我们这个对象
# 如何被序列化成一个字符串
def __str__(self):
return f"{self.hour}:{self.min}"
t = Time(13,30)
print(t)
13:30
print 这个内建函数先把对象转成字符串
先把对象序列化,转成字符串 然后打印字符串的结果
如果不__str__
就按照默认方法进行打印,打印这个id
例子:
class Time:
def __init__(self, hour,min):
self.hour = hour
self.min = min
def __str__(self):
return f"{self.hour}:{self.min}"
def __len__(self):
return len(self.__str__())
t = Time(13,30)
print(t)
print(len(t))
13:30
5 #13:30的长度
我们以 str 为例写一个简单的代码. 更多的特殊方法, 同学们课后自己学习
class Time(object):
def __init__(self, h, m):
self.hour = h
self.minute = m
def __str__(self):
return '[%02d:%02d]' % (self.hour, self.minute)
t = Time(13, 5)
print(t)
# 执行结果
[13:05]