面向对象具有三大特征,分别为:
封装,简单的讲,就是信息隐藏。封装即隐藏具体的实现细节,只提供给外界调用的接口。这样,底层细节改变的时候,不会对外界造成影响,只要提供给外界的接口不变即可。
在程序中,我们可以通过将变量私有化来做到封装。所谓的变量私有化,就是在类中定义的变量,仅能在当前类(定义变量的类)中访问,而不能在类的外部访问。
如果一个属性名(或方法名)使用两个下划线(__)开头,并且少于两个下划线结尾,则这样的属性(方法)就称为私有属性(方法)。私有属性(方法)只能在类的内部访问。
示例:
class person:
#public
def __init__(self,a,b):
self.__name = a
self.__age = b
def set_age(self,x):
self.__age = x
def set_name(self,name):
self.__name =name
a = person("tom",20)
print(a.name,a.age)
报错,因为__name和__age是私有的,不能直接在类外访问:
AttributeError Traceback (most recent call last)
<ipython-input-14-fbc02d77b16f> in <module>
9
10 a = person("tom",20)
---> 11 print(a.__name,a.__age)
AttributeError: 'person' object has no attribute '__name'
正确方法是定义一个可以类外使用的方法:
class person(object):
def __init__(self,a,b):
self.__name = a
self.__age = b
def set_age(self,x):
self.__age = x
def set_name(self,name):
self.__name =name
def show_age(self):
print(self.__age)
def show_name(self):
print(self.__name)
a = person("tom",20)
a.show_name()
a.show_age()
output:
tom
20
- 说明:如果变量(方法)以两个下划线开头,但同时结尾也是两个(或更多)的下划线,则这样的变量(方法)不是私有变量(方法)。因为Python中很多特殊变量与方法(魔法方法)都是这样命名的,例如__init__方法。如果这样的命名称为私有变量(方法),将会导致无法访问。
名称的私有化会带来一些问题。比如__name为私有,那么想要获取Person对象的名字(name属性),或者设置该属性的值,现在都已经无法做到。为了能够不影响客户端的正常访问,我们可以提供公有的访问方法,一个用来获取私有属性值,一个用来设置私有属性值。
封装是一个过程,它分隔构成抽象的结构和行为的元素。封装的作业是分离抽象的概念接口与实现。
不过,在Python语言中,所谓的私有,不过是一种假象。当我们在类中定义私有成员时,在程序内部会将其处理成_类名 + 原有成员名称的形式。也就是会将私有成员的名字进行一下伪装而已,如果我们使用处理之后的名字,还是能够进行访问的。但是,我们不要这样做,因为这会破坏封装性,从而给自己埋下一颗不定时的炸弹。
然而,在客户端访问时,公有的方法总不如变量访问那样简便,怎样才能既可以直接访问变量,又能够实现很好的封装,做到信息隐藏呢?
我们可以使用property的两种方式来实现封装:
简单示例:
class Person(object):
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return self.first_name+self.last_name
a = Person("zhang","san")
print(a.full_name)
output:
zhang san
property详细可以参见:https://www.cnblogs.com/z-x-y/p/10148911.html
继承体现的是一种一般与特殊的关系。如果两个类型之间,存在一种一般与特殊的关系时(例如苹果与水果),我们就称特殊的类型继承了一般的类型(苹果继承了水果)。对于一般的类型(水果),我们称为父类,而对于特殊的类型(苹果),我们称为子类。
当子类继承了父类,子类就可以继承父类中定义的成员(变量,方法等),就好像在子类中自己定义的一样。
继承的语法为:
class B(A):
类体
这样,B类就继承了A类,B就成为一种特殊的A,B类就会继承A类的成员。
如果没有显式指定继承的类型,则类隐式继承object类,object是Python中最根层次的类,所有类都是object的直接或间接子类。
如果我们需要Fruit,我们可以直接使用Fruit类啊,为什么还要写一个类去继承这个类呢?
答案是,如果现有Fruit类的功能完全适合我们,我们自然可以使用现有的Fruit类,但是,我们有时候可能还需要对现有类进行调整,这体现在:
- 现有类的提供的功能不充分,我们需要增加新的功能。
- 现有类的提供的功能不完善(或对我们来说不适合),我们需要对现有类的功能进行改造。
两个内建函数:isinstance与issubclass
成员的继承
子类可以继承父类的成员,父类中声明的类属性、实例属性、类方法、实例方法与静态方法,子类都是可以继承的。
重写
当子类继承了父类,子类就可以继承父类的成员。然而,父类的成员未必完全适合于子类(例如鸟会飞,但是鸵鸟不会飞),此时,子类就将父类中的成员进行调整,以实现适合子类的特征与功能。我们将父类中的成员在子类中重新定义的现象,称为重写。
当通过子类对象访问成员时,如果子类重写了父类的成员,将会访问子类自己的成员。否则(没有重写)访问父类的成员。
重写时访问父类的成员
子类重写了父类的成员,则在子类中,访问的将是自己的成员。如果子类需要访问父类的成员,可以通过一下方法进行访问:
super().父类成员
在Python中,类是支持多重继承的,即一个子类可以继承多个父类。这在现实中也会存在这样的情况。例如,正方形既是一种特殊的矩形(有一组临边相等的矩形),也是一种特殊的菱形(有一个角是直角的菱形),则正方形会继承矩形与菱形两个类,同时,矩形与菱形又都是一种特殊的平行四边形。
当子类继承多个父类时,子类会继承所有父类的成员。当多个父类含有相同名称的成员时,我们可以通过具体的父类名,来指定要调用哪一个父类的成员(如果是实例方法,需要显式传递一个类对象),这样就能够避免混淆。
通过类名调用,可以避免混淆,但是,我们以子类的方式来调用从父类继承的成员时,会访问哪一个父类的成员呢?此时,就要求Python中的方法解析顺序来决定了。
所谓的方法解析顺序(MRO,Method Resolution Order),就是当我们访问某个类的成员时,成员的搜索顺序。该顺序大致如下:
在此例中,类B与类C继承类A,类D继承类B,类E继承类C,类F同时继承类C与类D。我们可以划分为两条分支,F -> D与F -> E,因为类F继承的顺序为(D,E),所以先从F -> D这条分支搜索,顺序为F -> D -> B,但是,虽然这条分支的上方还有类A,但这时不会搜索类A,因为搜索时有一个原则,那就是子类一定会在父类之前进行搜索。又因为类E与类C都是类A的子类,而这些子类还尚未搜索,故此时会跳过类A,当然,也会跳过所有类A的父类,例如类object。第一条分支结束后,会进行第二条分支F -> E,因为类F已经搜索过,故此时会搜索类F的父类,顺序为E -> C -> A。注意,类A此时就会搜索到,因为在这个时候,待搜索的所有候选类中,已经不存在类A的子类了。
综上,当通过类F访问某个成员时,成员的搜索顺序为:
F -> D -> B -> E -> C -> A -> object
当通过类F访问其类内的成员,会按照之前介绍的方法解析顺序搜索成员。
成员搜索顺序:
我们之前使用的super类,就是根据方法解析顺序来查找指定类中成员,但是有一点例外,就是super对象不会在当前类中搜索,即从方法解析顺序的第二个类开始。super的构造器会返回一个代理对象,该代理对象会将成员访问委派给相关的类型(父类或兄弟类)。
现在,我们就来处理一下之前提及的案例。可以发现,两个类中存在大量的代码重复,是由于两个类中存在很多公共的功能。我们知道,不管是Python教师,还是Java教师,都是一种特殊的教师。因此,我们可以采用继承的方式来处理。我们可以定义一个父类——教师类(Teacher),然后将所有公共的功能(目前两个类中重复的代码)放入父类中,使用两个子类去继承父类,这样就无需在每个子类中编写重复的内容。
所谓多态,就是多种形态。指的是根据运行时对象的真正类型,来表现其应该具有的特征。即根据运行时对象的真正类型来访问该类型所对应的成员。
在Python语言中,定义变量时,没有具体的类型。我们也将Python语言的这种特性成为“鸭子类型”。因此,Python中的多态概念比较薄弱,不像一些定义变量时需要指定明确类型的语言(例如Java,C++等)中那么明显。