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...
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.