python进阶——类(二)访问限制、继承
本文最后更新于:1 年前
¶(三)类的访问限制
¶1. 原因:
Python中,当正常使用类实例时,外部代码可以通过直接调用实例变量的方法操作数据,从而隐藏内部的复杂逻辑——但有些时候,不希望外部代码自由修改某些属性时,需要为其设置访问权限,阻止外部代码的访问。
¶2. 概念:
Python中,属性权限的控制是通过属性名来实现的。
当属性名是由双下划线(__)开头但不是以双下划线(__)结尾,该属性就无法被外部访问。(如果是也以双下划线结尾的话就是特殊属性了,一般是内置类属性,不建议自己定义时使用)
如果以单下划线(_)开头,实际上是不能阻止外部代码访问的,但是一般情况下约定外部代码不应该访问这样的变量。(继承中会详细讲解)
-
如果已经学习c++,可以理解为,一般的属性是public的成员,双下划线开头的是private成员,单下划线开头的是protected成员,不过可以被外部访问,但通过约定变成了protected的形式。
如果想要使用或者修改这些属性,请在类中定义相关的方法,用方法包装好,这样有利于通过方法检查属性的类型和值是否合理,避免传入无效参数。
¶3. 深入理解:
实际上,以双下划线开头的属性不是一定就不能从外部访问——本质上,Python解释器是把双下划线开头的属性从__属性名解释成为了_类名__属性名的格式(这不一定正确,因为不同版本的解释器可能解释的结果是不同的),如果直接指定这种格式,也可以绕过限制实现访问。
但按一般要求,强烈不建议这样做,这样做破坏了访问限制本身的约束。
如果直接在实例中采用__属性名格式赋值的话,实际上,是创建了一个新的变量和对象,和类内部的双下划线开头的属性是不同的。
根本上,从语法层面,Python的私有成员和访问限制是不彻底的,Python没有提供完善的机制阻止访问私有成员和公共成员。
另外,方法也可以通过加双下划线的方式定义为私有方法,此时方法只能在类内部使用,外部调用同样要用_类名__属性名的形式。
¶4. 例子:
首先定义一个私有成员,实现获得修改方法:
1 |
|
尝试直接用__属性名修改私有成员:
1 |
|
用上面的Python的解释器内部格式输出和修改私有成员:
1 |
|
可以看到,私有成员被外部代码修改了,说明了访问权限不是绝对的。
¶(四)继承
¶1. 概念:
类的最重要一点就是需要支持继承。
继承是类和类之间的一种关系,继承是代码重用的最重要的方式之一。
在实现某个新类,可以通过继承完成——继承中有两类对象,基类和派生类,派生类可以从基类中继承其属性和方法,所以,创建的新类也就是派生类,方法、属性的原所有者也就是基类。
基类和派生类也可以称为父类和子类。
派生类除了继承的属性和方法外,还可以覆盖、添加功能——前者是公有的属性和功能,后者是自己特有的属性和功能。
Python支持多级继承,派生类仍然可以作为另一个类的基类使用;也支持多继承,即一个派生类由多个基类派生。
对于派生类的一个实例,其也是基类的一个实例;而对于基类的一个实例,其不是派生类的实例——所以,我们说派生类和基类是"is"的关系,可以用isintance判断。
这里演示一下简单的例子:
1 |
|
¶2. 语法格式:
class DerivedClassName(BaseClassName1, BaseClassName1, ...,BaseClassNameN): <statement-1> . . . <statement-N>
DerivedClassName是派生类类名,BaseClassNameN对应了每一个被继承的基类类名,内部类语句不变。
BaseClassName一般情况下必须与派生类定义在一个命名空间内。
基类类名可以用某些表达式代替,比如modname.BaseClassName,使用某个模块中的类作为基类,这时允许BaseClassNameN与派生类不在一个作用域内。
例如可以定义如下派生类:
1 |
|
¶3. 分类:
-
单继承:
最普通的继承,单独继承一个父类,获得其全部的属性和方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class father:
def __init__(self):
print("init father work")
self.init_f_val1=1
self.init_f_val2="classfather"
def print_father(self):
print("print_father:")
print("init_f_val1:",self.init_f_val1)
print("init_f_val2:",self.init_f_val2)
class son(father):
def __init__(self):
super().__init__()
print("init son work")
self.init_s_val1=2
self.init_s_val2="classson"
def print_son(self):
self.print_father()
print("print son:")
print("init_s_val1:", self.init_s_val1)
print("init_s_val2:", self.init_s_val2)
s =son()
# 运行结果:
# s.print_son()
# init father work
# init son work
# print_father:
# init_f_val1: 1
# init_f_val2: classfather
# print son:
# init_s_val1: 2
# init_s_val2: classson上面使用的super稍微解释。
-
多重继承和多层继承:
多重继承是对多个类的继承,也就是一个类同时拥有多个基类。
多层继承是类的继承不只是两级的,派生类可以作为新的基类被继承,从而形成多个层次的继承,派生类本身的属性和方法以及通过继承得来的属性和方法都会继承给下一层派生类。
多继承和多层继承可以统称为多继承。
python支持的多继承是有限的,对于一个类继承多个父类时,优先继承第一个类的属性和方法——也就是按照优先顺位完成继承过程。
不支持对已经有继承关系的多个类先继承其基类,再继承其派生类,这会导致继承顺序的混乱而报错。
实际上,复杂的继承需要考虑选择同名方法的问题,python3.x版本使用的是MRO——方法搜索顺序,而python2.x版本使用的是深度优先继承方式。
方法搜索顺序的具体原理较复杂,这里不详细讲述,但可以直接利用**内置属性__mro__**来查看类的继承顺序,
**__mro__**属性的查看需要通过类名来进行。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49# fun+类名每个类独有,fun1除了最终的派生类每个类都有,fun2ABC三个类有
class A:
def funA(self):
print("funA")
def fun(self):
print("funa")
def fun2(self):
print("funa")
class B(A):
def funB(self):
print("funB")
def fun(self):
print("funb")
def fun2(self):
print("funb")
class C(A):
def funC(self):
print("funC")
def fun(self):
print("func")
def fun2(self):
print("func")
class D(B,C):
def funD(self):
print("funD")
def fun(self):
print("fund")
class E(B,A):
def funE(self):
print("funE")
def fun(self):
print("fune")
class F(D,E):
def funF(self):
print("funF")
f=F()
print(F.__mro__)
f.funF()
f.funA()
f.funE()
f.fun()
f.fun2()
# 运行结果:
# (<class '__main__.F'>, <class '__main__.D'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
# funF
# funA
# funE
# fund
# funb可以看到,方法搜索搜索顺序为FDEBCA。
对于funF,由于自己独有,直接选择自己的方法;对于funA,只有类A有, 则选择A的方法;对于funE,同理要选择E的方法;而对于fun,由于除了最终的派生类每个类都有,按方法搜索顺序搜索到了D,使用D的方法;对于只有ABC有的fun2,由于搜索顺序中第一个是B,所以选择B的方法运行。
另外,对于派生类没有__init__函数时,会从第一个查询到的有__init__函数的基类中继承,并发生默认调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class father1:
i = 1
def __init__(self):
print("father1 __init__")
class father2:
ii = 2
def __init__(self):
print("father2 __init__")
class son(father1,father2):
def printson(self):
print("i:",self.i)
print("i:",self.ii)
s = son()
s.printson()
# 运行结果:
# father1 __init__
# i: 1
# i: 2
¶4. super函数:
¶(1)概念:
super()方法用于调用多继承中某基类的一个方法,super()方法的查找顺序遵循上面提到的方法搜索顺序,也就是按__mro__属性的顺序发生继承。
和直接使用基类类名调用方法不同,super()方法调用方法能够保证每个基类最多只被调用一次,前者在多继承可能会发生一个基类的构造函数被调用多次的情况,这对__init__方法的执行效果很好。
super()在多层继承的中间某个层次的类中使用,也会按最终派生类的__mro__属性的顺序进行继承,因此会发生一些看似"不合理"的现象。
多个super()方法的反复调用会按照__mro__属性的顺序调用,直到遇到object类时停止。
对于单继承,super()方法和单继承调用基类方法的效果是相同的。
¶(2)语法:
super(type[, object-or-type]).funcname()(python2.x唯一格式)和super().funcname()(python3.x独有格式)
其中,type是类名,[]内是对象或者类名,一般指定self,funcname是被调用基类的方法名,_()_内是形参列表。
返回一个代理的super对象,对其的方法调用会导致方法搜索的发生,进而实际调用搜索到的第一个方法。
¶(3)例子:
当对多层继承除了最开始的基类以外的所有派生类使用super().__init__方法时:
1 |
|
这里可以看到,两个对象创建时都没有执行father2的__init__方法,实际上很简单,因为从对象出发的__mro__属性决定了super()方法的搜索顺序,比如son1处执行了super().__init__,那么就会按__mro__属性内的顺序搜索下一个,也是就son2的__init__方法,son2进而搜索的是father1,而接下来没有了super()方法,所以停止搜索,执行完成全部遇到__init__方法。
如果想要保证每个都被执行,只需要对father1和father2也使用super().__init__方法即可,这样就会继续搜索,保证了每个基类的__init__方法都被执行且只执行一次,此时的停止是因为遇到了object类。
1 |
|