python中的反射及在动态导入中的使用详解

    科技2024-04-21  11

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

    Processed: 0.029, SQL: 9