报错原文
Traceback (most recent call last):
File "locust/core.py", line 305, in execute_task
task(self, *args, **kwargs)
TypeError: 'str' object is not callable
GitHub 真实案例
locustio/locust#450 — 用户写压测脚本,把 tasks 定义成字典而非可调用对象列表:
# ❌ 错误
class UserBehavior(TaskSet):
tasks = {"index": 2, "profile": 1}
# ✅ 正确
class UserBehavior(TaskSet):
tasks = [index, profile] # 传函数对象本身
框架遍历字典 key("index"、"profile"),试图 task(self, *args, **kwargs) 调用——字符串不能加括号。这是表层原因,但真正需要回答的是:为什么 Locust 在拿到 error 之前不知道 tasks 配置错了?这个"静默延迟"才是中级玩家该搞明白的。
根因:Python 的 callable 协议
Python 判断一个对象能不能加 () 调用,只看一件事:
hasattr(obj, '__call__')
函数有 __call__、类有 __call__、实现了 __call__ 的实例也可以调用。但 str、int、list 这些内置类型没有 __call__——它们不是「调用」,而是「构造」。
>>> callable("hello")
False
>>> callable(print)
True
>>> callable(str)
True # str 是类,类是可调用的
问题出在哪?Python 不会在「赋值」时做类型检查。 你在模块级写 tasks = {...},框架在运行时才遍历它。这中间可能隔了十几个文件 import、几十层继承——赋值点离炸点十万八千里。
五种生产级触发场景
场景 1:模块级命名空间污染(不只是「别用 str」)
# config.py
type = "production" # 覆盖了内置 type()
id = 42 # 覆盖了内置 id()
filter = {"debug": True} # 覆盖了内置 filter()
# 另一个模块
from config import *
print(type("hello")) # TypeError: 'str' object is not callable
初级解法是「别用内置名」。中级解法是知道即使你避开了,from xxx import * 也可能污染你的命名空间。而且 builtins 模块还在——你可以恢复:
import builtins
result = builtins.type("hello") # 绕过当前命名空间
排查技巧:
print(globals().get('type'))看当前作用域里type指向什么。
场景 2:装饰器返回了字符串而不是函数(中级核心)
这是出镜率最高的中级场景:
# ❌ 错误
def logger(func):
return f"[LOG] wrapping {func.__name__}" # 返回字符串!
@logger
def process_data():
return "done"
process_data()
# TypeError: 'str' object is not callable
装饰器的本质是 process_data = logger(process_data)。logger 返回了一个字符串,于是 process_data 就变成了字符串——装饰器返回什么,被装饰的名字就绑定什么。
正确写法:
def logger(func):
def wrapper(*args, **kwargs):
print(f"[LOG] calling {func.__name__}")
return func(*args, **kwargs)
return wrapper # ✅ 返回函数对象,不是调用结果
更隐蔽的变体——装饰器工厂忘了返回真正的装饰器:
# ❌ 错误
def retry(times=3):
def decorator(func):
# ... 定义 wrapper ...
return wrapper
return "retry enabled" # 忘了返回 decorator!
@retry(3)
def fetch():
...
# fetch 变成字符串 "retry enabled"
铁律:装饰器函数的 return 语句,要么返回一个 callable,要么把代码删了。没第三种选择。
场景 3:__init__ 中属性覆盖了父类方法
class BaseHandler:
def handle(self):
return "base response"
class ChildHandler(BaseHandler):
def __init__(self, config):
self.handle = config.get("handle", "default") # 覆盖了父类方法!
h = ChildHandler({})
h.handle() # TypeError: 'str' object is not callable
实例属性 self.handle 在 MRO 查找中优先级高于类方法 handle。当你 h.handle() 时,Python 按 instance.__dict__ → class.__dict__ → parent.__dict__ 顺序找——在第一步就找到了字符串,不继续往上了。
修复:别让实例属性和方法同名。
class ChildHandler(BaseHandler):
def __init__(self, config):
self.handle_path = config.get("handle", "default") # 换名字
场景 4:动态调用时字符串与方法混淆
class Worker:
def task_a(self): return "A done"
def task_b(self): return "B done"
w = Worker()
task_name = "task_a"
result = task_name(w) # TypeError,task_name 是字符串
# 正确
method = getattr(w, task_name)
result = method() # ✅
为什么这在中级场景里高频出现?因为很多框架(Celery、Airflow、自定义任务调度器)需要根据配置名称动态分发任务。如果配置里存的是字符串而调度代码没有调 getattr,运行时才会炸。
场景 5:__call__ 实现本身有 bug
class LazyLoader:
def __init__(self):
self._loaded = False
self._result = None
def __call__(self):
if not self._loaded:
self._result = self._load()
self._loaded = True
return self._result # ✅ 正常
def _load(self):
return "data loaded"
没问题——但如果 _load 写成这样呢:
def _load(self):
self._result = "data loaded" # 设了属性却没 return
# 隐式 return None
那么第一次 instance() 会返回 "data loaded",没问题。但第二次 instance() 时 self._loaded 已经是 True,跳过了 _load,直接返回 self._result,本以为还是数据,实际被某处错误地设成了 None 或字符串——开始产生各种奇怪的 is not callable。
排查技巧:
print(callable(your_obj), type(your_obj))在每次调用前检查。
中级排障流程
遇到 X is not callable,按以下顺序排查,不要跳步:
1. 定位:错误行是哪个变量被调用了?
↓
2. 类型:print(type(variable_name))
— 是 str/int/list → 赋值点有问题
— 是 NoneType → 函数没 return 或装饰器没返回 callable
↓
3. 追溯:这个变量最后一次被赋值在哪?
— 搜索 = 号 ← 直接赋值
— 搜索 @ 号 ← 装饰器赋值
— 搜索 import ← 模块导入覆盖
↓
4. 验证:print(callable(variable_name))
— False → 确认根因
↓
5. 修复:改命名 / 改装饰器返回值 / 加 getattr
一个一行定位命令:
import inspect
# 在报错行之前插入
print(f"is_callable={callable(target)}, type={type(target).__name__}, "
f"assigned_at={inspect.getmodule(target).__file__ if inspect.ismodule(target) else 'N/A'}")
生产环境的预防措施
1. 类型注解 + mypy:
from typing import Callable
tasks: list[Callable] = [] # mypy 会在赋值时告警
2. 装饰器强制返回 callable(防御性代码):
def safe_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
assert callable(wrapper), f"Decorator must return callable, got {type(wrapper)}"
return wrapper
3. 类属性命名规范:
方法用动词(handle、process、render),属性用名词(handler_name、process_config)——永远不会撞车。
总结
| 层级 | 理解 |
|---|---|
| 初级 | "别用 str 做变量名" |
| 中级 | Python 对象模型里只有实现了 __call__ 的才能加括号;赋值不检查类型,装饰器返回值决定了被装饰名称的类型;实例属性优先于类方法(MRO);动态调用需要 getattr |
| 记忆锚点 | 赋值点 ≠ 炸点。不要只看报错行——往上找最后一次赋值 |
同类家族
'NoneType' object is not callable→ 函数忘写 return / 装饰器返回 None / 变量未初始化'int' object is not callable→ 同名变量覆盖内置 int'module' object is not callable→ 把模块当函数调了,少了一层 import'list' object is not callable→ 同名变量覆盖内置 list
本文由 admin 原创,转载请注明出处。
评论
0