Python第七章-面向对象高级
面向对象高级
一、 特性
特性是指的property
.
property
这个词的翻译一直都有问题, 很多人把它翻译为属性, 其实是不恰当和不准确的. 在这里翻译成特性是为了和属性区别开来.
属性是指的attribute
, 我们以前学习的实例变量和类变量是attribute
, 所以也可以叫做实例属性和类属性.
property
(特性)到底是个什么东西?
我们前面学习类属性和实例属性的时候知道, 访问他们的时候就可以直接获取到这些属性的值.
而特性可以看成一种特殊的属性, 为什么呢?
但从访问方式来看, 特性和属性看不出来差别, 但是特性实际上会经过计算之后再返回值. 所以每一个特性都始终与一个方法相关联.
1.1 定义特性
定义特性和定义实例方法类似, 只需要另外在方法上面添加一个内置装饰器:@property
访问特性和访问实例变量完全一样, 不需要使用添加括号去调用.
import math class circle: def __init__(self, r): self.r = r @property def area(self): """ 定义特性 这个特性是计算出来圆的面积 :return: """ return math.pi * (self.r ** 2) c = circle(10) print(c.area)
很明显, 特性背后的本质是一个方法的存在, 所以你不可能在外面去修改这个特性的值!
试图修改特性的值只会抛出一个异常.
c.area = 100
1.2 使用特性的设计哲学
这种特性使用方式遵循所谓的 统一访问原则.
实际上, 定义一个类总是保持接口的统一总是好的.
有了特性, 把访问属性和访问方法统一了, 都像在访问属性一样, 省得去考虑到底什么时候需要添加括号,什么时候不用添加括号.
1.3 特性的拦截操作
python 还提供了设置和删除属性.
通过给方法添加其他内置装饰器来实现
设置:@特性名.setter
删除:@特性名.deleter
class student: def __init__(self, name): self._name = name # name 是特性了, 所以用实例变量存储特性的值的是换个变量名!!! @property def name(self): return self._name @name.setter def name(self, name): if type(name) is str and len(name) > 2: self._name = name else: print("你提供的值" + str(name) + "不合法!") @name.deleter def name(self): print("对不起, name 不允许删除") s = student("李四") print(s.name) s.name = "彩霞" print(s.name) s.name = "张三" print(s.name) del s.name
二、三大特性之一-封装性
面向对象的三大特征:封装, 继承, 多态
2.1什么是封装性
1.封装是面向对象编程的一大特点 2.面向对象编程的第一步,就是讲属性和方法封装到一个抽象的类中 3.外界使用类创建对象,然后让对象调用方法 4.对象方法的细节都被封装在类的内部
在类中定义属性, 定义方法就是在封装数据和代码.
2.2 私有化属性
首先先明确一点, python 不能真正的对属性(和方法)进行私有, 因为 python 没有想 java 那样的private
可用.
python 提供的"私有", 是为了怕在编程的过程中对对象属性不小心"误伤"提供的一种保护机制! 这种级别的私有稍微只要知道了规则, 是很容易访问到所谓的私有属性或方法的.
2.2.1 为什么需要私有
封装和保护数据的需要.
默认情况下, 类的所有属性和方法都是公共的, 也就意味着对他们的访问没有做任何的限制.
意味着, 在基类中定义的所有内容都可以都会被派生类继承, 并可从派生类内部进行访问.
在面向对象的应用程序设计中, 我们通常不希望这种行为, 因为他们暴露基类的内部实现, 可能导致派生类中的使用的私有名称与基类中使用的相同的私有名称发生冲突.
属性或方法私有后就可以避免这种问题!
2.2.2 "私有"机制
为了解决前面说的问题, python 提供了一种叫做名称改写(name mangling)的机制
如果给属性或者方法命名的时候, 使用两个下划线开头(__
)的属性和方法名会自动变形为_类名__方法名
, 这样就避免了在基础中命名冲突的问题.
class student: def __init__(self): pass def __say(self): print("我是私有方法你信吗?") s = student() s.__say() # 双下划线开头的方法已经被形变, 此处访问不到
s._student__say()
2.2.3 不是真正的私有
尽管这种方案隐藏了数据, 但是并没有提供严格的机制来限制对私有属性和方法的访问.
虽然这种机制好像多了一层处理, 但是这种变形是发生在类的定义期间, 并不会在方法执行期间发生, 所以并没有添加额外的开销.
2.2.4 不同的声音
有部分人认为这种使用双__
的机制好辣鸡, 写两个下划线影响效率. 他们使用一个下划线, 并把这个作为一个约定.
好吧, 你喜欢哪种呢?
三、面向对象三大特性-继承性(inheritance)
这一节我们来学习面向的对象的再一个特征: 继承
3.1继承性的概念
继承(extends
)是创建新类的一种机制, 目的是专门使用和修改先有类的行为.
原有类称为超类(super class
), 基类(base class
)或父类.
新类称为子类或派生类.
通过继承创建类时, 所创建的类将继承其基类所有的属性和方法, 派生类也可以重新定义任何这些属性和方法, 并添加自己的新属性和方法
3.2 继承性的意义
继承实现代码的重用,相同的代码不需要重复的编写
从子类的角度来看,避免了重复的代码。(子类继承父类后,子类可以直接使用父类的属性和方法)
从父类的角度来看,子类扩展了父类的功能。(因为子类也是一个特殊的父类)
- 子类可以直接访问父类的属性和方法。
- 子类可以新增自己的属性和方法。
- 子类可以重写父类的方法。
3.3 继承的语法和具体实现
继承的语法如下:
class 父类名: pass class 子类名(父类名): pass
3.3.1最简单的继承
python 的继承是在类名的后面添加括号, 然后在括号中声明要继承的父类.
class father: def speak(self): print("我是父类中的 speak 方法") # son继承 father 类 class son(father): pass s = son() s.speak()
说明:
- 从字面上我们看到
son
没有定义任何的方法, 但是由于son
继承自father
, 则son
会继承father
的所有属性和方法 - 调用方法时, 方法的查找规则: 先在当前类中查找, 当前类找不到想要的方法, 则去父类中查找, 还找不到然后继续向上查找. 一旦找到则立即执行. 如果找到最顶层还找不到, 则会抛出异常
示例代码
# 创建人类 class person: # 定义吃东西方法 def eat(self): print("吃窝窝头。。") # 定义睡觉方法 def sleep(self): print("睡着啦。。") # 创建学生类 class student(person): # 子类新增方法:学习 def study(self): print("学生学习啦。。。把你爸乐坏了。。。。。") # 创建父类对象,访问父类的方法 zhangsan = person(); zhangsan.eat() zhangsan.sleep() # 创建子类对象,访问父类的方法和子类的方法 ergou = student(); ergou.eat() # 访问父类的方法 ergou.sleep() # 访问父类的方法 ergou.study() # 访问子类的新增方法
3.3.2 继承中的__init__()
的调用规则
如果子类没有手动__init__()
方法, 则 python 自动调用子类的__init__()
的时候, 也会自动的调用基类的__init()__
方法.
class father: def __init__(self): print("基类的 init ") # son继承 father 类 class son(father): def speak(self): pass s = son()
如果子类手动添加了__init__()
, 则 python 不会再自动的去调用基类的__init__()
class father: def __init__(self): print("基类的 init ") # son继承 father 类 class son(father): def __init__(self): print("子类的 init ") def speak(self): pass s = son()
如果想通过基类初始化一些数据, 则必须显示的调用这个方法, 调用语法是:基类名.__init__(self, 参数...)
class father: def __init__(self, name): print("基类的 init ") self.name = name def speak(self): print("我是父类中的 speak 方法" + self.name) # son继承 father 类 class son(father): def __init__(self, name, age): # name 属性的初始化应该交给基类去完成, 手动调用基类的方法. 一般放在首行 father.__init__(self, name) # 调动指定类的方法, 并手动绑定这个方法的 self print("子类的 init ") self.age = age s = son("李四", 20) s.speak() print(s.name) print(s.age)
3.4方法的重写(override)
3.4.1重写的概念
我们已经了解了调用方法时候的查找规则, 先在子类中查找, 子类查找不到再去父类中查找.
如果父类的方法不满足子类的需求, 利用这个查找规则, 我们就可以在子类中添加一个与父类的一样的方法, 那么以后就会直接执行子类的方法, 而不会再去父类中查找.
这就叫方法的覆写.(override
)
>重写,就是子类将父类已有的方法重新实现。
父类封装的方法,不能满足子类的需求,子类可以重写父类的方法。在调用时,调用的是重写的方法,而不会调用父类封装的方法。
3.4.2重写父类方法的两种情况
-
覆盖父类的方法
父类的方法实现和子类的方法实现,完全不同,子类可以重新编写父类的方法实现。
具体的实现方式,就相当于在子类中定义了一个和父类同名的方法并且实现
-
对父类方法进行扩展
子类的方法实现中包含父类的方法实现。(也就是说,父类原本封装的方法实现是子类方法的一部分)。
在子类中重写父类的方法
在需要的位置使用
super().父类方法
来调用父类的方法代码其他的位置针对子类的需求,编写子类特有的代码实现。
如果在覆写的方法中, 子类还需要执行父类的方法, 则可以手动调用父类的方法:父类名.方法(self, 参数...)
class father: def __init__(self, name): self.name = name def speak(self): print("我是父类中的 speak 方法" + self.name) # son继承 father 类 class son(father): def __init__(self, name, age): father.__init__(self, name) self.age = age # 子类中覆写了父类的方法 def speak(self): father.speak(self) print("我是子类的 speak 方法" + self.name + " 年龄:" + str(self.age)) s = son("李四", 20) s.speak()
3.4.3关于super
在python中super是一个特殊的类(python 3.x以后出现)
super()就是使用super类创建出来的对象
最常使用的场景就是在重写父类方法时,调用在父类中封装的方法实现
3.5、父类的私有属性和方法
- 子类对象不能在自己的方法内部,直接访问父类的私有属性或私有方法
- 子类对象可以通过父类的共有方法间接访问到私有属性或私有方法
私有属性和方法是对象的隐私,不对外公开,外界以及子类都不能直接访问
私有属性和方法通常用于做一些内部的事情
3.6、多继承
3.6.1多继承的概念
多继承:子类可以拥有多个父类,并且具有所有父类的属性和方法
比如:孩子会继承自己的父亲和母亲的特性
3.6.2多继承的语法
class 子类名(父类名1, 父类名2...): pass
示例代码:
# 父类a class a: def test1(self): print("a类中的test1方法。。") # 父类b class b: def test2(self): print("b类中的test2方法。。") # 子类c同时继承a和b class c(a,b): pass # 创建c对象 c1 = c() c1.test1() c1.test2()
3.6.3多继承的注意事项
提问:如果不同的父类中存在同名的方法,子类对象在调用方法时,会调用哪一个父类中的方法呢?
开发时,应该尽量避免这种容易产生混淆的情况。如果父类之间存在同名的属性或者方法,应该尽量避免使用多继承
3.6.4 python中的 mro (方法搜索顺序)[扩展]
python中针对类提供了一个内置属性,___mro__
可以查看方法搜索顺序
mro是method resolution order,主要用于在多继承时判断方法,属性的调用路径
print(c.__mro__)
输出结果:
(<class '__main__.c'>, <class '__main__.a'>, <class '__main__.b'>, <class 'object'>)
- 在搜索方法时,是按照__mro_-的输出结果从左至右的顺序查找
- 如果当前类中找到方法,就直接执行,不再搜索
- 如果没有找到,就查找下一个类中是否有对应的方法,如果找到,就直接执行,不再搜索
- 如果找到最后一个雷,还没有对应的方法,程序报错
3.6.5 python 中的上帝类型
python 中有个类比较特殊, 所有的类都直接和间接的继承自这个类.
这个类就是:object
. 他是所有类的基类.
如果一个类没有显示的去继承一个类, 则这个类默认就继承object
, 也可以去显示的继承这个类.
class student(object): pass
3.6.6 新式类和旧式(经典)类[扩展]
object
是python为所有对象提供的基类,提供有一些内置的属性和方法,可以使用
dir
函数查看
新式类:以object为基类的类,推荐使用
经典类:不以object为基类的类,不推荐使用
-
在python 3.x中定义类时,如果没有指定父类,会默认使用object作为该类的父类。所以python 3.x中定义的类都是新式类
-
在python 2.x中定义类时,如果没有指定父类,则不会以object作为父类
新式类和经典类在多继承时,会影响到方法的搜索顺序
提示:为了保证编写的代码能够同时在python 2.x 和python 3.x 运行,在定义类的时候,如果没有父类,建议统一继承自object
class 类名(object): pass
四、面向对象三大特性-多态性(polymorphism)
4.1多态性的概念
-
封装性,根据职责将属性和方法封装到一个抽象的类中
定义类的准则
-
继承性,实现代码的重用,相同的代码不需要重复的编写
设计类的技巧
子类针对自己特有的书需求,编写特定的代码
-
多态性,不同的子类对象,调用相同的父类方法,产生不同的执行结果
多态可以增加代码的灵活性
以继承和重写父类方法为前提
是调用方法的技巧,不会影响到类的内部设计
示例代码:
""" 多态性: 继承和重写为前提,创建不同的对象执行的具体方法不同 """ class father(object): def __init__(self, name): print('父类的init方法') self.name = name def say(self): print('父类的say方法' + self.name) # son类继承于father类,python中是类继承于类的 class son(father): def __init__(self, name, age): father.__init__(self, name) self.age = age print('子类的init方法') def say(self): father.say(self) print('子类的say方法:' + self.name + ',' + str(self.age)) # 以下程序会体现出多态性 def mytest(obj): obj.say() f1 = father("张爸爸") mytest(f1) print("---------------") f2 = son("小头儿子",5) mytest(f2)
4.2属性和方法查找顺序
多态性(多态绑定)是在有继承背景情况下使用的一种特性.
是指在不考虑实例背景的情况下使用实例
多态的理论根据是属性和方法的查找过程. 只要使用obj.attr
的方式使用属性和方法, 则查找顺序一定是: 对象本身, 类定义, 基类定义...
关于先查找对象本身的说明: 因为 python 是一门动态语言, 允许我们在代码执行的过程中去动态的给对象添加属性和方法, 所以先从对象本身查找.
class father: def __init__(self, name): self.name = name def speak(self): print("我是父类中的 speak 方法" + self.name) # son继承 father 类 class son(father): def __init__(self, name, age): father.__init__(self, name) self.age = age def speak(self): father.speak(self) print("我是子类的 speak 方法" + self.name + " 年龄:" + str(self.age)) def foo(): print("我是动态添加上去的...") s = son("李四", 20) s.speak = foo s.speak()
4.3 鸭子类型
python 的多态有的时候很多人把它称之为鸭子类型
鸭子类型是指: 看起来像鸭子, 叫起来像鸭子, 走起来像鸭子, 那么它既是鸭子, 你就可以把它当鸭子来用.
换成编程语言的说法就是: 对象属性和方法的时候完成时和类型分开的.
class a: def speak(self): print("a 类中的方法") class b: def speak(self): print("b 类中的方法") def foo(obj): obj.speak() a = a() b = b() foo(a) foo(b)
说明:
-
foo
接受一个对象, 只要这个对象中有speak()
方法, 就可以正常执行, 我们并不关注他的类型 -
a, b
这两个类没有任何的关系, 但是他们都有speak
方法, 所以传递过去都没有任何的问题. - 这就是鸭子模型, 只要你看起来有
speak
就可以了
五、其他
5.1 特殊属性__slot__
5.1.1动态添加属性的问题
通过前面的学习中我们知道, 由于 python 的动态语言的特性, 我们可以动态的给对象添加属性和方法.
但是这种方式添加的属性和方法, 只在当前对象上有用, 在其他对象上是没用.
class a: pass a1 = a() a1.name = "李四" #给 a1 对象添加一个属性 print(a1.name) a2 = a() print(a2.name) # a2中没有 name 属性, 所以抛异常
5.1.2 __slot__
的基本使用
添加属性和方法最好直接在类中添加, 这样所有的对象都可以拥有了.
如果我想避免把某些属性直接添加到实例对象上, 可以使用一个特殊属性:__slot__
类实现.
给__slot__
定义一个元组, 则元组内的属性名允许在实例对象上直接添加, 其他的都不允许.
class a: __slots__ = ("name", ) a1 = a() a1.name = "李四" # 给 a1 对象添加一个属性 name 属性是允许的 print(a1.name) a1.age = 20 # age 不允许, 所以抛异常 print(a1.age)
注意:
- 我们的
__init__()
中添加属性是在self
上添加的, 其实也是直接在对象上添加, 所以没有在元组中的属性名, 也是不允许的. - 对于我们直接在类中添加方法是没有任何的影响的.
class a: __slots__ = ("name",) def __init__(self): self.age = 30 # 也是不允许的 a = a()
5.1.3 继承中的__slot__
__slot__
只对当前类有用, 对他的子类不起作用. 所以子类也要有自己的__slot__
class a: __slots__ = ("name",) def __init__(self): self.age = 30 # 也是不允许的 class b: def __init__(self): self.age = 30 b = b() print(b.age)
5.1.4 __slot__
对性能上的提升
一些人把__slot__
作为一种安全的特性来实现, 然后实际上他对内存和执行速度上的性能优化才是最重要的.
不使用__slot__
, python 使用字典的方式去存储实例数据的, 如果一个程序使用大量的实例, 测内存占用和执行效率都会影响比较大.
使用__slot__
后, python 存储实例数据的时候, 不再使用字典, 而是使用一种更加高效的基于数组的数据结构. 可以显著减少内存占用和执行时间.
5.2 实例的测试类型
任何一个类都可以做为类型!
创建类的实例时, 该实例的类型是这个类本身, 如果有继承存在, 则父类型也是这个实例的类型.
有些情况下, 我们需要先测试实例的类型然后再写相应的代码.
python 支持 2 种测试方法:
5.2.1 内置函数:type(实例)
class a: pass class b(a): pass class c: pass a = a() b = b() c = c() print(type(a)) print(type(b)) print(type(c))
说明:type
返回的是这个实例的所属类的类对象.
补充一下:
其实我们经常接触到的有两种对象:1. 实例对象 2. 类对象
类对象就是: 表示类本身的那个对象!
5.2.2 内置函数:isinstance(实例, 类型)
class a: pass class b(a): pass class c: pass a = a() b = b() c = c() print(isinstance(a, a)) # true print(isinstance(b, b)) # true print(isinstance(b, a)) # true 继承关系 print(isinstance(c, c)) # true print(isinstance(c, a)) # false
说明:
- 这个函数返回的是布尔值, 使用起来方便, 所以以后测试类型建议用这个函数
- 这个函数继承关系也可以测试出来.
b
是b
类创建出来的,b
继承自a
, 所以b
也算是类a
的实例. - 对一个实例也可以同时测试多个类型, 有一个满足就返回
true
,isinstance(实例, (类 a, 类 b, ...)))
. 需要把多个类封装到一个tuple
中.
print(isinstance(c, (a, b, c))) # true
5.2.3 类与类的关系: issubclass(类1, 类2)
用来测试类1
是不是类2
的子类.
class a: pass class b(a): pass class c: pass print(issubclass(b, a)) # true print(issubclass(c, a)) # false print(issubclass(a, b)) # false print(issubclass(a, object)) # true print(issubclass(c, (object, a))) # true 第二个参数也是可以 class 组成的元组