☰
python 动态调用函数实例解析
2026/10/28 2:45:33 网站建设 项目流程

前言


所谓"动态调用函数",指的是在运行时才决定调用哪个函数——函数名可能来自一个 字符串、一份配置、一个字典的键,甚至来自用户在界面上的选择。这和平时写死的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)) # 120

getattr同样适用于实例方法,此时拿到的是绑定方法(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/execeval(name)几乎不该用极低(勿用于外部输入)



常见坑点



  1. 把函数对象和调用结果搞混


``python # ❌ 括号多写,f 变成了返回值 f = add(2, 3) # f 是 5,不是函数 # ✅ 不加括号才是函数对象的引用 f = add``



  1. 用 eval 执行来自外部的名字


``python # ❌ eval 会执行任意代码,输入被污染就是灾难 eval(user_input) # ✅ 用分发表做白名单 OPS.get(user_input)``



  1. 在函数内部用 globals() 找局部函数


``python def outer(): def inner(): return "inner" # ❌ inner 是局部名字,globals() 里没有 return globals().get("inner") # ✅ 用 locals()(注意:locals() 在函数内是快照,别指望改它生效)`更稳的做法是显式把inner` 放进一个字典再查。



  1. getattr 不带默认值导致 AttributeError


``python # ❌ 名字不对就直接炸 getattr(math, "squart") # ✅ 给个默认值再判断 fn = getattr(math, "squart", None)``



  1. 类方法和实例方法混用


``python # ❌ 从类上取函数却按实例方法调用 getattr(Robot, "walk")() # 缺少 self # ✅ 从实例上取,得到绑定方法 getattr(robot, "walk")()``



  1. 分发表里存了还没定义的函数


``python # ❌ 字典在模块顶部,引用的函数在下面才定义 OPS = {"add": add} # NameError def add(a, b): ... # ✅ 先定义函数,再建表;或把建表放进函数里延迟执行``



  1. 动态拼字符串生成调用


``python # ❌ 拼 f"{name}({args})" 再 eval,既慢又危险 # ✅ 取到可调用对象后直接传参调用 fn = OPS[name] fn(*args)``


总结




你想做的事推荐做法



在几个固定动作里选一个字典分发表

按模块里的函数名调用globals()+callable()判断

按模块 / 对象的属性名调用getattr(obj, name, default)

让别人把行为传进来直接传可调用对象(回调)

名字来自用户输入白名单分发表,绝不eval



动态调用的核心不是"高级技巧",而是理解函数是对象、名字是标签这两件事。 只要坚持"用对象引用和字典来选函数、把不可信输入挡在白名单之外",就能写出既 灵活又不会被人利用的代码。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询