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
备注:内容仅供参考。
