Here's a typical loop that checks for some condition and breaks the first time it passes:
def list_contains_urls(maybe_urls):
for url in maybe_urls:
if url.startswith('http'):
return True
return False
Can we do that more concisely? Sure.
def list_contains_urls(maybe_urls):
return any(u for u in maybe_urls if u.startswith('http'))
This, I'd argue, is still pretty clear. any is a builtin, and everybody
should learn their builtins.
This version would still work, but would be less efficient:
def list_contains_urls(maybe_urls):
return any([u for u in maybe_urls if u.startswith('http')])
What's the difference? In the second case, we used a list comprehension; in the first, we used a generator expression (introduced in Python 2.4). They look the same, except for the brackets around the list comprehension.
Why is the generator expression better?
Because the list comprehension wastes memory building a list we don't actually
need, and wastes work computing every member of this list even though we only
care about the first one that matches the conditional. The generator
expression version does neither; it short-circuits just like the original for
loop version.
Confused? Consider this generator expression:
>>> ints = (x for x in range(-10, 10)) # `xrange` in python 2
>>> ints
<generator object <genexpr> at 0x214ceb0>
>>> any(x for x in ints if x > 0)
True
>>> list(ints)
[2, 3, 4, 5, 6, 7, 8, 9]
2
What happened there? The generator stopped iterating as soon as any() stopped consuming
it, when any() found the first matching value. The value that tested true must have
been 1. That's why, when we call list(ints), we get the remaining
elements that hadn't been generated yet when it stopped, starting with 2. The generator
"remembered" where it left off.
Incidentally, that means if you call list(ints) again, you get an empty list,
because there was nothing left to generate:
>>> list(ints)
[]