[Python-ideas] Adding an optional function argument to all() and any() builtins

Steven D'Aprano steve at pearwood.info
Mon Nov 22 00:12:30 CET 2010


Andy Buckley wrote:
> I may be missing a standard idiom, but it seems to me that the any(...)
> and all(...) builtins are unnecessarily limited by requiring that the
> iterable they take as an argument is already in a form suitable for the
> intended kind of boolean comparison.

Yes, just like every function requires its input to already be in a form 
suitable for whatever the function expects to do. Just as any() and 
all() expect their argument to be truth-values (not necessarily True or 
False, but values to be inspected for their truthness), so sum() expects 
input consisting of numbers. Would you expect to be able to write this?

mylist = ["a12", "b13", "c14", "d15"]
sum(mylist, convert=lambda s: int(s[1:]))

I hope not. Converting its input into a suitable format is not the 
responsibility of sum. sum's responsibility begins and ends with adding 
the input, any conversions should be done either externally:

mylist = ["a12", "b13", "c14", "d15"]
newlist = [int(s[1:]) for s in mylist]
sum(newlist)

or lazily using a generator expression as the argument to sum:

mylist = ["a12", "b13", "c14", "d15"]
sum(int(s[1:]) for s in mylist)

There are variations on this using built-in map, itertools.imap, and 
similar.

The problems with shifting the responsibility to any and all (or sum) 
instead of keeping it with the caller include:

* It adds complexity to the API for all and any, and that makes the API 
harder to learn and harder to remember.

* It duplicates functionality: now two more functions are expected to 
know how to accept a second argument and call it against the input 
before testing.

* It can lead to "Jack of All Trades, Master of None" functions:

oven.roast(
     chicken, add_stuffing="sage and onion",
     serve=["pre-dinner drinks", "appetizer", "main course", "desert"],
     wash_pots_afterward=False
     )

Why is the oven being asked to prepare the stuffing and serve the food? 
Now this is an extreme case, but any time you consider adding extra 
responsibility to a function, you're taking a step in that direction.

There should be a clear benefit to the caller before accepting these 
extra costs. For example, list.sort(), sorted(), min() and max() now all 
take a key argument which is superficially similar to your suggested 
"test" argument. You would need to demonstrate similar benefit.


> Most of the time, when I want to check that any or all of a collection
> matches some test criterion, my iterable is not already in a valid form
> to pass to any() or all(), and so I either have to explicitly re-code a
> slightly modified version of the builtin, or wastefully use map() to
> apply my test to *all* the items in the list. 

Re-coding all and any is usually a big No. That's almost always the 
wrong solution.

In Python 3, map becomes a lazy iterator, which means it no longer 
applies the test to every item up front. In Python 2, the simple 
alternative is itertools.imap, which saves you from re-coding map:

def lazymap(func, iterable):
     # Don't use this, use itertools.imap
     for obj in iterable:
         yield func(obj)


But probably the best solution is to use a generator expression.

# Like a list comp, except with round brackets.
genexp = (mytestfunction(x) for x in mylist)
any(genexp)


This becomes even easier when you embed the generator expression 
directly in the function call:

any(mytestfunction(x) for x in mylist)

If mytestfunction is simple enough, you can do it in-place without 
needing to write a function:

any(x >= 42 for x in mylist)



[...]
> The "test" keyword arg is perhaps not the best name, I admit, but no
> other jumps prominently to mind ("match", "fn"?).

When you have difficulty thinking of a name which clearly and concisely 
describes what the argument does, that's often a good hint that you are 
dumping too much responsibility onto the function.

Having said that, I'd suggest that an appropriate name might be the same 
name used by sort and friends: key.

-0 on the idea -- I don't find it compelling or necessary, but nor do I 
hate it.



-- 
Steven



More information about the Python-ideas mailing list