Python的封装与继承

一、封装

封装Encapsulation,将数据和操作组织到类中,即属性和方法将数据隐藏起来,给使用者提供操作(方法);使用者通过操作就可以获取或者修改数据;getter和setter;
通过访问控制,暴露适当的数据和操作给用户,该隐藏的隐藏起来,例如保护成员或私有成员;

二、继承

1、类的继承

1)定义

继承Inheritance,在面向对象的世界中,从父类继承,就可以直接拥有父类的属性和方法,这样可以减少代码、多复用;子类可以定义自己的属性和方法;
继承可以让子类从父类获取特征(属性和方法);父类,也称为基类、超类;子类,也称为派生类;
如果类定义时,没有基类列表,等同于继承自object;在Python3中,object类是所有对象的根基类;Python支持多继承,继承也可以多级;

2)查看继承的特殊属性和方法

类才有继承属性和方法,实例的话要加__class__才能看
注意__subclasses__() 是方法,要加括号

class Animal:
    AGE=5
    def __init__(self,name):
        self.name=name
    def shout(self):
        print('{} shout'.format(self.__class__.__name__))
​
class Cat(Animal):
    pass
​
class Dog(Animal):
    pass
​
a=Cat('hicat')
​
print(1,a.AGE)  # 5
a.shout()  # Cat shout
print(3,a.__dict__)  # {'name': 'hicat'}
print(4,Animal.__base__)  # 
print(5,Cat.__base__)  # 
print(6,Cat.__base__.__base__)  # 
print(7,Cat.__bases__)  # (,) 不显示object
print(8,Cat.__mro__)  # (, , ) 元祖
print(9,Cat.mro())  # [, , ] 列表
print(10,Animal.__subclasses__()) # [, ]
print(11,int.__subclasses__())  # []

3)继承中的访问控制

从父类继承,自己没有的,就可以到父类中找;私有的都是不可以访问的,但是本质上依然是改了名称放在这个属性所在类或实例的__dict__中;知道这个新名称就可以直接找到这个隐藏的变量,这是个黑魔法技巧,慎用;

继承时,公有的,子类和实例都可以随意访问;私有成员被隐藏,子类和实例均不可直接访问,但私有变量所在的类内的方法中可以访问这个私有变量;
Python通过自己一套实现,实现和其它语言一样的面向对象的继承机制;

属性查找顺序:实例的__dict__ - > 类__dict__ === 如果有继承 - > 父类 dict 如果搜索这些地方后没有找到就会抛异常,先找到就立即返回了;

class Animal:
    __COUNT=100
    HEIGHT=0
    
    def __init__(self,age,weight,height):  #初始化就要想到赋值即定义
        self.__COUNT+=1
        self.age=age
        self.__weight=weight
        self.HEIGHT=height
​
    def shout(self):
        #谁调用,显示谁的类,传进来的是c,self就是c,self.__class__是Cat
        print('{} shout'.format(self.__class__.__name__))
​
    @classmethod
    def showcount1(cls):  #传进来的确实是Cat,不会因为定义在Animal就显示Animal
        print(cls)
        print(cls.__dict__)
        print(cls.__COUNT)
        #注意,此处不是_Cat__COUNT,查找__COUNT会找不到,然后找到父类,父类Animal有__COUNT
        #类属性的查找原则也是层层向上找,此处实际是cls._Animal__COUNT
        #私有属性定义在哪里受谁管制
        #cls,即Cat依然会从内向外找,因自己已改名,自己的字典里没有__COUNT,才向父类找  
​
    @classmethod
    def __showcount2(cls):
        print(cls.__COUNT)
​
    #实例化后就会初始化,__COUNT=101,然后放在该实例的字典中
    def showcount3(self):   
        print(self.__COUNT)
        print(self.__dict__)
​
class Cat(Animal):
    NAME='CAT'
    __COUNT=200   #200只在Cat的类字典上
​
c=Cat(3,5,15)
c.shout()  # Cat shout
c.showcount1()  # 尤其注意

# 
# {'__module__': '__main__', 'NAME': 'CAT', '_Cat__COUNT': 200, '__doc__': None}
# 100  Cat依然会从内向外找,因自己已改名,自己的字典里没有__COUNT,才向父类找 

c._Animal__showcount2()  # 100
c.showcount3()
# 101
# {'_Animal__COUNT': 101, 'age': 3, '_Animal__weight': 5, 'HEIGHT': 15}

两个注意点:

关于类方法classmethod,传进来如果是Cat的实例,cls就是Cat,不会因为定义在Animal就显示cls为Animal
对于私有变量,定义在哪个类、就被改成什么名字,即私有属性定义在哪里受谁管制;例如此处不是_Cat__COUNT,查找__COUNT会找不到,然后找到父类,父类Animal有__COUNT

4)方法的重写、覆盖override

super( )可以返回父类,访问到父类的属性和方法,等价于super(Cat, self);
super()是找当前类的父类和实例进行绑定和操作;

class Animal:
    def shout(self):
        print('Animal shout')
​
class Cat(Animal):  
    def shout(self):   # 覆盖了父类方法
        print(1,super())
        print(2,super(Cat,self))
        print(3,super(Cat))
        super().shout()
        super(Cat,self).shout()
        self.__class__.__base__.shout(self)
​
c=Cat()
c.shout()
​
# 结果
1 , >
2 , >
3 , NULL>
Animal shout
Animal shout
Animal shout

5)继承中的初始化

如果类B定义时声明继承自类A,则在类B中__bases__中是可以看到类A;但是这和是否调用类A的构造方法是两回事;如果B中调用了A的构造方法,就可以拥有父类的属性了;作为好的习惯,如果父类定义了__init__方法,你就该在子类的__init__中调用它;

B实例一旦定义了初始化__init__方法,就不会自动调用父类的初始化__init__方法,需要手动调用;在子类的__init__方法中,要想使用父类的初始属性,应该显式调用父类的__init__方法,例如super().init(age);注意,调用父类的__init__方法,出现在不同的位置,可能导致出现不同的结果;

注意一个原则,自己的私有属性,就该自己的方法读取和修改,不要借助其他类的方法,即使是父类或者派生类的方法;

class A:
    def __init__(self, a, d=10):
        self.a = a
        self.__d = d
​
class B(A):
    def __init__(self, b, c):
        super().__init__(b+c, b-c)  
        self.b = b                  
        self.c = c 
    #充分借用父类创建的__init__方法,通过super()的调用                                
    #完善子类的属性,添加a和d属性,最后这些属性都归当前f实例
    #自始至终,self都指代f,不关父类实例任何事
    #super().__init__(b+c, b-c) 等价于self.a=a,self._A__d=d 
    #也等价于 A.__init__(self,b+c,b-c)                              
    
    def printv(self):               
        print(self.b)               
        #print(self.a)
​
f = B(200,300)
print(f.__dict__)  # {'a': 500, '_A__d': -100, 'b': 200, 'c': 300}
print(B.__dict__)
# {'__module__': '__main__', '__init__': , 'printv': , '__doc__': None}

2、不同版本的类

Python2和Python3最大区别:类的继承体系,Python3更像面向对象;
dir()方法返回所有属性的字符串列表,会尽可能收集对象所有的属性,新式类继承自object,其dir()内容更多;

Python2.2之前类是没有共同的祖先的,之后引入object类,它是所有类的共同祖先类object;Python2中为了兼容,分为古典类(旧式类)和新式类;Python3中全部都是新式类;新式类都是继承自object的,新式类可以使用super;

# 以下代码在Python2.x中运行 
# 古典类(旧式类)
class A: pass
​
# 新式类
class B(object): pass
​
print(dir(A))  # ['__doc__', '__module__']
print(dir(B))  # ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
print(A.__bases__) # ()
print(B.__bases__) # (,)
​
# 古典类
a = A()
print(a.__class__)  # __main__.A
print(type(a)) # 
​
# 新式类
b = B() 
print(b.__class__)  # 
print(type(b))  # 

3、多继承和多态

OCP原则:多用“继承”、少修改
一个类继承自多个类就是多继承,它将具有多个类的特征;
继承的用途:在子类上实现对基类的增强、实现多态
多态:在面向对象中,父类、子类通过继承联系在一起,如果可以通过一套方法,就可以实现不同表现,就是多态;

1)多继承弊端
多继承很好的模拟了世界,因为事物很少是单一继承,但是舍弃简单,必然引入复杂性,带来了冲突;多继承的实现会导致编译器设计的复杂度增加,所以现在很多语言也舍弃了类的多继承;
C++支持多继承;Java舍弃了多继承; Java中,一个类可以实现多个接口,一个接口也可以继承多个接口;Java的接口很纯粹,只是方法的声明,继承者必须实现这些方法,就具有了这些能力;

多继承可能会带来二义性,例如,猫和狗都继承自动物类,现在如果一个类多继承了猫和狗类,猫和狗都有shout 方法,子类究竟继承谁的shout呢?
实现多继承的语言,要解决二义性,解决方案是深度优先或者广度优先;

2)Python多继承实现

Python使用MRO(method resolution order方法解析顺序)解决基类搜索顺序问题;
历史原因,MRO有三个搜索算法:

  • 经典算法,按照定义从左到右,深度优先策略;2.2版本之前;左图的MRO是MyClass,D,B,A,C,A
  • 新式类算法,经典算法的升级,深度优先,重复的只保留最后一个;2.2版本;左图的MRO是MyClass,D,B,C,A,object
  • C3算法,在类被创建出来的时候,就计算出一个MRO有序列表;2.3之后,Python3唯一支持的算法;左图中的MRO是MyClass,D,B,C,A,object的列表;C3算法解决多继承的二义性

经典算法有很大的问题,如果C中有覆盖A的方法,就不会访问到了,因为先访问A;
新式类算法,依然采用了深度优先,解决了重复问题,但是同经典算法一样,没有解决继承的单调性;

C3算法之前:最大的难点是难以解决继承路线的单调性/不确定性;
C3算法,解决了继承的单调性,它阻止创建之前版本产生二义性的代码;求得的MRO本质是为了线性化,且确定了顺序;Python2.3之后唯一支持的算法就是C3算法;

3)多继承的缺点

当类很多,继承复杂的情况下,继承路径太多,很难说清什么样的继承路径;
Python语法是允许多继承,但Python代码是解释执行,只有执行到的时候,才发现错误;团队协作开发,如果引入多继承,那代码很有可能不可控;不管编程语言是否支持多继承,都应当避免多继承;

4、Mixin
类有如下继承关系:文档Document类是其他所有文档类的抽象基类;Word、Pdf类是Document的子类;
需求:为Document子类提供打印能力;有以下4中思路:

1)在Document中提供print方法
基类提供的方法不应该具体实现,因为它未必适合子类的打印,子类中需要覆盖重写;
基类中只定义,不实现的方法,称为“抽象方法”;调用这些未实现的方法,会抛出一个NotImplementedError;

2)需要打印的子类上增加

class Document:
    def __init__(self,content):
        self.content=content
​
class Word(Document):
    pass
​
class Pdf(Document):
    pass
​
class PrintableWord(Word):
    def print(self):
        print(self.content)
​
print(PrintableWord.__dict__)  # {'__module__': '__main__', 'print': , '__doc__': None}
print(PrintableWord.mro())  # [, , , ]
​
pw=PrintableWord('test string')
pw.print()  # test string

如果在现有子类上直接增加,违反了OCP的原则,所以应该继承后增加;
看似不错,如果需要还要提供其他能力,如何继承? 应用于网络,文档应该具备序列化的能力,类上就应该实现序列化;可序列化还可能分为使用pickle、json、messagepack等;
这个时候发现,为了增加一种能力,就要增加一次继承,类可能太多了,继承的方式不是很好了;功能太多,A类需要某几样功能,B类需要另几样功能,它们需要的是多个功能的自由组合,继承实现很繁琐;

3)装饰器
用装饰器增强一个类,把功能给类附加上去,那个类需要,就装饰它
优点:简单方便,在需要的地方动态增加,直接使用装饰器,可以为类灵活的增加功能

def printable(cls):
    def _print(self):
        print(self.content,'装饰器打印')
    cls.print=_print
    return cls
​
class Document:
    def __init__(self,content):
        self.content=content
​
class Word(Document):
    pass
​
class Pdf(Document):
    pass
​
@printable  # 先继承,后装饰
class PrintableWord(Word):
    pass
​
@printable
class PrintablePdf(Pdf):
    pass
​
​
print(PrintableWord.__dict__)  # {'__module__': '__main__', '__doc__': None, 'print': ._print at 0x1053c0730>}
print(PrintableWord.mro())  # [, , , ]
​
pw=PrintableWord('test string')
pw.print()  # test string 装饰器打印

4)Mixin类
Mixin就是其它类混合进来,同时带来了类的属性和方法;这里看来Mixin类和装饰器效果一样,也没有什么特别的;但是Mixin是类,就可以继承;
Mixin本质上就是多继承实现的;Mixin体现的是一种组合的设计模式;

class Document:
    def __init__(self,content):
        self.content=content
​
class Word(Document):
    pass
​
class Pdf(Document):
    pass
​
class PrintableMixin:
    def print(self):
        print(self.content,'Mixin')
​
class PrintableWord(PrintableMixin,Word):
    pass
​
print(PrintableWord.__dict__)  # {'__module__': '__main__', '__doc__': None}
print(PrintableWord.mro())  # [, , , , ]
​
pw=PrintableWord('test string')  
pw.print()  # test string Mixin
class Document:
    def __init__(self,content):
        self.content=content
​
class Word(Document):
    pass
​
class Pdf(Document):
    pass
​
class PrintableMixin:
    def print(self):
        print(self.content,'Mixin')
​
class SuperPrintableMixin(PrintableMixin):
    def print(self):
        print('super')
        super().print()
        print('super')
​
class PrintableWord(SuperPrintableMixin,Word):
    pass
​
print(PrintableWord.__dict__)  # {'__module__': '__main__', '__doc__': None}
print(PrintableWord.mro())  # [, , , , , ]
​
pw=PrintableWord('test string')
pw.print()
# super
# test string Mixin
# super

在面向对象的设计中,一个复杂的类,往往需要很多功能,而这些功能有来自不同的类提供,这就需要很多的类组合在一起;从设计模式的角度来说,多组合,少继承;

Mixin类的使用原则
Mixin类中不应该显式的出现__init__初始化方法
Mixin类通常不能独立工作,因为它是准备混入别的类中的部分功能实现
Mixin类的祖先类也应是Mixin类或object,不要继承别的类,应该尽可能简单

使用时,Mixin类通常在继承列表的第一个位置,例如 class PrintableWord(PrintableMixin, Word): pass;Mixin类和装饰器 这两种方式都可以使用;如果还需要继承就得使用Mixin类的方式 ;

你可能感兴趣的:(Python进阶知识)