Python基础 ( 十 ) —— 面向对象(多态、封装、反射、动态导入)
#面向对象的三大特性
1 继承(上一章的内容)
2 多态
python本身就是多态的
3 封装
# 多态
#不同类的实例化对象,调用同一个方法(执行的逻辑不同),而不用考虑他们具体的类,例如:
字符对象s和列表对象l都调用了同一个__len__的方法(都继承于他们的父类即str和list),却不需要考虑他们属于不同类
s = str(111) #相当于str这个类将数字1传入,实例化出来一个字符串对象 l = list('abc') #相当于list这个类将'abc'传入,实例化出来一个列表对象 len(s) #相当于字符对象s调用了字符类下的一个__len__的方法,即s.__len__() print(s.__len__()) len(l) #同理 print(l.__len__())
print(dir(str)) #里面有__len__
print(dir(list)) #里面有__len__
#其实len函数相当于
def len(obj) : obj.__len__()
#多态反应的是一个执行时候的状态
#再举一个例子加深理解
class H20 : def __init__(self,temperature): self.temperature = temperature def trans(self): if self.temperature < 0 : print('freezing') if self.temperature > 0 and self.temperature < 100 : print('flowing') if self.temperature > 100 : print('boiling') class liquid(H20): pass class solid(H20) : pass s1 = liquid(110) s2 = liquid(20) s1.trans() #都调用了同一种方法,却属于不同类 s2.trans() def trans(obj) : #其实这里定义了这个函数,才叫真正的多态。当你不调用时,没差别;而当你调用这个函数时,不管是什么类实例化出来的对象,都能执行同一套方法。 obj.trans() trans(s1) trans(s2)
到此,我们也就明白到底什么是多态了:不同子类继承了父类的共同的函数属性,并且不同子类实例化出来的不同对象都调用父类的函数属性,这种动态的过程,就叫做多态。其实多态的基础就是就是继承,就是一种继承的体现方式。
换句话说,继承解决的是代码重用的问题,而把这个优势体现出来的方式就叫做多态,即父类定义的函数,要考虑到多个子类的继承问题(需要是有意义的)
#封装
#封装是一种思想(本质就是要区别内外,内部就是类里面,外部就是调用者;外部调用者无需知道内部逻辑),装就是把东西都放到一个袋子里,封就是把袋子封口,将东西隐藏起来,也就是定义一个类,往里面定义数据属性和函数属性,这就叫装(装到了__dict__里)
#那么什么是封呢?
(类中定义私有的,只想让其在类内部被访问,外部不需访问,但需要给外部定义一个接口函数,用于外部访问内部)
1 在一个py文件里定义的类,由其他py文件调用,其实就已经有隐藏的含义了,调用者并不知道其内部逻辑,只需要用就行
2 通过python约定俗成的命名方法来命名,达到隐藏的效果:
3 为封装到内部的逻辑提供一个外部访问的接口
# _开头的变量如 _abc 代表这个属性不该被外部调用者调用
class A : _b = '你不该在外部调用这个数据属性' def __init__(self): pass a = A() print(a._b) #还是能调用到这个_b,因为这只是文字上的约束,python并没有内部逻辑帮你阻止调用 #但你看到_开头的就要明白,这个属性是不该被你调用的
#__开头的变量如__abc (__abc__这样末尾有__的不算),python会给你自动重命名为_类名__abc
class A :
__b = '你调用不到我,会报错'
def __init__(self):
print('但我可以在这里调',self.__b)
def C(self): #接口函数(访问函数)
print(self.__b,'内部就能调用')
a = A()
# print(a.__b) #调用不到__b,会报错 ,因为以及被python自动重命名为_A__b了,可以在__dict__中找到
print(A.__dict__)
print(a._A__b)
a.C() #外部调用者通过调用接口函数,访问到了在内部被封装的属性
#注意事项:
要把什么变量做成私有的需要深思熟虑(不然后期只能通过不断的添加接口函数来弥补,在一个项目里把私有的变量名再改回去是不现实的)
#来几个真正封装的例子
class cal_area : def __init__(self,name,width,length): self.name = name self.__width = width self.__length = length def result(self): print('%s的面积为%s平方米' %(self.name,self.__length*self.__width)) def port(self): return self.__width,self.__length s = cal_area('home',100,100) s.result() print(s.port()) #没法直接调用内部私有的__width和__length,只能通过新写的接口函数(也叫访问函数)来调用
#反射(又称自省)
#主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。
四个可以实现反射的函数(适用于类和对象):
hasattr(object,name):判断对象中有没有一个叫做name的方法或属性(数据属性或函数属性),
或者换句话说,能不能调用到name的方法或属性,返回bool值. 注意name是个字符串!
a = 'abc' print(hasattr(a,'__len__')) #可以传入实例化对象 print(hasattr(str,'__len__')) #也可以传入一个类 class A: def __init__(self,name): self.__name = name def __test(self): pass print(hasattr(A,'__test')) #hasattr检测的是能被调用的方法或属性,这里做了封装所以没找到
#模块也就是文件也是对象,也可用反射
import matplotlib.pyplot as plt
print(hasattr(plt, 'contourf'))
#甚至可以对当前执行文件进行反射
import sys
def a():
pass
obj = sys.modules[__name__] #获取当前执行文件名的这个模块
print(hasattr(obj, 'a'))
getattr(object,name,default=None):获取对象或者类中能被调用的方法或者属性
a = 'abc' b = getattr(a,'__len__') #在实例化对象a中,获取到叫做'__len__'的属性,并且可以调用.(没有则会报错) d = getattr(a,'adflkj','如果找不到,得到这里的值') #这样写找不到也不会报错 e = getattr(a,'adsf',default=None) print(b()) print(d) c = getattr(str,'__len__') print(c(a)) #其实getattr(object,name)就等于object.name()
setattr(object,key,value):给对象或类设置方法或属性
class A: def __init__(self,name): self.name = name def test(self): pass a = A('abc') setattr(a,'name0','ssy') print(a.__dict__) #相当于直接 a.name = 'alex' print(a.__dict__) #也可传入函数属性 def test(self) : print(self.name) setattr(A,'test',test) a.test()
delattr(object,key):删除对象或类的属性或方法,相当于 del A.key
class A: def __init__(self,name): self.name = name def test(self): pass a = A('abc') delattr(a,'name') #相当于直接 del a.name print(a.__dict__)
#反射的好处:
当多人开发一个项目时候,你需要调用别人负责实现的的类,但是对方没实现,甚至都没定义这时候怎么办?
难道要等到他实现了通知你,你才开始写代码么?这时候就可以用hasattr来进行一个判断
from XXX import A a = A('abc') #调用别人的类来实例化 if hasattr(a,'你要调用的方法') : b = getattr(a,'你要调用的方法') b() #运行调用的方法 #如果不存在,就接着下面的逻辑
这样,当你调用的类不存在时,就跳过;存在的时候即调用;你这段调用的代码其实就已经写好了,不论这个类有没有被实现。
换句话说就是你不用等别人实现这个类,再回过头来写这个调用逻辑了。
#动态导入
#以一个字符串的名字的形式导入模块
#动态导入,可以导入字符串名字的模块。但是无论你调用多少层,总是获取最顶层
a = __import__('module_test.bin') #导入相当于执行了一遍文件.
print(a) #获取的不是你导入的最低层bin,而是顶层module_test
a.bin.test() #所以得这么才能调用到,但是为了能调用到,__import__里还是得写到最低层,即'module_test.bin'
#导入的过程相当于执行了一次文件,下面拿以前的导入方式来距离
from module_test import bin
bin.test() #bin下定义的函数得通过bin这个类调用,不能直接test()
#或者直接把模块里的所有东西导入到当前,那就可以直接test()调用了
from module_test.bin import * # * 跟linux里的通配符一个意思
test()
#但是这种用*的方法没法导入_开头的函数
_test() #没找到这个函数
#不过python没有在真正逻辑层面上限制你调用,你可以这么调_test
from module_test.bin import test,_test
_test()
#或者用__import__调用也可以
a.bin._test()
#另一种更好的方法动态导入
import importlib
a = importlib.import_module('module_test.bin')
a.test()
a._test()
#为什么这里要讲动态导入呢?因为动态导入就是基于反射做的,通过反射文件下的模块。而模块其实就是一个类
比如 importlib.import_module('module_test.bin'),相当于从importlib这个类里找到一个import_module方法;
这个方法能从当前类(文件夹就是一个类)下找到module_test这个类,再从中找到bin这个类
这也就解释了为什么pycharm创建文件夹的时候,会生成一个__init__的文件,并且自动执行,其实它就是用来实例化出对象(文件夹里的文件)的
# __getattr__ 、 __setattr__ 、 __delattr__
(注意:1、形如 __XXX__ 都是类的内置方法,当不自己设置__XXX__的时候用内置的,设置跟他名字一样的__XXX__则用你设置的
2、 使用getattr、setattr、delattr本质都是在调用以上三个内置函数)
(常用)#__getattr__ : 当通过实例对象调用不存在的方法时,会自动运行类下的__getattr__方法 (如果不像下面自己设置__getattr__则调用内置的结果是输出报错信息到屏幕)
(不常用)#__delattr__: 通过实例对象在删除属性时,会自动运行__delattr__
(不常用)#__setattr__: 通过实例对象在设置属性时,会自动运行__setattr__
class Foo : name = 'abc' def __init__(self,x): self.x = x def __getattr__(self, item): print('__getattr__被执行') def __delattr__(self, item): print("__delattr__被执行")
#del self.item #无限递归了,与下面setattr同理
self.__dict__.pop(item) #这样才对
def __setattr__(self, key, value): print("__setattr__被执行") # self.key = value 不能用这种方式设置属性,因为这里这种设置方式会触发__setattr__的执行,然后再设置,无限循环 self.__dict__[key] = value #这样设置才对 a = Foo(1) a.adfdaf #类或对象调用不存在的属性时,会自动运行__getattr__ del a.name #类或对象在删除属性时,会自动运行__delattr__ a.name1 = 'cba' #触发__setattr__
#定制__getattr__ 、 __setattr__ 、 __delattr__
以上所说的这三个函数,如果自己额外定义的话,就会使用你定义的。如下为低级的自定义方法:
class A: def __init__(self, name): self.name = name def __setattr__(self, key, value): print('开始定制__setattr__函数') if type(value) is str: print('符合要求') self.__dict__[key] = value else: print('value必须为字符串类型') def __delattr__(self, item): print('请不要删除任何属性') a = A('alex') setattr(a,'age',18) a.age = 18 print(a.__dict__) delattr(a,'name') del a.name print(a.__dict__)
#利用继承与派生来完成包装(对内置函数进行二次加工)
包装:包装一个类型通常就是对已存在的类型的一些定制,这种做法可以新建、修改或删除一些原有类的功能,而其他功能保持不变。
1 class List(list): 2 ''' 3 这是我自定义的新列表类型 4 ''' 5 def append(self, object): #可以覆盖掉之前append的功能,自定义成自己的 6 if len(str(object)) <= 10: 7 super().append(object) #不用 list.append(self,object) 这种老土的调用父类的方式 8 # list.append(self,object) 9 else: 10 print('请不要添加超过10个字符以上的元素到列表中') 11 def find_mid(self): #也可以自己派生新的方法 12 id = int(len(self)*0.5) 13 print('-->%s<--' %self[id]) 14 a = List('abcd') 15 print(type(a)) #自己定义的List类(注意不是原本的list类) 16 a.append(111111111111111111) 17 print(a) 18 a.append('这样没超过10位吧') 19 print(a) 20 a.find_mid()
授权:授权是包装的一种,只是不是利用继承来实现类功能的制定。为什么叫授权呢?其实就是用这种方法选择性的开放类的各个方法的权限以及使用规则,比如我不让你用write这个方法,那我就自定一个write,但是什么也不做。
以下为用组合的方式实现授权的例子:
import time class Open: def __init__(self,filename,mode='r+',encoding='utf-8'): self.mode = mode self.encoding = encoding self.file = open(filename,mode,encoding=encoding) def __getattr__(self, item): return getattr(self.file,item) def write(self,line): #定制了你的write,在写入文本后附带上时间 t = time.strftime('%Y-%m-%d %X') self.file.write('%s <-- %s' %(line,t)) F = Open('a','w+') F.write('写入成功') F.seek(0) print(F.read())

浙公网安备 33010602011771号