封装、继承和多态
2021-7-7
| 2023-8-6
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

 

访问限制

Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样就隐藏了内部的复杂逻辑。
但是从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的namescore属性:
 
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问:
这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。
 
但是如果外部代码要获取namescore怎么办?可以给Student类增加get_nameget_score这样的方法:
 
如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:
 
原先那种直接通过bart.score = 99也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:
注:在Python中,变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用__name____score__这样的变量名。
 
以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,这样的变量意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
 
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以可以通过_Student__name来访问__name变量:
但是强烈建议不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
 
 
注意下面的这种错误写法:
表面上看,外部代码“成功”地设置了__name变量,但实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart新增了一个__name变量:
 

继承

OOP程序设计中,当定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
 
比如已经编写了一个名为Animalclass,有一个run()方法可以直接打印:
当需要编写DogCat类时,就可以直接从Animal类继承:
对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。CatDog类似。
 
继承最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此DogCat作为它的子类,就自动拥有了run()方法:
 
当然,也可以对子类增加一些方法,比如Dog类:
 
继承的第二个好处需要对代码做一点改进。无论是Dog还是Cat,它们run()的时候,显示的都是Animal is running...,符合逻辑的做法是分别显示Dog is running...Cat is running...,因此,对DogCat类改进如下:
当子类和父类都存在相同的run()方法时,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,就获得了继承的另一个好处:多态。
 

多态

当定义一个class的时候,实际上就定义了一种数据类型。定义的数据类型和Python自带的数据类型,比如strlist没什么两样:
 
要理解多态的好处,还需要再编写一个函数,这个函数接受一个Animal类型的变量:
新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
 
多态的好处就是,当需要传入DogCatTortoise……时,只需要接收Animal类型就可以了,因为DogCatTortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
 
继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树。比如如下的继承树:
notion image
 
 
 

静态语言 vs 动态语言

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,不一定需要传入Animal类型,只需要保证传入的对象有一个run()方法就可以了:
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python"file-like object"就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为"file-like object"。许多函数接收的参数就是"file-like object",不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。
 
 

多重继承

继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能
回忆一下Animal类层次的设计,假设要实现以下4种动物:
  • Dog - 狗狗;
  • Bat - 蝙蝠;
  • Parrot - 鹦鹉;
  • Ostrich - 鸵鸟。
notion image
notion image
notion image
如果要再增加“宠物类”和“非宠物类”,这么搞下去,类的数量会呈指数增长,很明显这样设计是不行的。
正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计:
通过多重继承,一个子类就可以同时获得多个父类的所有功能
 

MixIn

在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn。
为了更好地看出继承关系,我们把RunnableFlyable改为RunnableMixInFlyableMixIn。类似的,还可以定义出肉食动物CarnivorousMixIn和植食动物HerbivoresMixIn,让某个动物同时拥有好几个MixIn:
 
MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
 
Python自带的很多库也使用了MixIn。举个例子,Python自带了TCPServerUDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供。通过组合就可以创造出合适的服务来。
比如,编写一个多进程模式的TCP服务,定义如下:
编写一个多线程模式的UDP服务,定义如下:
如果你打算搞一个更先进的协程模型,可以编写一个CoroutineMixIn
这样一来,不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
 
  • Python
  • 类和实例获取对象信息
    目录