5

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?

jamylak
  • 128,818
  • 30
  • 231
  • 230
jerrymouse
  • 16,964
  • 16
  • 76
  • 97
  • 1
    It is difficult to answer this question on your toy example. Would you mind sharing the concrete use case? – Ferdinand Beyer Mar 27 '12 at 11:49
  • 1
    Your suggestion is the only sane thing to do this. Creatign more functions would clutter your API. Adding "magic" to return sometimes 1 sometimes 2 results would be a usability and maintainability nightmare. – jsbueno Mar 27 '12 at 11:54
  • Most of the times, it might be better to refactor the places where the old function is used. Otherwise, when a situation like this arises again, you might end up adding additional switches or sibling functions. – Ferdinand Beyer Mar 27 '12 at 11:57
  • @FerdinandBeyer I have a large method that uses urllib2.Request to open a url and return some generic results. Now I also need to return the request.info().headers to client, ON DEMAND, without breaking any legacy code. I dont want to define separate method to redo a HEAD request on same url. Thats my use case. Please suggest – jerrymouse Mar 27 '12 at 12:01

7 Answers7

10

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.

jamylak
  • 128,818
  • 30
  • 231
  • 230
Not_a_Golfer
  • 47,012
  • 14
  • 126
  • 92
3

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.

Alfe
  • 56,346
  • 20
  • 107
  • 159
  • +1 for putting in efforts to compare all options! Although it will work, but it will also render code unmanageable resulting in more man hours for debugging. – jerrymouse Apr 04 '12 at 17:58
  • I'd agree with the second part of what you said if you provided a solution without this disadvantage. All proposed alternatives have drawbacks which eventually make code harder to maintain. My version at least uses a pattern well-known (although known from another domain like C programming). – Alfe Apr 05 '12 at 12:56
2

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.

pcalcao
  • 15,789
  • 1
  • 44
  • 64
2

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 ;)

przemo_li
  • 3,932
  • 4
  • 35
  • 60
2

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.
jamylak
  • 128,818
  • 30
  • 231
  • 230
jerrymouse
  • 16,964
  • 16
  • 76
  • 97
  • Disadvantage: You have two completely different ways of calling the function f() with two different result types. In fact, you have just used some sugar to implement the same as f_v2(a,b,c). By this you bloat your API, which, if done multiple times, renders your code not so managable as you think ;-) – Alfe Apr 05 '12 at 13:02
1

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.

Silas Ray
  • 25,682
  • 5
  • 48
  • 63
0

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
jamylak
  • 128,818
  • 30
  • 231
  • 230
Marek
  • 439
  • 2
  • 5
  • 12
  • Eek. I *really* don't like this solution. – Chris Morgan Mar 27 '12 at 12:07
  • 1
    I agree that it looks similar to C code, but it does the job for given task - returns additional result without breaking existing code. – Marek Mar 27 '12 at 12:09
  • Yah... shouldnt be editing references like that... it is better pratice to return values and not have any side effects on the parameters... – jamylak Mar 27 '12 at 12:11
  • 1
    Jamylak: depends on what you expect. Using out-parameters is common practice in many domains. Not so much in Python, though, agreed. – Alfe Apr 05 '12 at 13:12
  • 1
    Beware the [Mutable Default Argument](http://docs.python-guide.org/en/latest/writing/gotchas/#mutable-default-arguments). – derek Jun 08 '18 at 12:33