前言
所谓"动态调用函数",指的是在运行时才决定调用哪个函数——函数名可能来自一个 字符串、一份配置、一个字典的键,甚至来自用户在界面上的选择。这和平时写死的foo()不一样:代码在写的时候并不知道要调用谁。
一个很常见的误解,是认为"动态调用"必须借助eval()或exec()才能做到。这 其实是个危险的坏习惯:eval()和exec()会执行任意 Python 代码,一旦名字 来自不可信输入,等于把整个程序交给了对方。本文只讲用对象引用、字典和getattr这类安全手段完成动态调用,不推荐任何形式的eval拼接。
这里的"动态调用"本质是两件事:函数的一等公民(first-class object)特性 (函数可以像数字、字符串一样被赋值、放进容器、当参数传),以及对名字空间 (namespace)的查找(globals()/locals()/getattr)。把这两点弄清楚, 动态调用就没有神秘感了。下面按"有哪些方式、各自适用什么场景、怎么保证安全" 来讲。
一、函数的本质是对象,名字只是标签
在 Python 里,def是一条可执行语句:它创建一个函数对象,然后把这个对象 绑定到名字上。名字和对象是两回事,同一个对象可以有多个名字。
# 适用于 Python 3.8+
def add(a, b):
return a + b
f = add # 没有括号,f 和 add 指向同一个函数对象
print(f is add) # True
print(f(2, 3)) # 5
print(add(2, 3)) # 5注意f = add后面没有括号。带了括号就变成"调用并保存返回值",这是初学 者最常犯的错。既然函数是对象,它就可以被放进列表、元组、字典,也可以作为 返回值从另一个函数里传出来。这就是动态调用的基础。
二、按名字查找:字典分发表最常用
如果可选的动作是有限的几个,最清晰、最安全也最快的做法是建一张分发表 (dispatch table)——用字符串当键,函数对象当值。
# 适用于 Python 3.8+
def add(a, b):
return a + b
def sub(a, b):
return a - b
def mul(a, b):
return a * b
OPS = {
"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 None
return op(a, b)
print(calculate("add", 3, 4)) # 7
print(calculate("sub", 10, 4)) # 6
print(calculate("mul", 3, 4)) # 12这张表的优点是白名单:只有登记过的名字能被调用,别的名字一律报ValueError。 比"根据字符串去猜函数"安全得多。这里raise ... from None是把原来的KeyError链隐藏掉,只暴露干净的ValueError,这是从 3.3 起就有的语法。
三、按字符串拿名字:globals() 与 locals()
如果想直接用"函数名转字符串"来查找,可以用globals()这个内置函数,它返回 当前模块的全局命名空间字典。字典的.get()方法可以在找不到时返回None, 配合callable()判断,就不用担心KeyError。
# 适用于 Python 3.8+
def greet():
return "hello"
def bye():
return "bye"
def call_by_name(name):
fn = globals().get(name)
if not callable(fn):
raise ValueError(f"没有名为 {name!r} 的可调用对象")
return fn()
print(call_by_name("greet")) # hello
print(call_by_name("bye")) # bye这里有个关键陷阱:globals()返回的是"定义这段代码的那个模块"的全局名字, 不是"调用方"的。如果call_by_name想找的函数是定义在别的函数体内部的局部 函数,它根本不在globals()里,这时要用locals(),或者干脆老老实实传一张 字典。这也是为什么生产代码里更推荐第二节的显式分发表。
四、按名字在模块或对象上取属性:getattr()
如果要动态调用的函数属于某个模块或对象(比如"用哪个模块的哪个函数"由配置 决定),就用getattr()。它的签名是getattr(object, name, default)——第三个 参数是缺省值,给了它,找不到时就不会抛AttributeError,而是返回缺省值。
# 适用于 Python 3.8+
import math
def call_math(name, arg):
fn = getattr(math, name, None)
if not callable(fn):
raise ValueError(f"math 模块里没有可调用对象 {name!r}")
return fn(arg)
print(call_math("sqrt", 16)) # 4.0
print(call_math("factorial", 5)) # 120getattr同样适用于实例方法,此时拿到的是绑定方法(bound method),已经 带上了self,可以直接调用:
# 适用于 Python 3.8+
class Robot:
def __init__(self, name):
self.name = name
def 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.8+
def 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/exec | eval(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 不带默认值导致 AttributeError
``python # ❌ 名字不对就直接炸 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 |
动态调用的核心不是"高级技巧",而是理解函数是对象、名字是标签这两件事。 只要坚持"用对象引用和字典来选函数、把不可信输入挡在白名单之外",就能写出既 灵活又不会被人利用的代码。