I’ve inherited an API to maintain.
Users can pass a callback function to the class which gets called on some event. The callback function is currently passed in a single argument.
I need to change the expected signature of the callback function to accept additional arguments.
This would be fine if everyones’ callback function took in *args and **kwargs, but unfortunately they don’t and it’s too much work to get everyone to change them.
What is the cleanest way for me to make the change in the API while keeping the callback backwards compatible?
I have this as a shoe-in but it’s not very future proof, nor is it very elegant:
...
if self._callback.func_code.co_argcount > 1:
self._callback( data, additional_arg )
else:
self._callback( data )
...
2
In Python, it’s “Easier to ask for forgiveness than permission” – it is common “Pythonic” practice to use exceptions and error handling, rather than e.g. if
checking up-front (“Look before you leap”) to handle potential problems. The documentation provides a few examples that demonstrate where the latter can really cause problems – if the situation changes between the look and the leap, you have serious trouble!
On that basis, and given that a function will raise a TypeError
if provided with the wrong number of arguments, you could use:
try:
# Have a go with the new interface
self._callback(data, additional_arg)
except TypeError:
# Fall back to the old one
self._callback(data)
You could use a decorator function to wrap any callback:
def api_compatible(func):
@functools.wraps(func)
def wrapper(data, *args, **kwargs):
try:
return func(data, *args, **kwargs)
except TypeError:
return func(data)
return wrapper
Now it becomes:
self._callback = api_compatible(callback)
...
self._callback(data, additional_arg)
2
An explicit check of the callback’s ability to handle parameters is about the best you’re going to be able to do. Python may be loosie-goosie in its duck typing, but it will complain and raise a TypeError
exception if you feed a function the wrong number of parameters. No ifs, ands, or buts about that.
You have existing functions in the field that you don’t feel you can change–and probably for good reason. So having an inspection that asks, “what can this callback function accept?” or “what does this callback function expect?”–it may be inelegant, but that’s what’s available to you.
The other choices involve:
-
Having some form of object-relative state (such as an additional instance value or method) or even global state (whether a true global variable, a class variable, a singleton reporting object, or whathaveyou) which callbacks can reference to get the extended information you’re now offering them. This is how C, Unix, and many other codebases handle exceptional and additional information. It is, unfortunately, not especially elegant–and it can be quite problematic/unworkable for multithreaded apps.
-
On the off chance that your
data
parameter is an extensible type, you might be able to add fields to it. If it’s adict
or similar structure you’re golden, as long as the callbacks play by duck typing rules and don’t strictly check that they got only the fields back they expected. This is a trick that often works in dynamic languages, even though it would fall flat on its face in statically typed languages/data passing environments. But, you have to get lucky to have this work. Ifdata
is a more static type, you’re back to choice 1 or your inspection-based approach (“choice 0”).
Update
As an aside, your code will run fine in Python 2. But Python 3 changes the place the code is stored. To encompass both your current Python 2 and future moves to Python 3, here’s a shim that works in both:
import sys
_PY2 = sys.version_info[0] == 2
def arg_count(f):
code = f.func_code if _PY2 else f.__code__
return code.co_argcount
Then:
if arg_count(self._callback) > 1:
....