On top of @BenC answer which provide an efficient writting for a straitforward algorithm, I'll provide a version where you build the resulting list already sorted, saving you some computation.
The main thing is that @BenC answer can easily be extended to provide more than 2 rolls of the dice, but by further examinating the structure of the desired output we can craft the required list specificaly for 2 rolls:
- Each number between
2
and 2 * num_sides
appears in the output sequence;
- The first number appear 1 time, the second number appear 2 times, …, the nth number appear n time until a number appears
num_sides
times;
- After that, the next number appears one time less than the previous one, with the number
2 * num_sides
appearing once.
Given that, we can iterate through range(1, num_sides)
to get the number of ascending items, and then through range(num_sides, 0, -1)
to get the number of descending items. The value of the items starts at 2 and end when we finish iterating.
First implementation
def sum_of_two_rolls(num_sides):
result = []
value = 2
for num_repeat in range(1, num_sides):
for _ in range(num_repeat):
result.append(value)
value += 1
for num_repeat in range(num_sides, 0, -1):
for _ in range(num_repeat):
result.append(value)
value += 1
return result
Note the use of _
when we do not care about the variable used to iterate and just want to loop a certain amount of times.
But this solution is not very elegant since it contains two loops that are very similar.
Second implementation
We now want to merge both for
loops since they perform the same operation. But we cannot use +
to concatenate two range
objects (assuming Python 3, since in Python 2 range returns lists and you can concatenate lists using +
). However, since range
objects are iterators, we can make good use of itertools.chain
which concatenates any number of iterators:
from itertools import chain
def sum_of_two_rolls(num_sides):
result = []
value = 2
for num_repeat in chain(range(1, num_sides), range(num_sides, 0, -1)):
for _ in range(num_repeat):
result.append(value)
value += 1
return result
But we can make this code even more pythonic.
Third implementation
The first thing to note is that using some sort of counter value being incremented inside a for loop is a common anti-pattern and is best served with enumerate
. In our case, we need to start at 2
instead of the default 0
usualy used with enumerate
. Luckily, enumerate
's optional second argument let us do that:
from itertools import chain
def sum_of_two_rolls(num_sides):
result = []
for value, repeats in enumerate(chain(range(1, num_sides), range(num_sides, 0, -1)), 2):
for _ in range(repeats):
result.append(value)
return result
Last improvement: the one-liner
Lastly, creating an empty list and appending to it in a loop is also an anti-pattern since a list-comprehension is most likely possible to use and has better performances.
Since this is the only computation that the function is doing, you can directly return the list comprehension:
from itertools import chain
def sum_of_two_rolls(num_sides):
return [v for v, r in enumerate(chain(range(1, num_sides), range(num_sides, 0, -1)), 2) for _ in range(r)]
Or splitted over several lines for better readability:
from itertools import chain
def sum_of_two_rolls(num_sides):
return [v
for v, r in enumerate(
chain(range(1, num_sides), range(num_sides, 0, -1)),
2)
for _ in range(r)]