views:

368

answers:

6

I read somewhere that functions should always return only one type so the following code is considered as bad code:

def x(foo):
 if 'bar' in foo:
  return (foo, 'bar')
 return None

I guess the better solution would be

def x(foo):
 if 'bar' in foo:
  return (foo, 'bar')
 return ()

Wouldn't it be cheaper memory wise to return a None then to create a new empty tuple or is this time difference too small to notice even in larger projects?

+1  A: 

If x is called like this

foo, bar = x(foo)

returning None would result in a

TypeError: 'NoneType' object is not iterable

if 'bar' is not in foo.

Example

def x(foo):
    if 'bar' in foo:
        return (foo, 'bar')
    return None

foo, bar = x(["foo", "bar", "baz"])
print foo, bar

foo, bar = x(["foo", "NOT THERE", "baz"])
print foo, bar

This results in:

['foo', 'bar', 'baz'] bar
Traceback (most recent call last):
  File "f.py", line 9, in <module>
    foo, bar = x(["foo", "NOT THERE", "baz"])
TypeError: 'NoneType' object is not iterable
+8  A: 

Why should functions return values of a consistent type? To meet the following two rules.

Rule 1 -- a function has a "type" -- inputs mapped to outputs. It must return a consistent type of result, or it isn't a function. It's a mess.

Mathematically, we say some function, F, is a mapping from domain, D, to range, R. F: D -> R. The domain and range form the "type" of the function. The input types and the result type are as essential to the definition of the function as is the name or the body.

Rule 2 -- when you have a "problem" or can't return a proper result, raise an exception.

def x(foo):
    if 'bar' in foo:
        return (foo, 'bar')
     raise Exception( "oh, dear me." )

You can break the above rules, but the cost of long-term maintainability and comprehensibility is astronomical.

"Wouldn't it be cheaper memory wise to return a None?" Wrong question.

The point is not to optimize memory at the cost of clear, readable, obvious code.

S.Lott
+1 for the last sentence. Maintainability is the real cost that should be considered with 99% of code.
Andrzej Doyle
You haven't really given any reasons for this conclusion though. Your reasons why a function should return the same type include 1) "any function that does otherwise is a mess", and 2) "it must return the same type to fit the mathematical definition of a function." The first is pure rhetoric, and the second begs the question. It's not that I disagree. I readily defer to your python prowess. But could you give some practical reasons on why people should observe this practice? For example, breaking with this practice surely wouldn't <i>always</i> make code unreadable.
twneale
-1: When the word 'function' is used in the context of this question it is reasonable to assume it means a Python function, not a mathematical function. It's quite normal for Python functions to not always return the same type (e.g. `copy.copy()`). Your Rule 1 says that some Python functions aren't really functions, which is a semantic mess.
Scott Griffiths
The `copy.copy()` is still a function. The signature (domain and range) are fixed. For a given domain value, the range is predictable. Yes, there are a lot of combinations, but the combinations are trivially predictable. It's a function with a simple, predictable signature in a mathematical sense. It does not spontaneously return an unpredictable type.
S.Lott
@Scott Griffiths: the point is to *reduce* complexity, by reasoning about things we can know and understand. By limiting ourselves to functions that mirror mathematical formalisms, we have a fighting chance of creating software that really works. That's the "why".
S.Lott
There are also lots of examples in the standard library that either return one type or return `None`. They don't 'spontaneously return an unpredictable type' any more then the OP's function. For example `os.getenv', `re.search`, `sys.callstats`. Raising an exception instead is a fine thing to do, but it's a design choice, and returning `None` is often quite reasonable.
Scott Griffiths
Interestingly, many of the counter examples -- functions that actually return an unpredictable type -- are inherited from OS library routines that are forced to cater to the C API, which lacks exceptions. Making them bad examples of Pythonic things to do. The only relevant example, so far, is `re.search` which spontaneously returns an expected value -- a object which is not of class `Match`. And that's often frustrating because of all the extra if-statements required.
S.Lott
And what happens if i want to say `[x(foo) for foo in seq]`?
David X
@David X: The expression -- as a whole -- is ill-formed because one part is ill-formed. That's the most logical because it's the most consistent and easiest to reason with. If you're talking about using the list comprehension as some kind of "filter", then you'd have to actually write the actual filtering expression. Consistency is easier to work with and a list of mixed values and `None`s is potentially confusing.
S.Lott
+5  A: 

It's not so clear that a function must always return objects of a limited type, or that returning None is wrong. For instance, re.search can return a _sre.SRE_Match object or a NoneType object:

import re
match=re.search('a','a')

type(match)
# <type '_sre.SRE_Match'>

match=re.search('a','b')

type(match)
# <type 'NoneType'>

Designed this way, you can test for a match with the idiom

if match:
    # do xyz

If the developers had required re.search to return a _sre.SRE_Match object, then the idiom would have to change to

if match.group(1) is None:
    # do xyz

There would not be any major gain by requiring re.search to always return a _sre.SRE_Match object.

So I think how you design the function must depend on the situation and in particular, how you plan to use the function.

Also note that both _sre.SRE_Match and NoneType are instances of object, so in a broad sense they are of the same type. So the rule that "functions should always return only one type" is rather meaningless.

Having said that, there is a beautiful simplicity to functions that return objects which all share the same properties. (Duck typing, not static typing, is the python way!) It can allow you to chain together functions: foo(bar(baz))) and know with certainty the type of object you'll receive at the other end.

This can help you check the correctness of your code. By requiring that a function returns only objects of a certain limited type, there are fewer cases to check. "foo always returns an integer, so as long as an integer is expected everywhere I use foo, I'm golden..."

unutbu
+5  A: 

Best practice in what a function should return varies greatly from language to language, and even between different Python projects.

For Python in general, I agree with the premise that returning None is bad if your function generally returns an iterable, because iterating without testing becomes impossible. Just return an empty iterable in this case, it will still test False if you use Python's standard truth testing:

ret_val = x()
if ret_val:
     do_stuff(ret_val)

and still allow you to iterate over it without testing:

for child in x():
    do_other_stuff(child)

For functions that are likely to return a single value, I think returning None is perfectly acceptable, just document that this might happen in your docstring.

Jeffrey Harris
+3  A: 

I personally think it is perfectly fine for a function to return a tuple or None. However, a function should return at most 2 different types and the second one should be a None. A function should never return a string and list for example.

Echo
Also, a function that returns either a 3-uple or a 0-uple as in the example, looks bad taste to me. "return None" is also my preferred choice in this example case.
A: 

Premature optimization is the root of all evil. The minuscule efficiency gains might be important, but not until you've proven that you need them.

Whatever your language: a function is defined once, but tends to be used at any number of places. Having a consistent return type (not to mention documented pre- and postconditions) means you have to spend more effort defining the function, but you simplify the usage of the function enormously. Guess whether the one-time costs tend to outweigh the repeated savings...?

Pontus Gagge