Reflection - 浅谈Python反射

Reflection - 浅谈Python反射

一、字符串也可以调用函数嘛?

看一个小例子,考虑有这么一个场景,根据用户输入的url的不同,调用不同的函数,实现不同的操作,也就是一个url路由器的功能,这在web框架里是核心部件之一。下面有一个精简版的示例:

首先,有一个commons模块,它里面有几个函数,分别用于展示不同的页面,代码如下:

def login():
  print("这是一个登陆页面!")

def logout():
  print("这是一个退出页面!")

def home():
  print("这是网站主页面!")

其次,有一个visit模块,作为程序入口,接受用户输入,展示相应的页面,代码如下:(这段代码是比较初级的写法)

import commons

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  if inp == "login":
    commons.login()
  elif inp == "logout":
    commons.logout()
  elif inp == "home":
    commons.home()
  else:
    print("404")

if __name__ == '__main__':
  run()

我们运行visit.py,输入:home,页面结果如下:

请输入您想访问页面的url: home
这是网站主页面!

这就实现了一个简单的WEB路由功能,根据不同的url,执行不同的函数,获得不同的页面。

然而,让我们考虑一个问题,如果commons模块里有成百上千个函数呢(这非常正常)?。难道你在visit模块里写上成百上千个elif?显然这是不可能的!那么怎么破?

仔细观察visit中的代码,我们会发现用户输入的url字符串和相应调用的函数名好像!如果能用这个字符串直接调用函数就好了!但是,前面我们已经说了字符串是不能用来调用函数的。为了解决这个问题,python为我们提供一个强大的内置函数:getattr!我们将前面的visit修改一下,代码如下:

import commons

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  func = getattr(commons,inp)
  func()

if __name__ == '__main__':
  run()

首先说明一下getattr函数的使用方法:它接收2个参数,前面的是一个对象或者模块,后面的是一个字符串,注意了!是个字符串!

getattr(object, name[, default])

Return the value of the named attribute of object. name must be a string. If the string is the name of one of the object’s attributes, the result is the value of that attribute. For example, getattr(x, 'foobar') is equivalent to x.foobar. If the named attribute does not exist, default is returned if provided, otherwise AttributeError is raised.

例子中,用户输入储存在inp中,这个inp就是个字符串,getattr函数让程序去commons这个模块里,寻找一个叫inp的成员(是叫,不是等于),这个过程就相当于我们把一个字符串变成一个函数名的过程。然后,把获得的结果赋值给func这个变量,实际上func就指向了commons里的某个函数。最后通过调用func函数,实现对commons里函数的调用。这完全就是一个动态访问的过程,一切都不写死,全部根据用户输入来变化。

执行上面的代码,结果和最开始的是一样的。

这就是python的反射,它的核心本质其实就是利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)成员,一种基于字符串的事件驱动!

如果用户输入一个非法的url,比如jpg,由于在commons里没有同名的函数,肯定会产生运行错误,具体如下:

请输入您想访问页面的url: jpg
Traceback (most recent call last):
 File "F:/Python/pycharm/s13/reflect/visit.py", line 16, in <module>
  run()
 File "F:/Python/pycharm/s13/reflect/visit.py", line 11, in run
  func = getattr(commons,inp)
AttributeError: module 'commons' has no attribute 'jpg'

那怎么办呢?其实,python考虑的很全面了,它同样提供了一个叫hasattr的内置函数,用于判断commons中是否具有某个成员。我们将代码修改一下:

import commons

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  if hasattr(commons,inp):
    func = getattr(commons,inp)
    func()
  else:
    print("404")

if __name__ == '__main__':
  run()

通过hasattr的判断,可以防止非法输入错误,并将其统一定位到错误页面。

hasattr(object, name)

The arguments are an object and a string. The result is True if the string is the name of one of the object’s attributes, False if not. (This is implemented by calling getattr(object, name) and seeing whether it raises an AttributeError or not.)

python的四个重要内置函数:getattrhasattrdelattrsetattr较为全面的实现了基于字符串的反射机制。他们都是对内存内的模块进行操作,并不会对源文件进行修改。

setattr(object, name, value)

This is the counterpart of getattr(). The arguments are an object, a string, and an arbitrary value. The string may name an existing attribute or a new attribute. The function assigns the value to the attribute, provided the object allows it. For example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123.

delattr(object, name)

This is a relative of setattr(). The arguments are an object and a string. The string must be the name of one of the object’s attributes. The function deletes the named attribute, provided the object allows it. For example, delattr(x, 'foobar') is equivalent to del x.foobar.

二、模块导入很麻烦?来试试importlib

3.1 动态模块导入

上面的例子是在某个特定的目录结构下才能正常实现的,也就是commons和visit模块在同一目录下,并且所有的页面处理函数都在commons模块内。如下图:

但在现实使用环境中,页面处理函数往往被分类放置在不同目录的不同模块中,也就是如下图:

难道我们要在visit模块里写上一大堆的import 语句逐个导入account、manage、commons模块吗?要是有1000个这种模块呢?

刚才我们分析完了基于字符串的反射,实现了动态的函数调用功能,我们不禁会想那么能不能动态导入模块呢?这完全是可以的!

python提供了一个特殊的方法:import(字符串参数)。通过它,我们就可以实现类似的反射功能。import()方法会根据参数,动态的导入同名的模块。

我们再修改一下上面的visit模块的代码。

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  modules, func = inp.split("/")
  obj = __import__(modules)
  if hasattr(obj, func):
    func = getattr(obj, func)
    func()
  else:
    print("404")

if __name__ == '__main__':
  run()

至此,动态导入模块的问题基本都解决了,只剩下最后一个,那就是万一用户输入错误的模块名呢?比如用户输入了somemodules/find,由于实际上不存在somemodules这个模块,必然会报错!那有没有类似上面hasattr内置函数这么个功能呢?答案是没有!碰到这种,你只能通过异常处理来解决。

3.1、__ import__和importlib的区别

Python 动态导入对象,importlib.import_module()使用

一个函数运行需要根据不同项目的配置,动态导入对应的配置文件运行。

文件结构

a #文件夹
    │a.py
    │__init__.py
b #文件夹
    │b.py
    │__init__.py
    ├─c#文件夹
        │c.py
        │__init__.py

# c.py 中内容
args = {'a':1}

class C:

    def c(self):
        pass

向a模块导入c中的对象


import importlib

params = importlib.import_module('b.c.c') #绝对导入
params_ = importlib.import_module('.c.c',package='b') #相对导入

# 对象中取出需要的对象
params.args #取出变量
params.C  #取出class C
params.C.c  #取出class C 中的c 方法

区别:

__ import__需要导入的是Lib里面的文件,而importlib.import_module导入的可以是任意的文件。

三、面对反射的静态分析仍然存在缺陷?

如果我们需要获得py文件所有的api或者调用关系,可以利用ast树进行分析,或者主流的静态分析工具pycg等等,但是针对反射(字符串的调用的函数)我们无法知道这个字符串变量所调用的真实的函数,

举个例子,目录结构如下:

image-20220816160335050

model.py

# -*- ecoding: utf-8 -*-
# @ModuleName: model
# @Author: linli
# @Time: 2022/8/16 9:32

class ModelInput:
    def __init__(self, num1, num2):
        self._num1 = num1
        self._num2 = num2

    @property
    def num1(self):
        return self._num1

    @property
    def num2(self):
        return self._num2

class Model:
    def __init__(self, model_info):
        self._model_info = model_info

    def add(self, input):
        res = input.num1 + input.num2
        print(f"{self._model_info}:{res}")

def testModel2(params=list()):
    print(params)

main.py

# -*- ecoding: utf-8 -*-
# @ModuleName: main
# @Author: linli
# @Time: 2022/8/16 9:34
from requests import *
import ast


def executeModel(config, input):
    model, func = config.strip().split(".")
    model_class_name = str.upper(model[0]) + model[1:len(model)]
    module = __import__(f"lib.{model}", fromlist=[model_class_name])
    import inspect
    for obj_name, obj in inspect.getmembers(module):
        if inspect.isclass(obj): # 实例化对象
            print(obj_name, obj)
            if obj_name == model_class_name + 'Input':
                model_input = obj(*input)
            elif obj_name == model_class_name:
                model_class = obj("测试反射模型")
            else:
                pass
    # 调用对象中方法
    model_method = getattr(model_class, func)
    model_method(model_input)



def executeModel2(config, input):
    model, func = config.strip().split(".")
    module = __import__(f"lib.{model}", fromlist=True)
    if hasattr(module, func):
        func = getattr(module, func)
        func(input)
    else:
        print("模型不存在!")

def test_get_all_import_apis():
    get("baidu.com")
    ast.parse("hello world",'4')

if __name__ == '__main__':
    executeModel2("model.testModel2", [1, 2])
    executeModel("model.add", [1, 2])
    test_get_all_import_apis()

get_all_called_apis.py

利用ast获取所有调用到的api

def get_all_call_apis_from_sources(sources):
    all_call_apis = set()
    result = set()
    for source in sources:
        try:
            text = open(source, 'r').read()
            module_ast = ast.parse(text)

            import_names = get_api_import(text)

            for node in ast.walk(module_ast):
                if isinstance(node, ast.Call):
                    call_visitor = FuncCallVisitor()
                    call_visitor.visit(node.func)
                    all_call_apis.add(call_visitor.name)
        except Exception as e:
            print(e)
            continue

    return all_call_apis
{'test_get_all_import_apis', 'inspect.isclass', 'obj', 'getattr', 'len', 'executeModel2', 'func', '__import__', 'config.strip.split', 'inspect.getmembers', 'get', 'str.upper', 'model_method', 'executeModel', 'ast.parse', 'config.strip', 'print', 'hasattr'}

可以看到,最终得到的是func,而不是func变量所指的字符串。

参考文献

https://blog.csdn.net/weixin_43587472/article/details/107782878

https://blog.csdn.net/sinat_38682860/article/details/108057497

https://blog.csdn.net/xie_0723/article/details/78004649

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值