6
\$\begingroup\$

I have the following function:

def item_by_index(iterable, index, default=Ellipsis):
    """Return the item of <iterable> with the given index.

    Can be used even if <iterable> is not indexable.

    If <default> is given, it is returned if the iterator is exhausted before
    the given index is reached."""

    iterable_slice = itertools.islice(iterable, index, None)
    if default is Ellipsis:
        return next(iterable_slice)
    return next(iterable_slice, default)

It should (and does) throw an exception if the default is not given and the iterator is exhausted too soon. This is very similar to what the built-in function next does.

Usually, one would probably use None as default value to make an argument optional. In this case, however, None is a valid value for default and should be treated differently from no argument. This is why I used Ellipsis as a default value. It sort of feels like an abuse of the Ellipsis object, though.

Is there a better way of doing this or is Ellipsis a good tool for the job?

\$\endgroup\$
6
\$\begingroup\$

I would argue the best option is to make an explicit sentinel value for this task. The best option in a case like this is a simple object() - it will only compare equal to itself (aka, x == y only when x is y).

NO_DEFAULT = object()

def item_by_index(iterable, index, default=NO_DEFAULT):
    ...

This has some semantic meaning, and means there is no potential overlap between a value a user wants to give and the default (I imagine it's highly unlikely they'd want to use Ellipses, but it's better to make as few assumptions as possible about use cases when writing functions). I'd recommend documenting it as such as well, as it allows the user to give a default/not using a variable, rather than having to change the call.

try:
    default = get_some_value()
except SomeException:
    default = yourmodule.NO_DEFAULT

item_by_index(some_iterable, index, default)

As opposed to having to do:

try:
    default = get_some_value()
except SomeException:
    item_by_index(some_iterable, index)
else:
    item_by_index(some_iterable, index, default)

The only downside it's not particularly clear what it is if you are looking at the object itself. You could create a custom class with a suitable __repr__()/__str__() and use an instance of it here.

As of Python 3 enums have been added - the default type of enum only compares equal on identity, so a simple one with only the one value would also be appropriate (and is essentially just an easy way to do the above):

from enum import Enum
class Default(Enum):
    no_default = 0

Thanks to metaclass magic, Default.no_default is now an instance of a Default, which will only compare equal with itself (not 0, by default, enums are pure).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.