Question

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?

Was it helpful?

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

OTHER TIPS

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.

  1. Flexible returns based on flags (asker's proposal) is hard to maintain and breaks any clear API structure.
  2. 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.
  3. 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).
  4. 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.
  5. 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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top