这篇博客记录下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上关注我,如果有问题欢迎在底下的评论区交流,谢谢。