Is there a Pythonic way of skipping if statements in a for loop to make my code run faster?
I'm writing a script in Python that essentially rolls a dice and checks whether the die roll exceeds a number x
. I want to repeat this process n
times and get the probability that the die roll exceeds the number x
. e.g.
Count = 0
for _ in itertools.repeat(None, Iterations):
x = 3
die_roll = rnd.randint(1,6)
if die_roll > x:
Count += 1
Probability_of_exceed = Count / Iterations
I want to modify both the die roll and x based on user input. This user input will select different routines to modify the script e.g. "Andy's_Routine"
might change x
to 4
. Currently I implement this using if statements in the for loop to check which routines are active, then applying them e.g.
Count = 0
for _ in itertools.repeat(None, Iterations):
x = 3
if "Andy's_Routine" in Active_Routines:
x = 4
die_roll = rnd.randint(1,6)
if "Bill's_Routine" in Active_Routines:
die_roll += 1
if "Chloe's_Routine" in Active_Routines:
# do something
pass
if "Person_10^5's_Routine" in Active_Routines:
# do something else
pass
if die_roll > x:
Count += 1
Probability_of_exceed = Count / Iterations
In practice the routines are not so simple that they can be generalised, they might add an extra output for example. The routines can be and are concurrently implemented. The problem is that there could be thousands of different routines, such that each loop will spend the majority of its time checking the if statements, slowing down the program.
Is there a better way of structuring the code that checks which routines are in use only once, and then modifies the iteration somehow?
2 answers

You're asking two things here  you want your code to be more Pythonic, and you want it to run faster.
The first one is easier to answer: make
Active_Routines
a list of functions instead of a list of strings, and call the functions from the list. Since these functions may need to change the local state (x
anddie_roll
), you will need to pass them the state as parameters, and let them return a new state. The refactor might look like this:def Andy(x, die_roll): return (4, die_roll) def Bill(x, die_roll): return (x, die_roll + 1) def Chloe(x, die_roll): # do something return (x, die_roll) Active_Routines = [Andy, Bill, Chloe] Count = 0 for i in range(Iterations): x = 3 die_roll = rnd.randint(1,6) for routine in Active_Routines: x, die_roll = routine(x, die_roll) if die_roll > x: Count += 1 Probability_of_exceed = Count / Iterations
The second one is harder to answer. This refactoring now makes a lot of function calls instead of checking
if
conditions; so there could be fewer missed branch predictions, but more function call overhead. You would have to benchmark it (e.g. using the timeit library) to be sure. However, at least this code should be easier to maintain. 
To apply Pythonic Way to your code, use the PEP8 Code Style Guide.
So:
Count
should becount
(use of capital letters only for classes);for ...:
statements with the determinate number of iteration can use list for iterate infor i in i_list:
orrange()
function for create list from intfor i in range(max_count)
;inside
for
may usecontinue
(orbreak
to finish loop) to skip the iteration after applying the changes OR use the single statementif ...: elif ...: elif ...: else:
, where you do not need to check one value for many checks.
@dataclasses
usage:from random import randint from dataclasses import dataclass @dataclass class Condition: x: int = 3 die_roll: int = randint(1,6) active_routine_list = ["Andy's_Routine", "Chloe's_Routine"] iterations = 5 count = 0 for _ in range(iterations): condition = Condition() print('from:', condition.die_roll) def _stack_conditions(routine_name): routine_conditions = { "Andy's_Routine": Condition(x=4, die_roll=condition.die_roll), "Chloe's_Routine": Condition(x=condition.x, die_roll=condition.die_roll + 1) } return routine_conditions[routine_name] for routine_name in active_routine_list: condition = _stack_conditions(routine_name) # Check this after one of the checks in the previous statement are applying if condition.die_roll > condition.x: count += 1 probability_of_exceed = count / iterations
Timeit:
[0.006040813000254275, 0.07721800600029383, 0.0213634470001125, 0.014464111999586748, 0.016983135000373295]
eval
usage: NOTE: it's not a good practice, but like a use casefrom random import randint active_routine_list = ["Andy's_Routine", "Chloe's_Routine"] iterations = 5 count = 0 for _ in range(iterations): x = 3 die_roll = randint(1,6) active_routines_action = { # Name : [x, die_roll] "Andy's_Routine": {'x': '4'}, "Bill's_Routine": {'die_roll': 'die_roll + 1'}, "Chloe's_Routine": {'x':'x + 5'}, "Person_10^5's_Routine": {} } # Apply routine actions related to active_routine_list for active_routine in active_routine_list: if active_routines_action[active_routine].get('x'): x = eval(active_routines_action[active_routine]['x']) if active_routines_action[active_routine].get('die_roll'): die_roll = eval(active_routines_action[active_routine]['die_roll']) # Check this after one of the checks in the previous statement are applying if die_roll > x: count += 1 probability_of_exceed = count / iterations
Timeit:
[0.0015670310003770282, 0.05414528299979793, 0.002308889999767416, 0.0019655290002447146, 0.0020383940000101575]
Your code updated to PEP8:
from random import randint count = 0 for _ in range(iterations): x = 3 die_roll = randint(1,6) # One statement for one check if "Andy's_Routine" in active_routine_list: x = 4 if "Bill's_Routine" in active_routine_list: die_roll += 1 if "Chloe's_Routine" in active_routine_list: # do something pass if "Person_10^5's_Routine" in active_routine_list: # do something else pass # Check this after one of the checks in the previous statement are applying if die_roll > x: count += 1 probability_of_exceed = count / iterations
if
usage timeit:[0.000327431999721739, 0.00041720400031408644, 0.0003407909998713876, 0.00031981899974198313, 0.00032971699965855805]