FMc
FMc

Reputation: 42421

How do I get an iterator over the keys/indexes of an arbitrary Python object?

I'm working on a project where I need a method that takes an arbitrary Python object, and if that object acts like a dict, list, or tuple -- meaning that it supports the idea of accessing members of a collection via a key or index -- my method should return an iterator that can traverse the object's key-value or index-value pairs. It would also be fine for my purposes if the iterator simply traversed the objects keys or indexes. Here's the code I've got so far:

from collections import Mapping, Sequence

# Tuple used to identify string-like objects in Python 2 or 3.
STRINGS = (str, unicode) if str is bytes else (str, bytes)

def get_keyval_iter(obj):
    if   isinstance(obj, STRINGS):  return None
    elif isinstance(obj, Sequence): return enumerate(obj)
    elif isinstance(obj, Mapping):  return getattr(obj, 'iteritems', obj.items)()
    else:                           return None

# For example:
print list(get_keyval_iter([0, 11, 22]))        # [(0, 0), (1, 11), (2, 22)]
print list(get_keyval_iter(dict(a = 1, b = 2))) # [('a', 1), ('b', 2)]
print [ get_keyval_iter("foobar") ]             # [None]
print [ get_keyval_iter(1234) ]                 # [None]

I don't like this solution for two reasons: (1) on general principle, I'd rather query the object's interface than check its type; (2) my code will return None for user-defined classes whose objects fail the isinstance tests but that nonetheless support the __getitem__ protocol and could, in theory, give me an iterator over the relevant keys or indexes.

Here's the code I'd like to write: return obj.__getitemiter__() -- or something like that.

Am I overlooking an obvious way to get what I need -- namely, an iterator over an arbitrary object's keys or indexes (or over its key-value or index-value pairs)?

Upvotes: 0

Views: 622

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1123360

You want to use the ABCs defined in the collections module only to detect a mapping (because you want to iterate over key-value pairs instead of keys), and use the standard iter() function for everything else:

import collections

def get_keyval_iter(obj):
    if isinstance(obj, collections.Mapping):
        return obj.iteritems()
    try:
        return enumerate(iter(obj))
    except TypeError:
        # not iterable
        return None

Note the iter() call; it takes any iterable sequence object and returns an iterator object that will operate on it. It supports both objects that implement the iterator protocol and objects that support a .__getitem__() method:

[...] o must be a collection object which supports the iteration protocol (the __iter__() method), or it must support the sequence protocol (the __getitem__() method with integer arguments starting at 0).

So where collections.Sequence looks for both a __getitem__ and a __len__ method, iter() only looks for __getitem__.

Note that you should not go overboard with accepting and handling too many different types; there should not be an exception for strings here, for example. Rethink your code to perhaps be more strict in what you promise to handle.

Upvotes: 1

Related Questions