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

  • answered 2019-11-08 13:51 kaya3

    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 and die_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.

  • answered 2019-11-08 14:00 Dmytro Hoi

    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 in for i in i_list: or range() function for create list from int for i in range(max_count);

    • inside for may use continue (or break to finish loop) to skip the iteration after applying the changes OR use the single statement if ...: 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 case

    from 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]