setutils - IndexedSet type

The set type brings the practical expressiveness of set theory to Python. It has a very rich API overall, but lacks a couple of fundamental features. For one, sets are not ordered. On top of this, sets are not indexable, i.e, my_set[8] will raise an TypeError. The IndexedSet type remedies both of these issues without compromising on the excellent complexity characteristics of Python’s built-in set implementation.

class boltons.setutils.IndexedSet(other=None)[source]

IndexedSet is a collections.MutableSet that maintains insertion order and uniqueness of inserted elements. It’s a hybrid type, mostly like an OrderedSet, but also list-like, in that it supports indexing and slicing.

Parameters:other (iterable) – An optional iterable used to initialize the set.
>>> x = IndexedSet(list(range(4)) + list(range(8)))
>>> x
IndexedSet([0, 1, 2, 3, 4, 5, 6, 7])
>>> x - set(range(2))
IndexedSet([2, 3, 4, 5, 6, 7])
>>> x[-1]
7
>>> fcr = IndexedSet('freecreditreport.com')
>>> ''.join(fcr[:fcr.index('.')])
'frecditpo'

Standard set operators and interoperation with set are all supported:

>>> fcr & set('cash4gold.com')
IndexedSet(['c', 'd', 'o', '.', 'm'])

As you can see, the IndexedSet is almost like a UniqueList, retaining only one copy of a given value, in the order it was first added. For the curious, the reason why IndexedSet does not support setting items based on index (i.e, __setitem__()), consider the following dilemma:

my_indexed_set = [A, B, C, D]
my_indexed_set[2] = A

At this point, a set requires only one A, but a list would overwrite C. Overwriting C would change the length of the list, meaning that my_indexed_set[2] would not be A, as expected with a list, but rather D. So, no __setitem__().

Otherwise, the API strives to be as complete a union of the list and set APIs as possible.

add(item) → add item to the set[source]
clear() → empty the set[source]
count(val) -> count number of instances of value (0 or 1)[source]
difference(*others) → get a new set with elements not in others[source]
difference_update(*others) -> discard self.intersection(*others)[source]
discard(item) -> discard item from the set (does not raise)[source]
classmethod from_iterable(it) → create a set from an iterable[source]
index(val) → get the index of a value, raises if not present[source]
intersection(*others) → get a set with overlap of this and others[source]
intersection_update(*others) -> discard self.difference(*others)[source]
isdisjoint(other) → return True if no overlap with other[source]
issubset(other) → return True if other contains this set[source]
issuperset(other) → return True if set contains other[source]
iter_difference(*others) → iterate over elements not in others[source]
iter_intersection(*others) → iterate over elements also in others[source]
iter_slice(start, stop, step=None)[source]

iterate over a slice of the set

pop(index) -> remove the item at a given index (-1 by default)[source]
remove(item) → remove item from the set, raises if not present[source]
reverse() → reverse the contents of the set in-place[source]
sort() → sort the contents of the set in-place[source]
symmetric_difference(*others) → XOR set of this and others[source]
symmetric_difference_update(other) → in-place XOR with other[source]
union(*others) → return a new set containing this set and others[source]
update(*others) → add values from one or more iterables[source]
boltons.setutils.complement(wrapped)[source]

Given a set, convert it to a complement set.

Whereas a set keeps track of what it contains, a complement set keeps track of what it does not contain. For example, look what happens when we intersect a normal set with a complement set:

>>> list(set(range(5)) & complement(set([2, 3])))

[0, 1, 4]

We get the everything in the left that wasn’t in the right, because intersecting with a complement is the same as subtracting a normal set.

Parameters:wrapped (set) – A set or any other iterable which should be turned into a complement set.

All set methods and operators are supported by complement sets, between other complement()-wrapped sets and/or regular set objects.

Because a complement set only tracks what elements are not in the set, functionality based on set contents is unavailable: len(), iter() (and for loops), and .pop(). But a complement set can always be turned back into a regular set by complementing it again:

>>> s = set(range(5))
>>> complement(complement(s)) == s
True

Note

An empty complement set corresponds to the concept of a universal set from mathematics.

Many uses of sets can be expressed more simply by using a complement. Rather than trying to work out in your head the proper way to invert an expression, you can just throw a complement on the set. Consider this example of a name filter:

>>> class NamesFilter(object):
...    def __init__(self, allowed):
...        self._allowed = allowed
...
...    def filter(self, names):
...        return [name for name in names if name in self._allowed]
>>> NamesFilter(set(['alice', 'bob'])).filter(['alice', 'bob', 'carol'])
['alice', 'bob']

What if we want to just express “let all the names through”?

We could try to enumerate all of the expected names:

``NamesFilter({'alice', 'bob', 'carol'})``

But this is very brittle – what if at some point over this object is changed to filter ['alice', 'bob', 'carol', 'dan']?

Even worse, what about the poor programmer who next works on this piece of code? They cannot tell whether the purpose of the large allowed set was “allow everything”, or if ‘dan’ was excluded for some subtle reason.

A complement set lets the programmer intention be expressed succinctly and directly:

NamesFilter(complement(set()))

Not only is this code short and robust, it is easy to understand the intention.