python 类篇
2. 类
面向对象的三个特点,封装,继承,多态。其中封装指的是将函数和数据封在一起形成基本单位,以该基本单位为操作原子实现功能的一种编程方法。
封装的集中体现就是类。继承和多态是建立在类的基础之上,是在类之上衍生出来的概念。
类对应面向对象编程中的封装。类中通常包含有属性和方法。类的属性和方法根据其本身具有的特点可以划分为不同的粒度或者层次。大致划分为三个层次
- 实例粒度:属性和方法 属于实例层面
- 类粒度:属性和方法 属于类层面,为类的所有实例所共享
- 其他粒度:不属于类层面,也不属于实例层面的函数,属于类空间。如静态方法
实例属性和类属性
实例属性:属于实例粒度的属性,通过构造函数赋值
类属性:属于类本身的属性,被所有类的实例共享。
class Car:
color = 'red' # 类属性
def __init__(self):
self.name='car' # 实例属性
if __name__=='__main__':
c = Car()
print(Car.color) # 访问类属性
print(c.name) # 访问实例属性
构造函数和self参数
类似于C++的构造函数。在类实例化的时候,调用构造函数。构造函数在类实例化的时候进行一些初始化的操作。如变量赋值等
self参数是类的当前实例的引用。类似于C++的This指针。
__init__
或者其他类的普通方法的首个参数一般都是self
,当然命名不一定是self
,也可以是其他,但是它必须是类中普通方法的首个参数
class Base:
def __init__(self):
self.color = 'red'
python2有新式类和老式类:(新式类继承自object
)
- 对于新式类,即使类没有显示定义构造函数,class也会隐式构造一个
__init__
函数 - 老式类则不会显示构建
如下:
class A: # 旧式类
def print_func(self):
print("This is A")
class D(object): # 新式类
def print_func(self):
print("This is D")
if __name__=='__main__':
a = A()
print(dir(a)) # 不含__init__方法
d = D()
print(dir(d)) # 含有___init方法
dir
可以查看一个类的内部属性和方法。
类方法
类方法是操作类属性的方法。
类方法相对于类中的实例方法,有如下区别:
- 使用
@classmethod
装饰器进行定义 - 第一个参数通常是
cls
(class的缩写),代表类本身
类方法的作用:
- 修改类属性,其不可以调用类的普通属性(实例属性)
类方法的调用:
- 使用类名可以调用
- 使用类的对象可以调用
class Car:
color = 'red' # 类属性
def __init__(self): # 实例方法
self.name='car' # 实例属性
@classmethod
def printx(cls): # 类方法
print("The color of car is %s" % cls.color)
# print("The name of car is " % self.name) # 报错无法调用实例属性
def run(self):
print("The name of car is %s " % self.name)
if __name__=='__main__':
Car.printx() # 使用类名调用类方法
c = Car()
c.printx() # 调用类方法
c.run()
静态方法
静态方法是python中定义在类中的一种特殊的方法,它的特殊之处在于它不与类绑定,也不与实例绑定。
静态方法的定义和类方法,实例方法的区别如下:
- 使用
@staticmethod
装饰器来声明 - 不需要传递类对象cls或者实例对象self作为第一个参数
静态方法与类以及实例无关,属于类的命名空间中的独立函数。
静态方法可以通过类名和实例名来调用。
案例:静态方法
class Car:
color = 'red'
def __init__(self):
self.name='car'
@staticmethod
def printx():
print("hello world")
print("The color of car is %s" % Car.color) # 可以使用 “类名.类变量“的方式 访问类的方式
@staticmethod
def printy(cls): # 完全独立的参数,可以传入任何对象,和所属的类有关或者无关都可以
print("The color of car is %s" % cls.color)
def run(self):
print("The name of car is %s " % self.name)
if __name__=='__main__':
Car.printx()
c = Car()
c.printx()
Car.printy(Car) # 类名调用,传入参数
c.printy(c) # 实例名调用,传入参数
缺少一个静态方法应用的好的案例
静态方法和类方法的异同点:
- 访问层面:两者都可以通过类或者对象实例进行访问;静态方法和类方法都无法访问实例变量,但是都可以通过类名访问类属性
- 静态方法和类方法 可以减少创造实例时所创造出的内存空间,加快运行速度
共有私有[待补充]
根据类内部变量和方法是否可以直接访问,可以将类内的变量或方法分为私有、共有,受保护的。
不像C++有确定的关键字private
、protected
、public
来表明属性和方法的是否可以直接访问,python
3.继承
单继承多继承
python支持单继承和多继承
单继承
class Derived(Base): # Base为基类,Derived为派生类
pass
多继承
class D(A,B,C): # A,B,C为基类,D为集成类
pass
两个常用函数:
-
使用
issubclass(子类,父类)
来确认一个类是否是一个父类的子类。 -
使用
isinstance(子类对象,类名)
来确认一个实例是否是一个由某一个类实例化得到的。
super函数
python引入super函数是为了方便子类调用父类的函数。
一般意义上,当类的继承比较简单的时候,在子类中,可以使用父类名字.函数名(参数)
调用父类函数,但是当调用关系复杂的时候,会出现问题。
案例1: 菱形继承
class Base():
def __init__(self):
print('This is Base')
class A(Base):
def __init__(self):
Base.__init__(self) # 使用"父类函数名.子类函数(self) 调用父类的构造函数
print("This is A")
class B(Base):
def __init__(self):
Base.__init__(self)
print("This is B")
class D(A,B):
def __init__(self):
A.__init__(self)
B.__init__(self)
print("This is D")
if __name__ == '__main__':
d = D()
输出:
This is Base
This is A
This is Base
This is B
This is D
上述是一个典型的菱形继承问题。其继承关系如下:
我们可以很明确的看出 Base
类的__init__
函数被调用了两次。super函数可以解决上述问题,其调用形式如下:
super(子类函数名,self).父类函数名(参数列表)
super(Classname, self).methodname() 或 super(Classname, cls).methodname() 调用"下一个"父类中的方法
用super函数修改如上案例,如下:
class Base(object): # python2中如果不添加该object,则 TypeError: super() argument 1 must be type, not classobj
def __init__(self):
print('This is Base')
class A(Base):
def __init__(self):
super(A,self).__init__() # 可以写成 super().__init__()
print("This is A")
class B(Base):
def __init__(self):
super(B,self).__init__() # 可以写成 super().__init__()
print("This is B")
class D(A,B):
def __init__(self):
super(D, self).__init__() # 可以写成 super().__init__()
print("This is D")
if __name__ == '__main__':
d = D()
运行结果如下:
This is Base
This is B
This is A
This is D
思考:为什么输出结果是上述的顺序?
MRO
列表
当定义一个派生类的时候,python会计算出一个方法解析顺序列表(MRO
列表),它代表了类的继承顺序。可以用过类名.mro()
来查看一个子类的MRO
列表
print(D.mro())
打印结果如下
[<class '__main__.D'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <type 'object'>]
上述打印结果表示继承顺序:D-->A-->B-->Base-->object
MRO
全称method resolution order (方法解释顺序)
,主要用于多继承时,判断一个属性和方法来自于那个基类。一个类的MRO
顺序综合其父类MRO
列表的结果。python使用C3
算法生成MRO
。
如果类继承了一个基类,如下class B(A)
,此时B的mro
序列为[B,A]
当存在多继承的时候,如class B(A1,A2,A3...)
,如何合并各个子类的MRO
就是C3算法的核心。其步骤如下:[存在疑问]
-
根据类的声明顺序,生成拓扑排序的列表(DAG)
-
在拓扑排序的列表中,检查每个节点的父类列表,并将其父类所在的位置移动到它自身的后面行形成新的节点。这样,可以保证子类在父类之前
-
对于多个父类同时出现在同一个节点之后的情况,需要按照它们在子类列表中的顺序保持不变
案例1
class A(O);
class B(O);
class C(A,B);
mro(C) = C + merge( mro(A), mro(B))
= C + merge( [A,O], [B,O])
= [C,A,B,O]
案例2: 存在交叉继承, python2出错误计算不出来
class A;
class B;
class C(A,B);
class D(B,A);
class E(C,D)
则 mro(E)
如何计算? python2
mro(E) = E + merge( mro(C), mro(D))
= E + merge( [C,A,B], [D,B,A])
整体代码如下:
class A(object):
def __init__(self):
print("A")
class B(object):
def __init__(self):
print("B")
class C(A,B):
def __init__(self):
super().__init__()
print("B")
class D(B,A):
def __init__(self):
super().__init__()
print("D")
''' #交叉继承会报错
class E(C,D):
def __init__(self):
super().__init__()
print("E")
'''
if __name__ == '__main__':
print(D.mro())
print(C.mro())
super的本质
super是一个函数,其定义如下:
def super(cls, inst):
mro = inst.__class__.mro()
return mro[mro.index(cls) + 1]
其中,cls是类,inst是类的实例, 上述函数的含义是:
- 获取实例inst的mro列表
- 查找mro列表中类cls的下表,返回下一个类即
mro[index+1]
即:super(cls, inst)
获得的是cls
在inst
的MRO
列表中的下一个类。
由上可知:super函数并非调用父类的函数,和调用父类函数并没有实质性的练习。super函数仅仅是传入什么入参,进行什么调用。特殊的输入让其表现出了像是在调用父类函数一样,但是这不是本质。
案例解析:上述super函数
中的 D类
class D(A,B):
def __init__(self):
super(D, self).__init__() # 可以写成 super().__init__()
print("This is D")
print(D.mro())
#[<class '__main__.D'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <type 'object'>]
调用d=D()
,结果为
This is Base
This is C
This is A
This is D
确认super(D, self).__init__()
函数调用:
super(D, self).__init__()
调用super函数,cls和inst都是D,根据D的mro,返回 类A。所以接下来是调用类A的构造函数__init__
super(A, self).__init__()
的入参要特别注意,发生了变化。此时调用super函数,cls是类A,inst是类D。所以返回调用 类B,此时调用类B的构造函数super(B, self).__init__()
的如参,cls是B,inst是D,此时调用Base函数,所以此时输出 "This is Base"- 接下来,执行 "This is B"
- 接下来回到 类A的构造函数,输出
This is A
- 接下来回到类D的构造函数,输出
This is A
上述过程,针对__init__
方法,沿着mro
依次执行。因为mro
中的类,除了基类,依次使用了super(类名,self).__init__()
这个函数,导致其会不断的沿着mro
顺序进行执行。
只要相应类在mro
中只出现一次,那么就能保证一个类的相应方法只调用一次。
案例:当mro基类中没有使用super。
class A(object):
def do_something(self):
print("This is A")
class B(object):
def do_something(self):
print("This is B")
class C(A,B):
def do_something(self):
super(C,self).do_something()
print("This is C")
if __name__=='__main__':
print(C.mro())
c = C()
c.do_something()
输出:
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>]
This is A
This is C
上面B也是作为C的基类,因为mro
中,C接下来是A,所以只执行了A的do_somethings
函数,执行完毕之后,直接返回C的do_something
执行后面的语句。
经典类和新式类
由于python版本的变化,python的类有了经典类和新兴类的区别。
在python2
中,如果定义类的方式是class Base
则为经典类;如果定义类的方式是class Base(object)
则该类为新兴类。
在python3
中上述两种方式定义出来的类都是新式类,无须显示继承object
类
super函数只能使用新式类。
4. 多态
C++ 多态的定义
多态,即多种形态。即用不同对象去实现一个相同的动作的时候,由于对象的不同会表现出多种不同的形态。简称“一个接口,多种实现"
C++有静态多态和动态多态。
- 静态多态: 在编译的时候,能确定程序的行为。比如重载函数
- 动态多态: 在运行的时候,才能确定程序的行为。 用父类的指针指向子类的函数
我们常说的多态就专指动态多态
python多态
python多态的含义和C++没有区别,但是在表现形式存在不同。
python多态只关心函数名称是否相同,不关心类之间是否有继承关系。
常见的多态形式如下:定义父类,子类重写父类的函数。
class A:
def print_func(self):
print("This is A")
class B(A):
def print_func(self):
print("This is B")
class C(A):
def print_func(self):
print("This is C")
def func(obj):
obj.print_func()
往函数func
中传入不同的对象,其会调用不同对象中名称参数匹配的函数。如下:
if __name__ == '__main__':
a = A()
b = B()
c = C()
for obj in [a,b,c]:
func(obj)
输出:
This is A
This is B
This is C
似乎与C++多态相同,但是python的特别之处在于:python中一切皆对象, 往func
可以传入任何对象,只要该对象有print_func
函数,那么不管对象之间是否有继承关系,相应函数都可以成功调用对象中相应的功能。这称之为鸭子类型。
如下:
class D(A):
def print_func(self):
print("This is D")
调用
d = D()
func(d.print_func()) # 输出 This is D
鸭子类型
鸭子类型的概念来自一首诗When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
如果一只鸟走起来像鸭子,游泳起来像鸭子,叫起来也像鸭子,那么它就是鸭子。
鸭子类型更像是一种编程思想,其关注的在于函数的行为,而不关注函数所来自的对象之间的是否存在关系。
python有很多使用方法都是可以看作是基于鸭子类型,比如 如果一个对象实现了 __getitem__
方法,那么在该对象上你就可以使用切片功能;如果一个对象实现了__iter__
和next方法,python就认为它是一个迭代器,可以通过循环来获取各个子项。
5. 虚函数
python虚函数需要借助abc
模块。该模块有如下功能:
- 提供了一个 元类
ABCmeta
可以用来定义抽象类(元类metaclass
); - 提供了一个装饰起
abstractmothod
定义纯虚函数
python中虚函数和纯虚函数貌似没有什么区别;纯虚函数只有定义在抽象类中才有意义,因为普通的父类中普通函数可以实现虚函数的功能
- 貌似没有纯虚函数,一旦一个python类定义为抽线基类,则不能初始化,虚函数必须需要要在子类中重写
案例1: 定义虚函数
from abc import ABCMeta,abstractmethod
class Base():
@abstractmethod
def print_func(self):
print 'This is Base'
pass
class C(Base):
def print_func(self):
print "This is C"
class D(Base):
pass
if __name__ == '__main__':
b = Base() # 可以实例化
b.print_func() # 输出: This is Base
c = C()
c.print_func() # 输出:This is C
d = D()
d.print_func() # 输出: This is Base
案例2:定义抽象类
from abc import ABCMeta,abstractmethod
class Base():
__metaclass__ = ABCMeta # 转变为抽象类
@abstractmethod
def print_func(self):
print 'This is Base'
pass
class C(Base):
def print_func(self):
print "This is C"
class D(Base):
pass
如上,使用ABCmeta
定义抽象类,此时类Base
转变为抽象类,当对Base类进行实例化,会报如下错误:
b = Base() # Error:Can't instantiate abstract class Base with abstract methods print_func
上述case,D
类没有对 Base
类中的虚函数print_func
进行实例化,所以也会报错
d = D() #Error:Can't instantiate abstract class D with abstract methods print_func
Python2
中通过修改__metaclass__
类变量指定元类,而Python3
中直接继承ABC类,范式如下:
## python2
from abc import ABCMeta
class Base(ABC): # Base此时为抽象类
__metaclass__ = ABCMeta
## python3
from abc import ABC, abstractmethod
class Base(ABC): # Base此时为抽象类
pass