return multiple values in python without breaking previous code
문제
I had a version 1 of a function like:
def f(a,b,c):
#Some processing
return some_result
Later, I upgraded it to version 2.
def f(a,b,c):
#Some processing
#Some more processing
return some_result, additional_result
The latter version returns a tuple. Hence all client code using version 1 gets obsolete. Can I get additional_result
on demand?
That is you get additional_result
only when you ask for it while you continue to get some_result
as if nothing changed.
One trick I thought of:
def f(a,b,c, need_additional = False):
#Some processing
#Some more processing
if not need_addional:
return some_result
else:
return some_result, additional_result
Anything better? Or more generic?
해결책 7
I am finally implementing it like:
def f(a,b,c, v2 = False):
#Some processing
#Some more processing
if not v2:
return some_result
else:
v2_dict = {}
v2_dict['additional_result'] = additional_result
v2_dict['...'] = ...
return some_result, v2_dict
Advantages:
- Legacy code doesn't break.
- No limitation on return values in future edition.
- Code is manageable.
다른 팁
I think a more elegant solution would be to make your old function a legacy wrapper for your new function:
def f(a,b,c):
some_result, additional_result = f_new(a,b,c)
return some_result
def f_new(a,b,c):
#Some processing
#Some more processing
return some_result, additional_result
I have to admit I mostly use the pattern you've suggested and not mine :), but default arguments for backwards compatibility are not that great of a practice.
Add additional_result
as field of some_result
If needed change some_result
as class that inherit whatever its data class should be, but with additional_result
.
(So legacy code can simple ignore your "extra bit")
Or even better.
Just make separate function that return multiple values. KISS ;)
None of the given solutions are perfect.
- Flexible returns based on flags (asker's proposal) is hard to maintain and breaks any clear API structure.
- Legacy wrappers (Dvir Volk's answer) will add more and more functions to your API and thus will let it grow. Also it does not put a lid on things; for any new additional return value you will do this again.
- Result enhancement (Przemo Li's answer) is only possible if you can control the creation of the result. If you want to return a value some library creates for you, all you could do is put a wrapper around it (which would bring up problems as soon as the class of the result changes due to library updates or similar).
- Making a hard cut and refactoring all using code to a new API is not always an option, e. g. if your customers maintain that using code.
- Additional functions (also in Przemo Li's answer and declared KISS) will clutter your API as well.
Actually, though it does seem ugly, I guess I would prefer the already mentioned way of passing additional results via out-parameters. My way to code it would be this:
Old caller:
result = f(a, b, c)
print result
New caller:
additionalResult = [0]
result = f(a, b, c, additionalResult)
print result, additionalResult[0]
Callee:
def f(a, b, c, additionalResult=[None]):
#Some processing
#Some more processing
additionalResult[0] = someValue
return some_result
This also is good and often used practice in C, so there is good reason to believe that this won't lay traps we might have overlooked, although it might look ugly.
Although I'm not a Python expert, I would implement that in the way you mentioned, it seems to me like a pretty straight-forward change in order to allow backward compatibility.
If you don't have to use both versions in the same file, you could control it by having 2 versions of the module that contain the method, then only importing the version you want in your calling modules. Then, when you do eventually go through and refactor your old code, all you have to do is alter the import statement when you are done.
Or, use the from x import y as z
construct with both implementations in the same module.
You may also return extra values as references in dictionary whose reference was passed as extra argument, e.g.:
def f(a,b,c, additional_result_dict = {}):
#Some processing
#Some more processing
additional_result_dict['additional_result'] = additional_result
return some_result