这篇博客记录下python中关于反射的知识点,同时结合实例看看其在动态导入中的应用。
反射(Reflect)是指面向对象编程中通过字符串动态获取对象的类型、属性和方法等信息。
想象下面的场景,我们编写了几十个插件供业务程序来使用,而具体业务中使用哪些插件是由配置文件来控制的,或者由用户输入的。不管是配置文件还是用户输入,都是采用字符串形式来表示插件名,我们需要检查字符串代表的插件名是否存在,如果存在需要获取对应的插件对象并调用。
文字表述不太直观,看如下的一个项目
在plugins模块中有两个插件,分别是plugin1和plugin2,
# plugin1 class Calc1: def process(self): print('class name is {}'.format(self.__class__.__name__)) # plugin2 class Calc2: def process(self): print('class name is {}'.format(self.__class__.__name__))并在__init__.py中进行了导入
from plugins import plugin1, plugin2对__init__.py的使用感兴趣可以查看另一篇博客《python目录中的__init__.py文件详解》
具体的业务代码在app中,如下
import plugins if __name__ == '__main__': name = input('请输入想要执行的plugin:') if name == 'plugin1': plugins.plugin1.Calc1().process() elif name == 'plugin2': plugins.plugin2.Calc2().process()根据用户的输入来决定使用哪一个插件。因为用户的输入为字符串,不能通过类似name.Calc()的方法来调用,所以要加入if...else...的结构来进行判断。
看起来没什么问题。
但是往往一个项目中的插件数量高达几十个,如果都要用if...else...结构来判断的话代码就太臃肿了,有没有更简单快捷的方法呢?
就需要用到反射了。
python内置了4个函数用来实现反射的4种操作,分别是
hasattr(obj,str) # 检查str对应的方法或者属性是否在obj对象内 getattr(obj,str) # 获取obj中和str相同的方法或者属性 setattr(obj,str,val) # 为obj对象设置一个名字为str,内容为val的方法或者属性 delattr(obj,str) # 删除obj对象中和str相同的方法或者属性下面简单测试下
import plugins if __name__ == '__main__': name = input('请输入想要执行的plugin:') print(hasattr(plugins, name))结果如下
请输入想要执行的plugin:plugin3 False 请输入想要执行的plugin:plugin1 True可见hasattr()方法能顺利完成检测的目的。
修改下上面用if...else...实现的代码如下
import plugins if __name__ == '__main__': name = input('请输入想要执行的plugin:') if hasattr(plugins, name): plugin = getattr(plugins, name) print(plugin) else: print('所输入的插件不存在')结果如下
请输入想要执行的plugin:plugin1 <module 'plugins.plugin1' from 'C:\\Users\\Admin\\PycharmProjects\\Reflect\\plugins\\plugin1.py'>可以看到,即使有几十个插件供检测这里的代码也不用变。
下面对反射进行一点引申,来完成实际项目中很常见的一个功能:动态导入。
如果对Django比较熟悉的朋友可以类比配置文件中的中间件配置MIDDLEWARE部分,例如
MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'middleware.middleware.loginMiddleware', ]实际在业务中使用哪些中间件由这里的配置动态决定,也就是所谓的动态导入。因为这里使用字符串来表示具体的中间件类,所以需要使用反射来进行实际的获取和使用。
还是用上面的例子来实现一个我们自己的动态导入。
首先在配置文件settings.py中定义如下列表,分别将两个插件类的地址放在这里,注意是用点号来连接路径
PLUGINS = [ 'plugins.plugin1.Calc1', 'plugins.plugin2.Calc2', ]然后修改业务代码如下
from importlib import import_module import settings if __name__ == '__main__': for plugin in settings.PLUGINS: module_name, class_name = plugin.rsplit('.', maxsplit=1) module = import_module(module_name) if hasattr(module, class_name): obj = getattr(module, class_name)() obj.process()针对settings.PLUGINS这个列表,循环处理其中的每一个元素。首先利用rsplit()方法分离出模块和类名,方便后续使用。然后导入模块,值得注意的是因为字符串不能直接被导入,还需要借助import_module()方法。最后就是我们前面提到的反射的使用,在模块中获取具体的类然后进行操作。值得注意的是,因为采用了动态导入,不需要再导入plugins模块了,也就避免了__init__.py文件书写错误导致的模块导入问题。
采用动态导入使得程序的可扩展性变得很高,插件的加减完全由配置文件来控制,不需要对业务代码进行任何修改,符合开闭原则。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。