0
\$\begingroup\$

Background/context: This program randomly selects predefined items ("Names" as strings) according to 2 rules:

#Rule 1: Each name/item in the list can only be selected once.

#Rule 2: Single item exclusion cases for all random selections such that a person cannot select themselves.

This program serves its intended purpose, but I'm seeking ways to improve the "logic" in the program such that the Exception case cannot occur in the first place and/or ways to reduce the lines of code.

Code example below:

#Rule 1: Each name/item in the list can only be selected once.
#Rule 2: Single item exclusion cases for all random selections such that a person cannot select themselves.

import random

#Function that loops through a list, randomly selecting 1 name/item from the list until the list is empty.
def choice_excluding(lst, exception):
  if len(lst) > 0:
    possible_choices = [v for v in lst if v != exception]
    return random.choice(possible_choices)

#Exception handling function for line 60, there is a case where "Morgan" can select themself against the rules of the program.
def choice_debug(lstdebug):
    return random.choice(lstdebug)

#Primary list
lst = ["Hunter", "Alena", "Derrell", "Christian", "Grant", "Audrey", "Elyas", "Loelle", "Morgan"]

#List for exception handling
lstdebug = ["Hunter", "Alena", "Derrell", "Christian", "Grant", "Audrey", "Elyas", "Loelle"]

#String variable for exception handling
alternate_choice = "Morgan"

#Print random names from the list until it's empty while excluding a specific item/name each time and removing the selected item/name from the list each time so it can't be selected again.
choice1 = choice_excluding(lst, "Hunter")
print("Hunter your assingment is " + choice1)
lst.remove(choice1)

choice2 = choice_excluding(lst, "Alena")
print("Alena your assingment is " + choice2)
lst.remove(choice2)

choice3 = choice_excluding(lst, "Derrell")
print("Derrell your assingment is " + choice3)
lst.remove(choice3)

choice4 = choice_excluding(lst, "Christian")
print("Christian your assingment is " + choice4)
lst.remove(choice4)

choice5 = choice_excluding(lst, "Grant")
print("Grant your assingment is " + choice5)
lst.remove(choice5)

choice6 = choice_excluding(lst, "Audrey")
print("Audrey your assingment is " + choice6)
lst.remove(choice6)

choice7 = choice_excluding(lst, "Elyas")
print("Elyas your assingment is " + choice7)
lst.remove(choice7)

choice8 = choice_excluding(lst, "Loelle")
print("Loelle your assingment is " + choice8)
lst.remove(choice8)

#Exception handling, programatically swapping choice9 in the case where Morgan can select themself by creating a new variable choice10. Morgan's new choice is selected from all other items/names in a new random pick then assign the name that had Morgan's choice to have "Morgan" instead.
try:
  choice9 = choice_excluding(lst, "Morgan")
  print("Morgan your assingment is " + choice9)
except Exception as e:
  print("Oops!", e.__class__, "occurred.")
  choice10 = choice_debug(lstdebug)
  print("Morgan your assingment is " + choice10)
  if choice1 == choice10:
    print("Hunter your new assingment is " + alternate_choice)
  
  if choice2 == choice10:
    print("Alena your new assingment is " + alternate_choice)
                          
  if choice3 == choice10:
    print("Derrell your new assingment is " + alternate_choice)

  if choice4 == choice10:
    print("Christian your new assingment is " + alternate_choice)

  if choice5 == choice10:
     print("Grant your new assingment is " + alternate_choice)

  if choice6 == choice10:
    print("Audrey your new assingment is " + alternate_choice)

  if choice7 == choice10:
    print("Elyas your new assingment is " + alternate_choice)

  if choice8 == choice10:
    print("Loelle your new assingment is " + alternate_choice)
                          
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

If I'm understanding your program correctly, the purpose is for each name to be assigned to a different name, like in a Secret Santa party.

The first thing to notice is that you have variables like choice1, choice2, etc. These variable names mean that you actually want a list or some other ordered container. Second, you have a lot of repeated code, which means what you want is a loop. So, using these two structures, the first part of the code can be written like this (ignoring the error case for a moment):

lst = ["Hunter", "Alena", "Derrell", "Christian", "Grant", "Audrey", "Elyas", "Loelle", "Morgan"]
working_copy = lst.copy()
choices = []
for person in lst:
    choices.append(choice_excluding(working_copy, person))
    working_copy.remove(choices[-1])

for person, assignment in zip(lst, choices):
    print(f"{person}, your assignment is {assignment}.")

By using loops and lists, we can more clearly express what we are doing. Plus, we are no longer have to rewrite the code if the name list changes size. By working on a separate copy of the list, we can modify it and still have access to the original list to match up the pairings. The other useful function here is zip takes multiple lists and returns tuples of corresponding items so you can iterate through both lists at the same time.

Now, what about the error case? The error occurs when the last remaining name in the list is the same as the name to be assigned, causing the function choice_excluding() to fail. We can fix by swapping this last name with an earlier name.

lst = ["Hunter", "Alena", "Derrell", "Christian", "Grant", "Audrey", "Elyas", "Loelle", "Morgan"]
working_copy = lst.copy()
choices = []
for person in lst:
    try:
        choices.append(choice_excluding(working_copy, person))
        working_copy.remove(choices[-1])
    except Exception:
        swap_index = random.randrange(len(choices))
        swapped_person = choices[swap_index]
        choices.append(swapped_person)
        choices[swap_index] = person

for person, assignment in zip(lst, choices):
    print(f"{person}, your assignment is {assignment}.")

The error-fixing part begins with except Exception:. The first two lines pick a random name in choices to swap with the current name. Let's say the last name in lst and working_copy is Morgan. Since Morgan is the only name to choose from, choices_excluding() raises an exception. Swapping this name with another in choices is a fix because Morgan is not in choices nor in the part of lst that matches with choices. The function random.randrange(n) picks a number from 0 to n-1, in other words, an index in a list of length n. So, the call to random.randrange(len(choices)) picks a random integer from 0 to len(choices) - 1, in other words, an index in choices. If the length of choices is 10, this would pick a random number from 0 to 9. The next two lines replaces the swapped_person with Morgan and puts the swapped_person at the end.

With this algorithm, you can get rid of your debugging lists and functions, as well as the "new assignment" output.

Now, let's look at the choice_excluding() function. This function has to build a new list on every call. For a list of 9 people, this doesn't matter. But, if the list has hundreds or thousands of names, it will take a very long time for the program to run. A better way is to keep picking names from the list until a name is picked that does not match the exception.

def choice_excluding(lst, exception):
    while True:
        choice = random.choice(lst)
        if choice != exception:
            return choice

But, what about the error case? This can only happen if there is one name in the list and it matches the exception.

def choice_excluding(lst, exception):
    if len(lst) == 1 and lst[0] == exception:
        raise Exception(f"No valid choice from {lst} excluding {exception}")

    # Alternate error check: if lst == [exception]:

    while True:
        choice = random.choice(lst)
        if choice != exception:
            return choice

Putting everything in one place:

import random

def choice_excluding(lst, exception):
    if len(lst) == 1 and lst[0] == exception:
        raise Exception(f"No valid choice from {lst} excluding {exception}")

    while True:
        choice = random.choice(lst)
        if choice != exception:
            return choice

lst = ["Hunter", "Alena", "Derrell", "Christian", "Grant", "Audrey", "Elyas", "Loelle", "Morgan"]
working_copy = lst.copy()
choices = []
for person in lst:
    try:
        choices.append(choice_excluding(working_copy, person))
        working_copy.remove(choices[-1])
    except Exception:
        swap_index = random.randrange(len(choices))
        swapped_person = choices[swap_index]
        choices.append(swapped_person)
        choices[swap_index] = person

for person, assignment in zip(lst, choices):
    print(f"{person}, your assignment is {assignment}.")


There is a way to implement a slightly restricted version of this algorithm without trial-and-error. The restriction is that it will be impossible to end up with assignments like Hunter -> Alena and Alena -> Hunter. The assignments will form a single loop like (for example):

Grant, your assignment is Elyas.
Elyas, your assignment is Morgan.
Morgan, your assignment is Derrell.
Derrell, your assignment is Loelle.
Loelle, your assignment is Christian.
Christian, your assignment is Audrey.
Audrey, your assignment is Alena.
Alena, your assignment is Hunter.
Hunter, your assignment is Grant.

To form a list like this, use the following steps:

  1. Shuffle the list.
  2. Copy the shuffled list.
  3. On the copy, move the first name to the end.
  4. Match up the two lists in order.

In code, that would look like this:

import random

lst = ["Hunter", "Alena", "Derrell", "Christian", "Grant", "Audrey", "Elyas", "Loelle", "Morgan"]
random.shuffle(lst)
choices = lst.copy()
choices = choices[1:] + choices[0:1]
for person, assignment in zip(lst, choices):
    print(f"{person}, your assignment is {assignment}.")

It is a very common pattern for a function or operation to exclude the value representing the end of a sequence. For example:

s = [13, 24, 35, 46, 57, 68]
print(s[2:5])  # [35, 46, 57]

The colon operator in 2:5 represents a range of indices from 2 to 5 but not including 5, that is, the indices 2, 3, and 4. So, the call to choices[0:1] creates a new list with only the first item (index 0). This is different than choices[0] because the expression would return just the first item, not a list containing one item. In the original lst, lst[0] == "Hunter", lst[0:1] == ["Hunter"]. The other range choices[1:] means the items in choices from index 1 to the end (or, all but the first).

There's no guess-and-check and no functions to write.

\$\endgroup\$
7
  • \$\begingroup\$ Hi Mark, thank you for your answer, it's very helpful. I have a couple questions for clarification on two of the lines: swap_index = random.randrange(len(choices) - 1) Can you explain what this line of code is doing to prepare for the variable swap? I know the len function gives a numerical representation of an object, so is this line saying to pick a new random object from the "choices" list as it's index position? choices = choices[1:] + choices[0:1] I'm not very familiar with the colon operator, so I'm not sure what this line of code is doing. \$\endgroup\$ Commented Dec 5, 2022 at 4:35
  • 1
    \$\begingroup\$ @ChristianR I've added answers to your questions to my answer. Let me know if anything remains unclear. \$\endgroup\$
    – Mark H
    Commented Dec 5, 2022 at 7:53
  • \$\begingroup\$ Mark, it seems like there's a bug with the swap method. What can happen is if the first person in the list picks themselves, the program is stuck. Reason being, "Hunter" is the first item in the list. If "Hunter" picks himself, the swap_index variable and call random.randrange(len(choices) -1) is triggered. Since "Hunter" is the only object in the "choices" list at this point, there's nothing else in the choices list to swap with. Do you have any thoughts of how to fix that bug? \$\endgroup\$ Commented Dec 6, 2022 at 5:19
  • \$\begingroup\$ Correction, I was missing this line in my code: if len(lst) == 1 and lst[0] == exception: raise Exception This line seems to fix that error case from happening, can you explain how that is, it's not making sense to me. \$\endgroup\$ Commented Dec 6, 2022 at 5:31
  • 2
    \$\begingroup\$ @ChristianR First, keep asking questions if something I wrote is confusing. Second, you have correctly figured out the exception-and-swap part of the code. The fact that understanding what I wrote requires jumping between so many parts of the code means that there's probably a simpler way to do this. Actually, let me make an edit to the answer to simplify things. \$\endgroup\$
    – Mark H
    Commented Dec 6, 2022 at 8:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.