python 动态调用函数实例解析
前言所谓动态调用函数指的是在运行时才决定调用哪个函数——函数名可能来自一个 字符串、一份配置、一个字典的键甚至来自用户在界面上的选择。这和平时写死的foo()不一样代码在写的时候并不知道要调用谁。一个很常见的误解是认为动态调用必须借助eval()或exec()才能做到。这 其实是个危险的坏习惯eval()和exec()会执行任意 Python 代码一旦名字 来自不可信输入等于把整个程序交给了对方。本文只讲用对象引用、字典和getattr这类安全手段完成动态调用不推荐任何形式的eval拼接。这里的动态调用本质是两件事函数的一等公民first-class object特性 函数可以像数字、字符串一样被赋值、放进容器、当参数传以及对名字空间 namespace的查找globals()/locals()/getattr。把这两点弄清楚 动态调用就没有神秘感了。下面按有哪些方式、各自适用什么场景、怎么保证安全 来讲。一、函数的本质是对象名字只是标签在 Python 里def是一条可执行语句它创建一个函数对象然后把这个对象 绑定到名字上。名字和对象是两回事同一个对象可以有多个名字。# 适用于 Python 3.8def add(a, b):return a bf add # 没有括号f 和 add 指向同一个函数对象print(f is add) # Trueprint(f(2, 3)) # 5print(add(2, 3)) # 5注意f add后面没有括号。带了括号就变成调用并保存返回值这是初学 者最常犯的错。既然函数是对象它就可以被放进列表、元组、字典也可以作为 返回值从另一个函数里传出来。这就是动态调用的基础。二、按名字查找字典分发表最常用如果可选的动作是有限的几个最清晰、最安全也最快的做法是建一张分发表 dispatch table——用字符串当键函数对象当值。# 适用于 Python 3.8def add(a, b):return a bdef sub(a, b):return a - bdef mul(a, b):return a * bOPS {add: add,sub: sub,mul: mul,}def calculate(op_name, a, b):try:op OPS[op_name]except KeyError:raise ValueError(f未知的运算: {op_name!r}) from Nonereturn op(a, b)print(calculate(add, 3, 4)) # 7print(calculate(sub, 10, 4)) # 6print(calculate(mul, 3, 4)) # 12这张表的优点是白名单只有登记过的名字能被调用别的名字一律报ValueError。 比根据字符串去猜函数安全得多。这里raise ... from None是把原来的KeyError链隐藏掉只暴露干净的ValueError这是从 3.3 起就有的语法。三、按字符串拿名字globals() 与 locals()如果想直接用函数名转字符串来查找可以用globals()这个内置函数它返回 当前模块的全局命名空间字典。字典的.get()方法可以在找不到时返回None 配合callable()判断就不用担心KeyError。# 适用于 Python 3.8def greet():return hellodef bye():return byedef call_by_name(name):fn globals().get(name)if not callable(fn):raise ValueError(f没有名为 {name!r} 的可调用对象)return fn()print(call_by_name(greet)) # helloprint(call_by_name(bye)) # bye这里有个关键陷阱globals()返回的是定义这段代码的那个模块的全局名字 不是调用方的。如果call_by_name想找的函数是定义在别的函数体内部的局部 函数它根本不在globals()里这时要用locals()或者干脆老老实实传一张 字典。这也是为什么生产代码里更推荐第二节的显式分发表。四、按名字在模块或对象上取属性getattr()如果要动态调用的函数属于某个模块或对象比如用哪个模块的哪个函数由配置 决定就用getattr()。它的签名是getattr(object, name, default)——第三个 参数是缺省值给了它找不到时就不会抛AttributeError而是返回缺省值。# 适用于 Python 3.8import mathdef call_math(name, arg):fn getattr(math, name, None)if not callable(fn):raise ValueError(fmath 模块里没有可调用对象 {name!r})return fn(arg)print(call_math(sqrt, 16)) # 4.0print(call_math(factorial, 5)) # 120getattr同样适用于实例方法此时拿到的是绑定方法bound method已经 带上了self可以直接调用# 适用于 Python 3.8class Robot:def __init__(self, name):self.name namedef walk(self):return f{self.name} 正在行走def stop(self):return f{self.name} 已停止robot Robot(R2)action getattr(robot, walk) # 取得绑定方法print(action()) # R2 正在行走这里如果写成Robot.walk类上的函数调用时就必须自己传self行为不同。 这个区别在插件式架构里特别容易踩。五、把可调用对象当参数传回调最动态也最干净的用法是干脆不传名字直接传可调用对象本身也就是回调 callback。这样调用方根本不关心对方是普通函数、绑定方法还是lambda。# 适用于 Python 3.8def apply_all(func, values):return [func(v) for v in values]print(apply_all(abs, [-1, -2, -3])) # [1, 2, 3]print(apply_all(str.upper, [a, b])) # [A, B]print(apply_all(lambda x: x * x, [1, 2, 3])) # [1, 4, 9]只用协议说话能被()调用就行不关心名字这是 Python 里最推荐 的动态调用方式安全性和可读性都最好。六、四种方式对照方式典型写法适用场景安全性字典分发表OPS[name]动作有限、可控高白名单名字查找globals().get(name)模块级函数按名调用中受限于全局名属性查找getattr(obj, name, None)插件 / 配置驱动中高可设默认值直接传对象apply_all(func, xs)回调、策略注入最高eval/execeval(name)几乎不该用极低勿用于外部输入常见坑点把函数对象和调用结果搞混python # ❌ 括号多写f 变成了返回值 f add(2, 3) # f 是 5不是函数 # ✅ 不加括号才是函数对象的引用 f add用 eval 执行来自外部的名字python # ❌ eval 会执行任意代码输入被污染就是灾难 eval(user_input) # ✅ 用分发表做白名单 OPS.get(user_input)在函数内部用 globals() 找局部函数python def outer(): def inner(): return inner # ❌ inner 是局部名字globals() 里没有 return globals().get(inner) # ✅ 用 locals()注意locals() 在函数内是快照别指望改它生效更稳的做法是显式把inner 放进一个字典再查。getattr 不带默认值导致 AttributeErrorpython # ❌ 名字不对就直接炸 getattr(math, squart) # ✅ 给个默认值再判断 fn getattr(math, squart, None)类方法和实例方法混用python # ❌ 从类上取函数却按实例方法调用 getattr(Robot, walk)() # 缺少 self # ✅ 从实例上取得到绑定方法 getattr(robot, walk)()分发表里存了还没定义的函数python # ❌ 字典在模块顶部引用的函数在下面才定义 OPS {add: add} # NameError def add(a, b): ... # ✅ 先定义函数再建表或把建表放进函数里延迟执行动态拼字符串生成调用python # ❌ 拼 f{name}({args}) 再 eval既慢又危险 # ✅ 取到可调用对象后直接传参调用 fn OPS[name] fn(*args)总结你想做的事推荐做法在几个固定动作里选一个字典分发表按模块里的函数名调用globals()callable()判断按模块 / 对象的属性名调用getattr(obj, name, default)让别人把行为传进来直接传可调用对象回调名字来自用户输入白名单分发表绝不eval动态调用的核心不是高级技巧而是理解函数是对象、名字是标签这两件事。 只要坚持用对象引用和字典来选函数、把不可信输入挡在白名单之外就能写出既 灵活又不会被人利用的代码。