apeter
apeter

Reputation: 311

Making return type of python optional

In JVM languages there is a data type called Optional which says value can be Null/None. By using the data type of a function as Option(for example Option(Int)). Calling statement of function can take action.

How does one implement a similar approach in Python. I want to create a function and the return value of function should tell me 1. Function was successful so I have a value returned. 2. Function was not successful and so there is nothing to return.

Upvotes: 7

Views: 23604

Answers (3)

ljleb
ljleb

Reputation: 312

One approach would be to return either () or (<type>,) (int in your case). You could then come up with some helper functions like this one:

def get_or_else(option: tuple, default):
  if option:
    return option[0]
  else:
    return default

One benefit of this approach is that you don't need to depend on a third-party library to implement this. As a counterpart, you basically have to make your own little library.

Upvotes: 0

Chad Befus
Chad Befus

Reputation: 1968

I also wanted to tackle this problem and made a library called optional.py to do so. It can be installed using pip install optional.py. It is fully test covered and supports python2 and python3. Would love some feedback, suggestions, contributions.

To address the concerns of the other answer, and to head off any haters, yes, raising exceptions is more idiomatic of python, however it leads to ambiguity between exceptions for control-flow and exceptions for actual exceptional reasons.

There are large discussions about preventing defensive programming which mask the core logic of an application and smart people on both sides of the conversation. My personal preference is towards using optionals and so I provided the library to support that preference. Doing it with exceptions (or returning None) are acceptable alternatives, just not my preference.

Upvotes: 6

csl
csl

Reputation: 11358

To your original question: Raise an exception instead of returning None. This is an idiom in Python and therefore well understood by users. For example

def get_surname(name):
    if name == "Monty":
        return "Python"
    else:
        raise KeyError(name)

You'll see this usage pattern a lot in Python:

try:
    print(get_surname("foo"))
except KeyError:
    print("Oops, no 'foo' found")

Based on your feedback, it also seemed like you wanted to make sure that certain return values are actually used. This is quite tricky, and I don't think there is an elegant way to enforce this in Python, but I'll give it a try.

First, we'll put the return value in a property so we can track if it has indeed been read.

class Result(object):
    def __init__(self, value):
        self._value = value
        self.used = False

    @property
    def value(self):
        self.used = True # value was read
        return self._value

Secondly, we require that the Result object must be retrieved in a with-block. When the block is exited (__exit__), we check if the value has been read. To handle cases where a user doesn't use the with-statement, we also check that the value has been read when it is being garbage collected (__del__). An exception there will be converted into a warning. I usually shy away from __del__ though.

class RequireUsage(object):
    def __init__(self, value):
        self._result = Result(value)

    def __enter__(self):
        return self._result

    def __exit__(self, type, value, traceback):
        if type is None: # No exception raised
            if not self._result.used:
                raise RuntimeError("Result was unused")

    def __del__(self):
        if not self._result.used:
            raise RuntimeError("Result was unused (gc)")

To use this system, simply wrap the return value in RequireUsage like so:

def div2(n):
    return RequireUsage(n/2)

To use such functions, wrap them in a block and extract the value:

with div2(10) as result:
    print(result.value)

If you don't invoke result.value anywhere, you will now get an exception:

with div2(10) as result:
    pass # exception will be thrown

Likewise, if you just call div2 without with at all, you will also get an exception:

div2(10) # will also raise an exception

I wouldn't particularly recommend this exact approach in real programs, because I feel it makes usage more difficult while adding little. Also, there are still edge-cases that aren't covered, like if you simply do res = div2(10), because a held reference prevents __del__ from being invoked.

Upvotes: 1

Related Questions