深入详谈之类与对象--01

120 阅读9分钟

一、python多态与鸭子类型

在python的面向对象:不同的对象在接收相同方法或者函数时会产生不同的行为,也就是说,每个对象可以用自己的方式去响应共同的函数,不同的方式实现不同的结果,即定义时的类型与运行时的类型不一样,则称为多态。

实现多态要有2个前提:1.继承:子类与父类之间;2.重写:父类的方法或函数

如果你还是有疑问,可以看一下下面的例子,加深理解,如下:

    def __init__(self,name,area,type):
        self.name=name
        self.area=area
        self.type=type

    def go_home(self):
        if self.type == '黄种人':
            print('%s要回%s' %(self.name,self.area))
        elif self.type == '黑种人':
            print('%s要回%s' %(self.name,self.area))
        elif self.type == '白种人':
            print('%s要回%s' %(self.name,self.area))
        elif self.type == '蓝种人':
            print('%s要回%s' %(self.name,self.area))
        else:
            print('不知道家在哪里')

class Melanoderm(Person):
    pass
class Yellow(Person):
    pass
class White(Person):
    pass
#定义不同的对象
M=Melanoderm('Rose','非洲','黑种人')
Y=Yellow('Alert','美国','白种人')
W=White('小明','中国','黄种人')


#不同对象实现相同的函数方法,达到不同的结果
def func(obj):
    obj.go_home()

func(M)
func(Y)
func(W)

运行结果: Rose要回非洲 Alert要回美国 小明要回中国

说到这里,大家是否对多态有疑问呢?为什么要使用多态呢?

1.增加了程序的灵活性,使用者都是同一种形式去调用,如func(obj)。

2.增加了程序可扩展性,通过继承Person类创建了一个新的类,使用者无需更改自己的代码,还是用func(obj)去调用 。

下面在原有基础的代码上新增1个实例化对象,看看调用方式有没有变化?

class Person:
    def __init__(self,name,area,type):
        self.name=name
        self.area=area
        self.type=type

    def go_home(self):
        if self.type == '黄种人':
            print('%s要回%s' %(self.name,self.area))
        elif self.type == '黑种人':
            print('%s要回%s' %(self.name,self.area))
        elif self.type == '白种人':
            print('%s要回%s' %(self.name,self.area))
        elif self.type == '蓝种人':
            print('%s要回%s' %(self.name,self.area))
        else:
            print('不知道家在哪里')


class Melanoderm(Person):
    pass
class Yellow(Person):
    pass
class White(Person):
    pass
class Blue(Person):  #增加一种形态的人种
pass
#定义不同的对象
M=Melanoderm('Rose','非洲','黑种人')
Y=Yellow('Alert','美国','白种人')
W=White('小明','中国','黄种人')

#增加一类对象
B=Blue('Caorl','某海岛','蓝种人')


#不同对象实现相同的函数方法
def func(obj):
    obj.go_home()

func(M)
func(Y)
func(W)

#使用者调用方式不变
func(B)

通过上面的代码可知,当增加一种形态时候,完全不影响外部用户的调用方式,对使用者是透明的,只需在内部修改类代码实现即可,以上就是python多态,你get到了吗?

而“鸭子类型”又是什么呢?

动态语言调用实例方法时不检查类型,只要方法存在,参数正确,就可以调用,这就是动态语言的“鸭子类型”。它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

下面看一个例子:

鸭子类型与多态:

class Cat(object):
    def info(self):
        print('i am cat')

class Dog(object):
    def info(self):
        print('i am dog')

class Duck(object):
    def info(self):
        print('i am duck')

animal_list = [Cat, Dog, Duck]
for animal in animal_list:
    # 加上(),它才是一个类的实例化,即对象,才可以调用相对应的方法
    animal().info()    

注意: 鸭子类型 在运行之前 Cat,Dog都是在列表里面,当作变量 当运行时,加上()调用info() 才明确Cat是一个类

二、抽象基类

抽象基类(abstract base class,ABC):抽象基类就是类里定义了纯虚成员函数的类。纯虚函数只提供了接口,并没有具体实现;抽象基类不能被实例化(不能创建对象),通常是作为基类供子类继承,子类中重写虚函数,实现具体的接口。

简而言之,抽象基类就是定义各种方法而不做具体实现的类,任何继承自抽象基类的类必须实现这些方法,否则无法实例化。

2.1 抽象基类有哪些应用特点呢?

    1.检查某个类中是否有某种方法
    2.强调某个子类必须实现某些方法

2.1.1 检查某个类中是否有某种方法

  • 定义Demo类,类中含有__len__魔法方法
  • 导入抽象基类中的Sized类
"""
abc模块 abc.py  不要以abc为模块名称
"""

class Demo(object):
    def __init__(self, li):
        self.li = li

    def __len__(self):
        return len(self.li)

l = ['c', 'chen', 'wu']
d = Demo(l)
# print(d)   # 打印demo对象的地址  <__main__.Demo object at 0x0000025821DBA1D0>
# print(len(d))   # 会触发__len__  return len(self.li)  所以 返回3

"""
如何判断 Demo中 是否 含有 __len__魔法方法
"""
# print(hasattr(d,'__len__'))  # 判断 d的内部 是否含有__len__   返回值为True   可查看源码

"""
可以通过判断 d 是否是 Sized的子类  然后进一步判断 d这个对象的类 是否 含有 __len__方法
"""
from collections.abc import Sized
# 判断d 是否是 Sized 这个类
print(isinstance(d, Sized))  # True 说明d 是Sized 类型

【拓展】:查看Sized源码

class Sized(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __len__(self):
        return 0

    @classmethod   
    def __subclasshook__(cls, C):
        if cls is Sized:
            return _check_methods(C, "__len__")
        return NotImplemented

Sized类里面封装了一个__len__ 魔法方法,此方法是一个抽象类的方法

2.1.2 强制子类必须实现父类的方法

下面举一个例子来理解此特点:

"""
强制子类重写父类
方法1 主动抛出异常
"""
class CacheBase(object):
    def dele(self):
        raise NotImplementedError

    def creat(self):
        raise NotImplementedError

class RedisBase(CacheBase):
    """
      1.子类 如果不重写 父类 方法 访问时  直接抛出异常
    """
    # 重写父类方法
    def creat(self):
        print('jsjcd')

r = RedisBase()
r.creat()
r.dele()   # 不重写父类方法,报错

【注意】: 这个是正常的父类重写调用,继承自object基类, r.dele() 的方法调用时没有重写父类的方法,所以它会直接报错

下面再对比一下抽象基类是如何实现的?

"""
强制子类重写
方法2 抽象基类实现
"""
import abc

# 注意:不是直接继承object 而是继承abc.ABCMeta
class CacheBase(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def dele(self):
        print('dele')

    def creat(self):
        print('creat')

class RedisBase(CacheBase):
    def dele(self):
        print('adgj')

    def creat(self):
        print('asdd')
        
r= RedisBase()
r.creat()
# r.dele()

【注意】:在父类定义方法时,需加上@abc.abstractmethod,否则在子类调用时,会出现异常。

三、type与isinstance区别

两者的区别主要有2点:

  • type 不考虑 继承关系
  • isinstance 考虑继承关系

下面先举一个简单例子说明:

"""
isinstance 与 type 基础使用
"""

a = 1
b = "耗子"
print(isinstance(b,(int, str)))  # 判断b是否属于哪种类型
print(type(b))    # 直接验证b的数据类型

接着看以下例子,理解两者对于继承关系之间的区别:

"""
考虑继承
"""
class Father(object):
    pass

class Son(Father):
    pass

ls = Son()
# print(isinstance(ls,Son))  # True
# print(isinstance(ls, Father))  # True  继承关系

print(type(ls) is Son)  # True
print(type(ls) is Father)  # False 并不考虑继承关系 主要 两个类

【总结】: 通过以上的例子可以发现,isinstance是考虑继承关系的,而type不考虑继承关系,默认是两个类。

四、类属性与实例属性的关系

4.1 两者有何区别呢?

由于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'

当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到吗?

>>> 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

通过以上的例子,可以发现类的属性,实例也是可以访问到的,但如果实例中有属性,则优先取实例属性。

这里拓展一下两者的查找顺序:

1. 对象是可以向上查找的,所以可以访问到类属性
    • 当对象自己有该实例属性时 ,则输出的是自己的
2. 类不能向下查找,所以只能访问到类属性

【总结】:

  • 实例属性属于各个实例所有,互不干扰;
  • 类属性属于类所有,所有实例共享一个属性;
  • 不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。

4.2 多继承的查找顺序是咋样的呢?

例如以下的多继承关系,它的查找顺序如何呢?

image.png

这里会涉及到一个经典算法:MRO算法,DFS(deep first search) 深度优先,它的查找顺序会如下:先左再右

image.png

但如果是菱形继承,查找顺序又是如何呢?

image.png

这里引入BFS(广度优先),它的查找顺序如下:

image.png

举一个简单的例子,如下:

棱形继承
class D(object):
    pass

class B(D):
    pass

class C(D):
    pass

class A(B, C):
    pass

# 查找顺序(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)
# 通过className.__mro__来查看顺序   MRO算法
print(A.__mro__)

五、Python对象自省机制

Python中比较常见的自省(introspection)机制(函数用法)有: dir(),type(), hasattr(), isinstance(),通过这些函数,能够在程序运行时得知对象的类型,判断对象是否存在某个属性,访问对象的属性。

# python自省机制

class Person(object):
    name ='chem'

class Student(Person):
    def __init__(self,school_name):
        self.school_name = school_name

chen = Student('华软')
print(chen.__dict__)   # 当前对象的属性 {"属性":"属性的值"}

print(dir(chen))  # []  查看属性和方法

以上举了一个简单例子,如想了解其他的自省机制,可参考此链接:www.cnblogs.com/ArsenalfanI…

六、super函数

下面先来了解一下super函数的简单使用:

super基础使用
"""
class A(object):
    def __init__(self):
        print('a')

class B(A):
    def __init__(self):
        print('b')
        super().__init__()

b = B()
print(b)

以上是单继承关系,此运行会同时输出b、a。

但如果是多继承关系,它又会如何呢?

class A(object):
    def __init__(self):
        print("A")

class C(A):
    def __init__(self):
        print("B")
        super().__init__()

class B(A):
    def __init__(self):
        print("C")
        super().__init__()

class D(B,C):
    def __init__(self):
        print("D")
        super().__init__()

if __name__ == '__main__':
    d = D()

可以通过打印 print(D.mro)了解它的继承顺序,如下:

(<class 'main.D'>, <class 'main.B'>, <class 'main.C'>, <class 'main.A'>, <class 'object'>)