Generalized Set Functions

Let’s look at a more useful example of special argument-matching modes at work. At the end of Chapter 16, we wrote a function that returned the intersection of two sequences (it picked out items that appeared in both). Here is a version that intersects an arbitrary number of sequences (one or more) by using the varargs matching form *args to collect all the passed-in arguments. Because the arguments come in as a tuple, we can process them in a simple for loop. Just for fun, we’ll code a union function that also accepts an arbitrary number of arguments to collect items that appear in any of the operands:

def intersect(*args):
    res = []
    for x in args[0]:                  # Scan first sequence
        if x in res: continue          # Skip duplicates
        for other in args[1:]:         # For all other args
            if x not in other: break   # Item in each one?
        else:                          # No: break out of loop
            res.append(x)              # Yes: add items to end
    return res

def union(*args):
    res = []
    for seq in args:                   # For all args
        for x in seq:                  # For all nodes
            if not x in res:
                res.append(x)          # Add new items to result
    return res

Because these are tools potentially worth reusing (and they’re too big to retype interactively), we’ll store the functions in a module file called inter2.py (if you’ve forgotten how modules and imports work, see the introduction in Chapter 3, or stay tuned for in-depth coverage in Part V). In both functions, the arguments passed in at the call come in as the args tuple. As in the original intersect, both work on any kind of sequence. Here, they are processing strings, mixed types, and more than two sequences:

% python
>>> from inter2 import intersect, union
>>> s1, s2, s3 = "SPAM", "SCAM", "SLAM"

>>> intersect(s1, s2), union(s1, s2)           # Two operands
(['S', 'A', 'M'], ['S', 'P', 'A', 'M', 'C'])

>>> intersect([1, 2, 3], (1, 4))               # Mixed types
[1]

>>> intersect(s1, s2, s3)                      # Three operands
['S', 'A', 'M']

>>> union(s1, s2, s3)
['S', 'P', 'A', 'M', 'C', 'L']

To test more thoroughly, the following codes a function to apply the two tools to arguments in different orders using a simple shuffling technique that we saw in Chapter 13—it slices to move the first to the end on each loop, uses a * to unpack arguments, and sorts so results are comparable:

>>> def tester(func, items, trace=True):
       for i in range(len(items)):
           items = items[1:] + items[:1]
           if trace: print(items)
           print(sorted(func(*items)))

>>> tester(intersect, ('a', 'abcdefg', 'abdst', 'albmcnd'))
('abcdefg', 'abdst', 'albmcnd', 'a')
['a']
('abdst', 'albmcnd', 'a', 'abcdefg')
['a']
('albmcnd', 'a', 'abcdefg', 'abdst')
['a']
('a', 'abcdefg', 'abdst', 'albmcnd')
['a']

>>> tester(union, ('a', 'abcdefg', 'abdst', 'albmcnd'), False)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'l', 'm', 'n', 's', 't']
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'l', 'm', 'n', 's', 't']
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'l', 'm', 'n', 's', 't']
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'l', 'm', 'n', 's', 't']

>>> tester(intersect, ('ba', 'abcdefg', 'abdst', 'albmcnd'), False)
['a', 'b']
['a', 'b']
['a', 'b']
['a', 'b']

The argument scrambling here doesn’t generate all possible argument orders (that would require a full permutation, and 24 orderings for 4 arguments), but suffices to check if argument order impacts results here. If you test these further, you’ll notice that duplicates won’t appear in either intersection or union results, which qualify them as set operations from a mathematical perspective:

>>> intersect([1, 2, 1, 3], (1, 1, 4))
[1]
>>> union([1, 2, 1, 3], (1, 1, 4))
[1, 2, 3, 4]
>>> tester(intersect, ('ababa', 'abcdefga', 'aaaab'), False)
['a', 'b']
['a', 'b']
['a', 'b']

These are still far from optimal from an algorithmic perspective, but due to the following note, we’ll leave further improvements to this code as suggested exercise. Also notice that the argument scrambling in our tester function might be a generally useful tool, and the tester would be simpler if we delegated this to another function, one that would be free to create or generate argument combinations as it saw fit:

>>> def tester(func, items, trace=True):
        for args in scramble(items):
            ...use args...

In fact we will—watch for this example to be revised in Chapter 20 to address this last point, after we’ve learned how to code user-defined generators. We’ll also recode the set operations one last time in Chapter 32 and a solution to a Part VI exercise as classes that extend the list object with methods.

Note

Because Python now has a set object type (described in Chapter 5), none of the set-processing examples in this book are strictly required anymore; they are included just as demonstrations of coding techniques, and are today instructional only. Because it’s constantly improving and growing, Python has an uncanny way of conspiring to make my book examples obsolete over time!