PAYATHON 2026

Celery 报错:'function' object has no attribute 'delay'

排障赵师傅

结论

问题出在 views.py 的名称覆盖。Flask 视图函数和 Celery task 都叫 send_sms,后定义的视图函数覆盖了当前模块中的同名任务对象。这与 RabbitMQ、Twilio 或 Celery 任务注册无关。

因此,视图函数里的:

send_sms.delay(...)

调用的是当前的 Flask 视图函数。普通 Python 函数没有 delay 方法,于是抛出:

AttributeError: 'function' object has no attribute 'delay'

send_email 可以正常运行,因为它没有被同名视图函数覆盖。

任务已注册,为什么仍会报错

Celery worker 的任务注册表与 Web 进程中的 Python 名称绑定互不影响。

Celery 启动时,任务对象已经注册为:

app.send_sms

之后定义同名视图函数并不会删除注册表中的任务,但会让当前模块的全局变量 send_sms 指向新定义的视图函数:

# 此时 send_sms 指向 Celery Task
@celery.task
def send_sms(...):
    ...

# 此后 send_sms 指向 Flask 视图函数
@app.route('/send_sms')
def send_sms():
    ...

因此,celery inspect registered 能查到任务,但 Flask 请求中的 send_sms 仍可能是普通函数。错误发生在消息发送到 broker 之前,修改 RabbitMQ 配置或重启 worker 无法解决这个名称冲突。

解决方法:使用不同的函数名

分别为任务和视图使用清楚且不重复的名称:

# tasks.py

@celery.task
def send_sms_task(recipients, text_body):
    for number in recipients:
        num = '+61' + str(number)
        msg = text_body + 'this message to' + num
        client.messages.create(
            to=num,
            from_="+14804054823",
            body=msg,
        )

然后在视图中导入并调用任务:

# views.py

from app.tasks import send_sms_task


@app.route('/send_sms', methods=['GET', 'POST'])
@roles_accepted('Admin')
def send_sms_view():
    form = SendMailForm(request.form)

    if request.method == 'POST' and form.validate_on_submit():
        recipients = db.session.query(User.mobile).all()
        list_recipients = list(filter(None, zip(*recipients)[0]))

        send_sms_task.delay(list_recipients, form.text.data)

    return render_template('send_generic.html', form=form)

如果需要保留原来的 Celery 任务名称,可以显式指定:

@celery.task(name='app.send_sms')
def send_sms_task(recipients, text_body):
    ...

此时 Python 变量名为 send_sms_task,Celery 注册名称仍是 app.send_sms。

通过模块访问任务

还可以导入整个任务模块,避免将 task 直接绑定到容易发生冲突的名称:

from app import tasks


@app.route('/send_sms', methods=['GET', 'POST'])
@roles_accepted('Admin')
def send_sms():
    form = SendMailForm(request.form)

    if request.method == 'POST' and form.validate_on_submit():
        recipients = db.session.query(User.mobile).all()
        list_recipients = list(filter(None, zip(*recipients)[0]))

        tasks.send_sms.delay(list_recipients, form.text.data)

    return render_template('send_generic.html', form=form)

如果这种导入方式引起循环依赖,可以把 Celery tasks 放入独立的 tasks.py,再由视图模块导入该模块。

确认名称是否被覆盖

在调用 .delay() 前检查对象:

print(send_sms)
print(type(send_sms))
print(send_sms.__module__)
print(getattr(send_sms, 'delay', None))

如果输出类似:

<function send_sms at 0x...>
<class 'function'>
None

当前名称指向的就是普通函数。

正常的 Celery task 对象通常具有可调用的 delay 属性:

assert callable(send_sms_task.delay)

还可以检查 Celery 注册表:

print(celery.tasks.get('app.send_sms'))

如果注册表中存在任务,而当前 send_sms 是普通 function,就可以确认问题来自 Python 名称覆盖,而非任务注册失败。

其他需要检查的细节

调用任务时,应传入处理后的 list_recipients,不要传递数据库查询的原始结果:

send_sms_task.delay(list_recipients, form.text.data)

在 Python 3 中,filter() 返回迭代器。Celery 使用 JSON 等序列化器时,通常无法直接序列化这个迭代器,因此要先转换为列表:

list_recipients = list(filter(None, ...))

模板渲染时通常也不需要传入:

send_sms=send_sms

这里传递的是同名视图函数,容易让任务和视图更加混乱。模板不需要该对象时,可以直接删除。

三类名称可以按下面的方式分开:

Celery task 函数:send_sms_task
Flask 视图函数:send_sms_view
路由地址:/send_sms

备注:内容仅供参考。