When you're writing code for educational purposes (or sometimes other purposes), verbose is good because it helps you understand what's really going on. So making the code shorter or snappier or whatever is not necessarily going to make it better.
With that disclaimer out of the way: one of the most common ways to condense Python code is to use list comprehensions or generators instead of loops. A list comprehension is what you use when you're constructing a list element by element: in its simplest form, instead of this,
the_list = []
for something in something_else:
the_list.append(func(something))
you write this:
the_list = [func(something) for something in something_else]
If you're doing something else instead of creating a list, you can have Python create an object that generates the elements on demand, rather than actually creating a list out of them. An object of that sort is called a generator and you can create one like this:
the_generator = (func(something) for something in something_else)
You can omit the parentheses when the generator is passed to another function as an argument, though.
the_sum = sum(func(something) for something in something_else)
would be equivalent to, but better than,
count = 0
for something in something_else:
count += func(something)
There are a lot of functions in Python that take iterables (list, generators, etc.) and "condense" them into one value using some sort of operation. You can also create your own, corresponding to whatever you would be doing to the result of the loop. You can convert most loops into generator expressions this way.
So let's investigate how you could use a generator to represent the sequences of consecutive throws in each trial. You can create a generator that produces 10 random numbers easily:
(random.random() for i in xrange(10))
(this is for Python 2.x; xrange
was renamed to range
for Python 3). Or you can create a generator that produces 10 random values which are either 0 or 1:
(random.randint(0,1) for i in xrange(10))
That saves you from having to check each random number against 0.5. In fact, you could produce a generator that produces 10 randomly chosen words, "Heads" or "Tails", like so:
(random.choice(("Heads","Tails")) for i in xrange(10))
but it'll be easier to stick with numbers. (It's usually better to represent things with numbers or objects than with strings.)
But perhaps you're thinking, "why are you telling me to make 10 numbers when I only have to check until I find a group of four consecutive heads?" For one thing, if you're just flipping 10 coins each time, it really doesn't matter because you'll make the computer flip at most 6, and on average 3, extra coins in each trial. That doesn't take very long - it'll extend the runtime of this part of your program by 50%, but we're talking 50% of a fraction of a second. It's not worth the effort to figure out how to do it for such a small number of flips. But if each flip had, say, a billion trials, then you would definitely want to stop early. Fortunately, a generator can do this for you! Since generators produce their elements only on demand, you can stop taking elements from it once you get what you want, and not waste much of any computation. I'll address this more later.
Anyway, suppose we have our generator that produces 10 binary values 0 (tails) or 1 (heads). Is there a way to go through this and check to see whether there is a sequence of four or more heads? It turns out that just such a function is provided in itertools.groupby
, which takes any iterable (list, generator, etc.) and groups consecutive identical elements. An example of its usage is
for k, g in itertools.groupby([1,0,0,1,1,1,1,0,0,0]):
print k, list(g)
and this would print out something like
1 [1]
0 [0,0]
1 [1,1,1,1]
0 [0,0,0]
So you can check for four or more consecutive heads by just looking at the length of the group and whether the key is heads or tails.
for k, g in itertools.groupby(random.randint(0,1) for i in xrange(10)):
if k and len(g) >= 4:
# got a run of 4 or more consecutive heads!
# wait, what now?
(In Python, 1 is true and 0 is false in a boolean context, so if k
is equivalent to if k == 1
.) OK, what shall we do with our run of 4 or more consecutive heads? Well, you're trying to find the number of trials in which this occurs. So it probably makes sense to set a success
flag if this happens.
success = False
for k, g in itertools.groupby(random.randint(0,1) for i in xrange(10)):
if k and len(g) >= 4:
success = True
break # this stops asking the generator for new values
But wait! This is starting to look a lot like the kind of loop that can be converted to a generator expression, isn't it? The only catch is that we're not adding anything up or constructing a list. But there is another function, any
, that will go through a generator until it finds an element which matches a condition, and that's just what this for
loop does. So you could write this as
success = any(k and len(g) >= 4 for k, g in
itertools.groupby(random.randint(0,1) for i in xrange(10))
Now finally, you'll want to count how many times this happens over, say, 10000 trials. So you might write that as something like this:
successes = 0
for i in xrange(10000):
if any(k and len(g) >= 4 for k, g in
itertools.groupby(random.randint(0,1) for i in xrange(10)):
successes += 1
But of course, we can also convert this to a generator, since you're just adding up numbers:
successes = sum(1 for i in xrange(10000)
if any(k and len(g) >= 4 for k, g in
itertools.groupby(random.randint(0,1) for i in xrange(10)))
The generator produces a 1
each time it finds a group of 4 consecutive 1
s among the 10 random numbers generated.
The last thing you'd want to do is divide by the total number of trials. Well, actually, what you really want to do is calculate the average instead of the sum, and in some places you can find a function mean
which is kind of like sum
except that it calculates the mean instead of the total. You could use such a function if you had it. But I don't know that one is in the Python standard library, so you can just do the division:
probability = sum(1 for i in xrange(10000)
if any(k and len(g) >= 4 for k, g in
itertools.groupby(random.randint(0,1) for i in xrange(10))) / 10000
So the task you're trying to accomplish can actually be written in one line of Python. But it's a rather complicated line, and I wouldn't necessarily recommend actually doing this. Sometimes it's good to use a good old fashioned for
loop to keep the code clear. More often, though, it's better to split your code up into modular pieces that are more useful than just what you're using them for.