pythondjangocelerylate-bindingearly-binding

Celery signatures .s(): early or late binding?


This blog post of Adam Johnson perfectly illustrates the difference between early binding and late binding closures in Python. This is also explained here.

I've got a function my_func which corresponds to a Celery task (it's decorated with @shared_task). It expects some arguments. I run it with the following piece of code, using Celery signatures as described in the documentation (see also this StackOverflow answer):

from functools import partial
from django.db import transaction
from example.tasks import my_func

# next line can be found at the end of some other functions which involve a transaction
transaction.on_commit(my_func.s(param1, param2).delay))

Are Celery signatures early binding or late binding? And how to demonstrate it in an elegant way?

If they are early binding, the following line of code should be equivalent:

transaction.on_commit(partial(my_func.delay, param1, param2))

If they are late binding, I think I could be facing a pesky bug with my current code, in some edge cases...


Solution

  • So, according to your link, in this section:

    partial() is early binding, whilst all the prior techniques are late binding.

    Early binding here means that function arguments are resolved when calling partial()

    At the risk of being pedantic, I would say that this is mixing terminology. Late/early binding applies to how free variables are evaluated when a function that creates a closures is called.

    When you are talking about how arguments are evaluated during a function call, that's different. For all function calls in python the arguments are evaluated fully when the function is called. That is a consequence of Python's evaluation strategy, which is strict (also called greedy) as opposed to non-strict (also called lazy).

    The strict/greed evaluation of function arguments is analogous to early-binding in closures. So using this way of phrasing it, yes, .s is "early binding".

    That is, because my_func.s(param1, param2) is just a function call, then the arguments are eagerly evaluated.


    Note, one example of a non-strict evaluation strategy is call-by-name. Languages like scala for an example) support call by name.

    There is also Haskell, which has call-by-need, which is like a cached version fo call-by-name. Because Haskell is purely functional, and doesn't allow variables to be re-assigned, you don't see bugs like the ones you would be worried about though.