【01】TypeError: 'str' object is not callable —— 你调用的根本不是函数

预计阅读时间:9 分钟

报错原文

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__ 的实例也可以调用。但 strintlist 这些内置类型没有 __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. 类属性命名规范:

方法用动词(handleprocessrender),属性用名词(handler_nameprocess_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
暂无评论,来发表第一条评论吧

发表评论

登录 后发表评论

发现更多