先大致粗略的说一下反射的概念,不是很准确,后续详细讲解:
1. 以字符串的形式,导入模块
2. 以字符串的形式,获取模块内部的函数,并执行
通常我们想在一个模块中导入另外一个模块,则需要通过 import xxx,所示
commons.py文件 NAME = 'XM' def f1(): print('f1')
下面要想在reflect.py文件中导入commons.py,并调用commons.py文件中的f1函数和NAME变量,通常情况下我们通过下面的方式导入
import commons # 获取变量 NAME = commons.NAME # 调用函数 commons.f1()
要想通过字符串的形式导入commons.py模块,通过__import__('xxx')即可,如果commons.py模块不存在,则会报错
commons = __import__('commons') print(commons) # 输出 # <module 'commons' from 'xxxxx/commons.py'> NAME = commons.NAME print(NAME) # 输出 # XM # 调用函数 commons.f1() # 输出 # f1
扩展:
如果要想像类似import xx.xxx.xxxx或者from xx.xxx import xxxx这两种形式导入模块,通过上述__import__('xx')已经失效,你会发现通过__import__(xx.xxx.xxxx),只能导入xx,此时在__import__('xx.xxx.xxxx',fromlist=True)即可
上面主要展示怎么以字符串的形式导入模块,是时候展示真正的技能了,之前我们学习内置模块时,有几个模块没有讲,说是放到后面讲,分别是hasattr(obj,name)、getattr(obj,name,default=None)、setattr(x,y,v)、delattr(obj,name)我们下面主要讲一下这4个模块
1.hasattr(obj,name) 此模块主要判断obj模块是否包含name变量或者函数
ret = hasattr(commons,'f1') print(ret) # 输出 # True ret = hasattr(commons,'NAME') print(ret) # 输出 # True ret = hasattr(commons,'Name') print(ret) # 输出 # False
2.getattr(obj,name,default=None)此模块主要判断obj模块是否包含name变量或者函数,default参数用于在不存在xx函数或者变量时防止报错
target_func = getattr(commons,'f1') target_func() # 输出 # f1 target_property = getattr(commons,'NAME') print(target_property) # 输出 # XM target_func = getattr(commons,'f2') print(target_func) # 输出 # Traceback (most recent call last): # File "xxx/reflect.py", line 46, in <module> # target_func = getattr(commons,'f2') # AttributeError: module 'commons' has no attribute 'f2' target_func = getattr(commons,'f2',None) print(target_func) # 输出 # None
3.settattr(x,y,v)模块主要用于动态给xx模块注入变量或者函数,可以用x.y = v更加形象的表示
# 先判断commons模块是否存在Name变量 ret = hasattr(commons,'Name') print(ret) # 输出 # False setattr(commons,'Name','DXM') setattr(commons,'f2',lambda x: x+1) ret = hasattr(commons,'Name') print(ret) # 输出 # True target_property = getattr(commons,'Name') print(target_property) # 输出 # DXM target_func = getattr(commons,'f2') ret = target_func(1) print(ret) # 输出 # 2
4.delattr(obj,name)模块主要是删除obj模块的变量或者函数
# 先判断commons模块是否存在Name变量 ret = hasattr(commons,'NAME') print(ret) # 输出 # True delattr(commons,'NAME') ret = hasattr(commons,'NAME') print(ret) # 输出 # False
反射当前模块成员:
import sys def s1(): print('s1') def s2(): print('s2') module = sys.modules[__name__] ret = hasattr(module,'s1') ret2 = getattr(module,'s2')
__setattr__,__getattr__,__delattr__三者的用法
class Foo: x = 1 def __init__(self,y): self.y = y def __getattr__(self, item): print('----> from getattr:您找的属性不存在') def __setattr__(self, key, value): print('----> from setattr:',key,'=',value) # self.key = value # 这就无限递归了 self.__dict__[key] = value # 应该使用他 def __delattr__(self, item): print('----> from delattr:',item) # del self.item # 需要无限递归 self.__dict__.pop(item) # __setattr__添加/修改属性会触发执行 f1 = Foo(10) # 因为你重写了__setattr__,凡是赋值就会触发它的运行,你啥都没写,就是根本没有赋值,除非你直接操作属性字典,否则永远无法赋值 print(f1.__dict__) f1.z = 3 print(f1.__dict__) # __delattr__删除属性的时候回触发 f1.__dict__['a'] = 3 # 可以直接添加/修改属性字典,来完成添加/修改属性的操作 del f1.a print(f1.__dict__) # __getattr__只有在使用.调用属性,并且属性不存在的时候才会触发 f1.xxxxx f1.y
__getattribute__用法以及和__getattr__的区别?
回顾__getattr__
class Foo: def __init__(self,x): self.x = x def __getattr__(self, item): print('只有属性不存在,才会执行') # return self.__dict__[item] f1 = Foo(10) print(f1.x) # 此时不会触发__getattr__ f1.xxx # 不存在属性时,会触发__getattr__ # 打印结果 # 10 # 只有属性不存在,才会执行
__getattribute__用法
class Foo: def __init__(self,x): self.x = x def __getattribute__(self, item): print('不管属性是否存在,都会执行') # return self.__dict__[item] f2 = Foo(10) print(f2.x) # 此时不会触发__getattr__ f2.xxx # 不存在属性时,会触发__getattr__ # 打印结果 # 不管属性是否存在,都会执行 # None # 不管属性是否存在,都会执行
两者同时存在时,只会触发__getattribute__,除非__getattribute__报异常
class Foo: def __init__(self,x): self.x = x def __getattr__(self, item): print('只有属性不存在,才会执行') # return self.__dict__[item] def __getattribute__(self, item): print('不管属性是否存在,都会执行') # return self.__dict__[item] f3 = Foo(10) print(f3.x) f3.xxxxx # 打印结果 # 不管属性是否存在,都会执行 # None # 不管属性是否存在,都会执行
__setitem__,__getitem__,__delitem__
''' 通过在类中实现这个3个方法__setitem__,__getitem__,__delitem__ 可以实现类似操作字典dict的访问方式,访问类的属性 ''' class Foo: def __init__(self,name): self.name = name def __setitem__(self, key, value): print('from __setitem__ key:',key,'value:',value) self.__dict__[key] = value def __getitem__(self, item): print('from __getitem__') return self.__dict__[item] def __delitem__(self, key): print('from __delitem__ key:',key) self.__dict__.pop(key) f = Foo('alex') print(f['name']) f['age'] = 123 print(f['age']) del f['age'] # 打印结果 # from __getitem__ # alex # from __setitem__ key: age value: 123 # from __getitem__ # 123 # from __delitem__ key: age
注意:这里的__setitem__、__getitem__、__delitem__只能通过字典的访问方式访问属性,如果通过“对象.属性”的方式则不会触发
总结
反射可以总结为一下几点:
1.以字符串的形式去某个模块找东西
2.以字符串的形式去某个模块中判断东西是否存在
3.以字符串的形式去某个模块设置东西
4.以字符串的形式去某个模块删除东西
用一句话说明反射,"以字符串的形式去某个模块操作成员"
项目练习
一. 基于反射机制的模拟Web框架路由系统:
使用场景:目前大多数Web页面都有切换标签的功能,切换标签时你会发现,不同标签对应的url是不一样的,那么现在我们就模拟通过输入不同的url,切换到不同的标签,项目的目录结构如下
不使用反射机制实现切换标签的代码如下:
from lib import account url = input('请输入xxxx/函数名格式的url:') if url.endswith('login'): ret = account.login() print(ret) elif url.endswith('logout'): ret = account.logout() print(ret) elif url.endswith('register'): ret = account.register() print(ret) else: print('404')
这种情况下,我们不使用反射,功能是可以完成,弊端就是如果页面中有好多标签,那我们就需要各种判断,调用各种函数,这肯定不是作为屌丝程序员的我们想要的,那么下面是时候展示真正的技能了
使用反射机制优化第一版
# 优化第一版 # 要求输入的url格式为:xxxx/函数名 # 通过getatter()函数解决了判断是否为各种函数的问题 url = input('请输入xxxx/函数格式的url:') func_name = url.split('/')[-1] if hasattr(account,func_name): target_func = getattr(account,func_name) ret = target_func() print(ret) else: print('404')
此时你会发现,哎呦!可以啊!离我们理想目标进了一大步啊,似乎从解放前回到现实了,通过hasattr()先判断模块是否存在这个成员,存在则通过getattr()获取成员,并执行,不存在则报404错误,但是这样似乎还是没有达到我们的要求,因为还需要import lib.account,其次实际的使用场景不是这样的,因为肯定是每个标签对应一个模块,不会写在一个模块中,那么问题来了,难道我还需要各种import xxx?下面我们通过另外一种方法解决这个问题
# 优化第二版 # 要求输入的url格式为:模块名/函数名 # 通过__import__('xxx')解决了导入模块的问题 # 通过getattr()解决了函数判断问题 # 通过hasatter()解决了函数是否存在的判断问题 url = input('请输入模块/函数名格式的url:') module_name,module_func = url.split('/') moudle = __import__('lib.' + module_name,fromlist=True) if hasattr(moudle,module_func): target_func = getattr(moudle,module_name) ret = target_func() print(ret) else: print('404')
我们在优化第一版的基础上加了__import__('xxx')的方法导入模块,这样只要值输入url时指定格式模块名/函数名,就可以很好的解决上面说的问题了
注意:因为lib模块中包含多个模块,所以在使用__import__('xxx',fromlist=True)别忘记加后面的参数
二. 二次加工标准类型(包装)
包装:Python为大家提供了标准的数据模型,以及丰富的内置方法,其实很多情况下我们需要基于标准数据模型来定制我们自己的数据模型,新增/修改方法,这就用到了我们刚学的继承/派生知识(其他标准类型,可以通过下面的方式进行二次加工)
1.基于继承实现二次加工
class List(list): def __init__(self,item): super().__init__(item)def append(self, object): if not isinstance(object,int): raise TypeError('Must be int') super().append(object) @property def mid(self): # 新增自己的属性 index = len(self) // 2 return self[index] li = List([1,2,3,4],tag=False) print(li) li.append(5) print(li) # print(li.append('12345')) # 报错 必须为int类型 print(li.mid) # 其他继承list方法仍然可以使用 li.insert(0,123) print(li)
2.list的clear()方法加权限限制
class List(list): def __init__(self,item,tag=False): super().__init__(item) self.tag = tag def append(self, object): if not isinstance(object,int): raise TypeError('Must be int') super().append(object) def clear(self): if not self.tag: raise PermissionError super().clear() @property def mid(self): # 新增自己的属性 index = len(self) // 2 return self[index] li = List([1,2,3,4],tag=False) print(li) li.append(5) print(li) # print(li.append('12345')) # 报错 必须为int类型 # li.clear() # 报错 没有权限 print(li.mid) # 其他继承list方法仍然可以使用 li.insert(0,123) print(li)
3.授权示范一
说明:自定义一个类似open的文件操作对象,你会发现FileHandle没有自定义seek()、read()等方法,为什么调用的时候,可以通过对象调用到,这一切都归功于__getattr__,聪明的你已经发现,在FileHandle类底部有一个__getattr__方法,之前我们讲解过,__getattr__触发的条件是,使用对象/类.属性的方式调用,并且属性不存在的时候会触发,而且我们返回的是通过反射getattr(self.file_obj,item)的方法,由于self.file_obj实质就是open()的文件句柄,所以实质就是在调用文件句柄的seek()等函数
import time class FileHandle: def __init__(self,filename,mode='r',encoding='utf-8'): self.file_obj = open(filename,mode,encoding=encoding) def write(self,line): t = time.strftime('%Y-%m-%d %T') self.file_obj.write('%s %s' % (t,line)) # 只有使用.调用属性,并且属性不存在会触发 这里是关键点 def __getattr__(self, item): print(self.file_obj,item) return getattr(self.file_obj,item) f1 = FileHandle('a.txt','w+') print(f1) f1.write('你好啊!') f1.seek(0) print(f1.read()) f1.close()
4.授权示范二(在示范一的基础上加上对'b'模式的支持)
# 授权示范二:支持b模式 class FileHandle: def __init__(self,filename,mode='r',encoding='utf-8'): if 'b' in mode: self.file_obj = open(filename,mode) else: self.file_obj = open(filename,mode,encoding=encoding) self.filename = filename self.mode = mode self.encoding = encoding
def write(self,line): if 'b' in self.mode: if not isinstance(line,bytes): raise TypeError('must be bytes') self.file_obj.write(line) def __getattr__(self, item): return getattr(self.file_obj,item) def __str__(self): if 'b' in self.mode: res = '<io.BufferedReader name=%s>' % self.filename else: res = '<io.TxtIOWrapper name=%s encoding=%s>' % (self.filename,self.encoding) return res f2 = FileHandle('b.txt','wb') # f2.write('你好啊!') # 报错 must be bytes f2.write('XM美女,你好啊!'.encode('utf-8')) print(f2) f2.close()
5.基于上面的授权模式,优化List
class List: def __init__(self,seq): self.seq = seq def append(self,object): if not isinstance(object,int): raise TypeError('must be int') self.seq.append(object) @property def mid(self): ''' 新增自己的方法 :return: ''' index = len(self.seq) // 2 return self.seq[index] def __getattr__(self, item): return getattr(self.seq,item) def __str__(self): return str(self.seq) li = List([1,2,3,4]) print(li) li.append(5) print(li) # li.append('12345') # 报错 must be int print(li.mid) #基于授权,获取insert方法 li.insert(0,1234567) print(li)