3、面向对象基础二
装饰一个类
面向对象之封装
普通方法
类方法装饰器
静态方法装饰器
私有属性
属性装饰器
对象销毁
面向对象之继承
继承定义
继承特殊属性和方法
私有属性无法继承
私有属性继承解析
总结
实例属性查找顺序
方法重写
子类调用父类
重写初始化方法
面向对象之多态
面向对象之封装
普通方法
类方法装饰器
静态方法装饰器
私有属性
属性装饰器
对象销毁
面向对象之继承
继承定义
继承特殊属性和方法
私有属性无法继承
私有属性继承解析
总结
实例属性查找顺序
方法重写
子类调用父类
重写初始化方法
面向对象之多态
装饰一个类
装饰一个类,一般来讲都是对类的属性进行操作,都是装饰类的属性定义或者类的函数定义,比如一个类,里面的age为18,那么因为特殊需求,我们需要为其改变为20,那么此时,我们也可以使用装饰器的思想来实现,如下。
def editage(age):
def wrapper(cls):
cls.age = age
return cls
return wrapper
@editage(20)
class Person:pass
p1 = Person()
print(p1.age) # 20
面向对象之封装
封装可以理解为一个多功能的自助饮料机器,且机器是不透明密封的,只在下方开了不同的龙头,当顾客需要不同的饮料时,只需要去打开不同的龙头开关即可得到自己想要的饮料,但顾客不知道机器内部是产生不同的饮料的,在python中,封装可以是类,可以是函数。封装是将数据或属性隐藏在内部,不让外部看到。
普通方法
普通方法,即类中定义的方法函数,主要是为了实现部分特定功能的操作函数,但它一般情况下只在实例下使用,普通方法的第一形参必须为self,而在调用该方法时self也必须指向一个对象,这个对象也就是实例化之后的实例,在Python中无序手动将实例作为参数传进去,只需要使用实例来调用该方法就会自动将实例传进去并作为第一参数和self关联。
class Person:
def method(self):
return 'method'
print(Person().method()) # method
# 等同于 print(Person.method(Person()))
类方法装饰器
类方法,即classmethod,它和普通方法不同,普通方法第一个形参为self,而classmethod第一个形参则为cls,也就是类,如果使用的是实例调用classmethod下面的这个方法,那么这个cls就是这个实例所属的类,如果直接使用类来调用classmethod下面的方法,那么这个cls则是类本身,既然cls是类本身,那么我们就可以使用cls来去访问类的属性,类方法调用时可以不需要实例化。
class Person:
id = 1
@classmethod
def clsmtd(cls): # 类方法,第一个参数为类
return cls, cls.id
p1 = Person()
print(p1.clsmtd()) # (<class '__main__.Person'>, 1)
静态方法装饰器
静态方法,即staticmethod,也是一种特殊的类方法,使用staticmethod修饰器修饰的类方法,第一个形参不再像普通方法第一个参数必须为self,也不像类方法(classmethod)第一个方法必须为cls,它没有必须存在的一个形参。
我们可以把它理解为一个在内里面定义的一个函数,所以如果一个方法既不跟实例相关也不跟特定的类相关,推荐将其定义为一个staticmethod,这样不仅使代码一目了然,而且似的利于维护代码。
class Person:
@staticmethod
def stmtd(): # 无需传递任何参数
return 'stmtd'
p1 = Person()
print(p1.stmtd()) # stmtd
私有属性
在Java中,有Public(公共)属性和Private(私有)属性,这可以对属性进行访问控制,在Python中也有类似JAVA的属性访问控制,在Python中一般情况下,我们会使用__private_attrs双下划线开头,声明该方法属性为私有,同样,对于数据属性也是一样的方式,使用双下划线打头,不能在类地外部被使用或直接访问。在类内部的方法中使用时self.__private_attrs调用。
为什么只能说一般情况下,因为实际上,Python中是没有提供私有属性等功能的,但是Python对私有属性的访问控制是靠程序员自觉的,因为这个属性其实是可以访问,只不过访问的方式变了。
class Person:
def __init__(self):
self.__num = True # 私有数据属性
def __private_attrs(self): # 私有方法属性
return self.__num
p1 = Person()
p1.__private_attrs() # 'Person' object has no attribute '__private_attrs' # 私有属性,不可见
print(dir(p1)) # ['_Person__private_attrs',...] # 实际上是修改了方法名称
print(p1._Person__private_attrs()) # 1 # 依旧可以调用
在Python中使用单下划线(_)或者双下划线(__)来标识一个成员被保护或者私有成员,但是不管用什么样的访问控制,都不能真正的阻止用户修改类的成员,Python中没有绝对安全的保护成员或私有成员的方法。
因此,不管是单下划线(_)还是者双下划线(__),只是一种警告或者提醒,除非真的有必要,否则尽量不要修改或使用保护成员或私有成员。
属性装饰器
在Python中,有一个属性装饰器,即property,它最大的好处就是在类中把一个方法变成属性来调用,起到既能检查属性,还能用属性的方式来访问该属性的作用,但是使用了property装饰器之后,所装饰的方法,将变成一个只读属性,所以还需要借助于getattr和delattr来对这个属性进行访问控制。
class Person:
def __init__(self):
self.__name = 'cce'
def get_info(self):
return self.__name
p1 = Person()
print(p1.get_info()) # cce
可以看到上述案例,一般来讲,双下划线是用来描述私有属性的,所以一般都会提供一个方法来获取这个私有属性,那么对于上述案例就是get_info这个方法来去获取self.__name这个私有属性,这样依旧不能得到很好的隐藏效果,直接暴露了这是一个方法,那么从此时我们可以使用property来对他进行装饰,将其包装成为一个属性。
class Person:
def __init__(self):
self.__name = 'cce'
@property
def get_info(self):
return self.__name
p1 = Person()
print(p1.get_info) # cce # 无需使用括号调用
p1.get_info = 2 # AttributeError: can't set attribute
可以看到,当我们将一个方法,使用property装饰了之后,它就可以使用"对象.属性"的方式访问了,同时,也将这个属性变成了一个只读属性,一旦发生修改,就会抛出“can't set attribute”的错误,那是因为对于使用了property装饰器,之后装饰后的属性变成了一个只读属性,需要设定setattr才能对其进行修改,需要设定deleter才能对其进行删除;
class Person:
def __init__(self):
self.__name = 'cce'
@property
def get_info(self):
return self.__name
@get_info.setter # 装饰器直接使用property装饰的方法名.的方式
def get_info(self, name): # 所有方法名,和property装饰的方法名一致
self.__name = name
@get_info.deleter
def get_info(self):
del self.__name
p1 = Person()
print(p1.get_info) # cce
p1.get_info = 2 # AttributeError: can't set attribute
print(p1.get_info) # 2
del p1.get_info
print(p1.get_info) # AttributeError: 'Person' object has no attribute '_Person__name'
可以看到,这样,我们就可以将一个被property装饰后的只读属性,变成可读可写可删的属性,完美的隐藏了属性。
对象销毁
对象的销毁是和引用计数有关的,在类中可以定义_del_析构方法,来进行对象的销毁,以释放占用的资源,其中就可以放一些清理资源的代码,比如释放连接,但是需要注意的是,这个方法不能引起对象的真正销毁
在Python中可以使用del来减少引用计数1,而对这个_del_方法来讲,只是在引用计数为0时,才会进行自动调用。
class Person:
def __del__(self):
print('del')
p = Person() # Person()实例对象 引用计数为 1
p1 = p # Person()实例对象 引用计数为 2
print('-----end-----')
del p # Person()实例对象 引用计数为 1
print('----start-----')
del p1 # del # Person()实例对象 引用计数为 0
print('---------')
### 执行结果
# -----end-----
# ----start-----
# del
# ---------
面向对象之继承
继承,面向对象的三要素之一,这是面向对象语言非常重要的知识点,人类和猫类都可以继承自动物类,我们在模拟大千世界,但不能凡事都模拟得一摸一样,所以说,只需要把关心的事模拟出来就行了,每个个体都继承自父母,继承了父母一部分特征,它又可以有自己特有的一些属性。
在面向对象的世界中,从父类继承,就可以直接拥有父类的属性和方法,这样可以减少代码达到复用的效果,父类有的属性和方法,子类可以直接复用,无需在子类中重新定义,当然,子类也可以有和父类不一样的属性和方法,可以继承也可以自有,如下示例。
class Animal:
def shout(self):
print("%s shouts" % type(self).__name__)
class Cat(Animal):
pass
a = Animal()
a.shout() # Animal shouts
c = Cat()
c.shout() # Cat shouts
可以看到,在Cat类中并没有定义shout方法,但是依旧可以调用,这就是继承的效果,这是一种单一继承的方式,Cat只继承自Animal,不论是数据属性还是方法属性都可以继承,如果自己没有将会直接在父类中去寻找。猫类、狗类都属于动物类,通过继承就可以让子类从父类获取相应的特征属性和方法。
继承定义
父类也称为基类,或者超类,子类也称之为派生类,如果在定义类时,没有基类列表,等用于继承自object,在Python3中,object类是所有对象的根基类,换句话说,object是所有类的根父类,对于类的继承语法定义如下。
class 子类名( 基类一 [ ,基类二 ,基类三 , ... ] )
语句块
继承特殊属性和方法
对于一个派生类,或已经有派生类的类,会新增许多的属性和方法,如下列表。
__name__:类、函数、方法等的名字;
__module__:类定义所在的模块名;
__class__:对象或类所属的类;
__doc__:类、函数的文档字符串,如果没有定义则为None;
__dict__:类或实例的属性字典;
__base__:类的基类;
__bases__:类的基类元组;
__mro__:显示方法查找顺序,基类的元祖;
mro:派生类新增的方法,同上,返回列表;
__subclasses__:返回一个父类的子类列表;
私有属性无法继承
私有属性,即双下划线"__"打头的方法或者属性,都不会继承,在子类中也无法进行访问,但是说无法访问,其实也可以访问,因为在Python中的私有方法并不是完全不可方法。
因为对于Python来讲,私有属性,其实就是将属性进行改名,以当前类所在的类名,作为它改名的前缀,想要访问其实还是可以访问的,只不过需要使用改名后的名字来访问才行,如下示例。
class Animal:
def __shout(self):
print("%s shouts" % type(self).__name__)
class Cat(Animal):
pass
c = Cat()
c._Animal__shout() # Cat shouts
# 可以访问
私有属性继承解析
下面的案例,可以帮助了解继承的特性,主要是在私有属性这一块,能快速了解继承与私有属性继承的特性,对于私有属性的继承,重点是这句话“私有属性属于定义私有属性的类,私有属性并不是真正意义的私有,因为其内部原理就是改名,改名规则为,_类名__属性/方法名”,如下示例。
class Animal:
__COUNT = 100
def __init__(self, age, weight, height):
self.__COUNT += 1
self.age = age
self.__weight = weight
self.height = height
def eat(self):
print('{} eat'.format(self.__class__.__name__))
def __getweight(self):
print(self.__weight)
@classmethod
def showcount1(cls):
print(cls)
print(cls.__dict__)
print(cls.__COUNT)
@classmethod
def __showcount2(cls):
print(cls.__COUNT)
def showcount3(self):
print(self.__COUNT)
class Cat(Animal):
__COUNT = 200
c = Cat(3,40,20)
print(c._Cat__COUNT)
c._Animal__getweight()
c.showcount1()
c._Animal__showcount2()
c.showcount3()
print(c._Cat__COUNT)
print(c._Animal__COUNT)
# 1、c = Cat() # 是否可以初始化
# 2、print(c.__COUNT) # 是否可以访问,如果不能如何改进
# 3、c.__getweight() # 是否可以访问,如果不能如何改进
# 4、c.showcount1() # 求输出结果
# 5、c.__showcount2() # 是否可以访问,如果不能如何改进
# 6、c.showcount3() # 求输出结果
# 7、print(c._Cat__COUNT) # 是否可以访问,如果不能如何改进
# 8、print(c._Animal__COUNT) # 是否可以访问,如果不能如何改进
# 解析
# 重点:私有属性,属于定义私有属性的类,原则上无法继承,实际可以继承,私有属性实现私有的方式就下改名
# 第一题:答案为,无法初始化
# 解析:因为Cat没有__init__方法,所以会继承自父类,所以需要提供age、height、weight三个参数
# 第二题:答案为无法访问
# 解析:私有属性是无法通过这种方式改名的,如果要访问Cat内部的__COUNT,那么正确的访问方式应该是c._Cat__COUNT
# 第三题:答案为无法访问
# 解析:和第二题差不多,无论是私有属性还是私有方法,实现私有的方式都是改名,如果需要访问Animal中的__getweight,那么应该是c._Animal__getweight()
# 第四题:答案。Cat {'_Cat__COUNT':200} 100
# 解析:之所以cls.__COUNT的值为100是因为,私有属性是改名了,此时我们不应该将它看成cls.__COUNT,应该看成cls._Animal__COUNT,c实例属于Cat类,那么c在寻找_Animal__COUNT的时候,Cat里面没有,则会去Animal中去找,之所以结果为100而不是101的原因是,我们访问的是cls的_Animal__COUNT而不是实例的_Animal__COUNT,如果我们访问self._Animal__COUNT那么结果就是101
# 第五题:答案。无法访问,应修改为。c._Animal__showcount2()
# 解析:不论是在子类中还是在父类中,我们都不应该将它们的方法属性标识符看成__showcount2(),而是_Animal__showcount2(),所以不管怎么调用,都应该使用_Animal__showcount2()这个标识符来调用
# 第六题:答案。101
# 解析:showcount3方法使用的是self,self就是实例,上面也说过,self.__COUNT我们就不应该看成self.__COUNT,而是self._Aniaml__COUNT,所以结果应该是实例的_Aniaml__COUNT,实例经过初始化+1,即为101
# 第七题:答案,可以访问,200
# 第八题:答案,可以访问,101
总结
从父类继承的类,自己没有的属性就可以去父类中找,前提是它不是私有的,私有的都是不可以访问的,但是Python并没有对这个私有方法做太多的限制,只是改名了,想要访问还是可以访问,但是在实际环境中不应该去访问父类的私有属性,因为它是父类私有的,独有的。
继承时,非私有的属性,子类都是可以随意访问的,私有的成员是被隐藏起来的,子类是不能直接访问的,之所以弄私有就是为了不让他其他人访问,尽量不要去突破这一点,Python就通过这一套实现,就实现了和其他语言一样的这种面向对象的继承机制,包括这种公有的、私有的实现,虽然是改名的方式,但是它达到了目的,只不过它不像其他语言那样严格,不让你访问就是不能访问。
实例属性查找顺序
在Python的面向对象中,如果我们需要通过"实例.属性"的方式来访问一个属性,它的查找顺序是这样的,首先,会在实例的__dict__字典中去寻找,然后会在类的__dict__中去寻找,如果还是找不到,如果就继承,就会走继承,在父类的__dict__字典中去寻找(其实最后都会object,但是object没有这个属性)如果搜索这些地方后都没有找到就会抛出异常,如果找到就立即返回。
其实这个顺序可以直接通过mro拿到,会直接查到属性的查找顺序。
class Animal:
pass
class Cat(Animal):
pass
print(Cat.mro()) # [<class '__main__.Cat'>, <class '__main__.Animal'>, <class 'object'>]
方法重写
方法重写,即覆盖overwrite,它的意思就是父类有的属性,子类也有同名属性,那子类就会覆盖父类的属性,对于重写,需要注意的一点的,如果涉及到重写,尽量子类中重写的方法要和父类重写的方法形式上一致,比如,如果父类是staticmethod那么子类也写成staticmethod。
class Animal:
name = "cce"
class Cat(Animal):
name = "cfj" # 实现了覆盖overwirte的效果
c1 = Cat()
print(c1.name) # cfj
# 覆盖了父类的name属性
子类调用父类
在项目开发中,我们经常会覆盖父类方法,但是有的时候,我们又不得不在子类中调用父类方法,那么当我们想要在子类的方法当中调用父类的属性时,我们可以很想当然的想到直接使用父类名.父类方法名来调用,但是这样会有一个问题,就是父类如果改名了,那么这段代码就失效了。
可能又会想到直接使用__class__.__base__方法,但是这样不利于代码的整洁性,更何况直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题,因此,Python给我们提供了一个super()方法,它就是用来解决多重继承问题的。
class Animal:
def shout(self):
print("Animal")
class Cat(Animal):
def shout(self):
print("Cat")
def call(self):
__class__.__base__.shout(self) # 不推荐
super(__class__, self).shout() # 指定具体调用哪一个父类的方法
super().shout() # 让super自己按照mro查找顺序去寻找调用的方法
c1 = Cat()
c1.call()
# Animal
# Animal
# Animal
重写初始化方法
在很多种情况下,我们可能需要重写父类的__init__方法,那么这就有一个问题,因为__init__方法是一个非常重要的方法,可能下面的其他方法引用了__init__下的某个实例属性,那么如果子类重写了,没有这个实例属性,但是从父类继承下来的方法又会用到,那么此时就会有问题了。
class A():
def __init__(self,name):
self.name=name
def showname(self):
return self.name
class B(A):
def __init__(self,age):
self.age=age
b=B(18)
print(b.__dict__) # {'age': 18} 此时b实例只有一个age属性
print(b.showname()) # AttributeError: 'B' object has no attribute 'name'
分析下这段代码,b实际上是class B的实例,那么在使用b去调用showname时,会首先在b.__dict__中去寻找,找不到就去B.__dict__最后在继承的父类A.__dict__中找到,b.showname实际上等价于A.showname(b),这个self其实就是b,showname这个方法需要在self里面去寻找name属性,因为self是b,所以其实可以看出来,b是实例没有name的。
因为虽然A、B有继承关系,但是如果方法重写了,那么这个重写的方法就各自独立了,并不会使用继承的方法。
这种情况,在编码中其实非常常见,那么此处,也可以直接在子类的__init__方法中初始化父类的__init__方法,并且在__init__方法的第一行,推荐这么使用,那么在子类中初始化父类的__init__方法,子类的实例属性将新增父类__init__方法的下面的数据属性(描述得不是很好,看代码11行),当然,如果在子类的__init__方法下中重写了父类存在的实例属性,会予以覆盖,如下示例。
class A():
def __init__(self,name):
self.name=name
def showname(self):
return self.name
class B(A):
def __init__(self,age):
super().__init__('cce')
self.age=age
b=B(18)
print(b.__dict__) # {'age': 18, 'name': 'cce'} # 将父类的name也加入到了子类实例中
print(b.showname()) # cce
在子类中调用父类的方法,也是每个程序员在编码时遇到继承的时候,应该做的一件事,避免不必要的麻烦。
面向对象之多态
在上述继承环节说过,一个类的子类,这个子类实现了继承,也实现了覆盖overwrite,那么对于多态来讲,其实就是基于继承的特性的,面向对象中有了继承和覆盖才会有多态,有继承之后,在子类中覆盖了父类的同名方法,这样子类的实例在使用的时候,它就可以使用哪些覆盖掉的方法,也就是说用同一套方法,却表现不同,因为用到的是覆盖后的方法,这种表现不同的这种形式叫做多态,这就是面向对象的三要素之一 。
总结一句话,在面向对象中,父类、子类通过继承联系在一起,如果可以同一套方法,实现不同的表现,那么这就是多态。