简体   繁体   中英

Is there a more pythonic way to write multiple comparisons

I am writing a simple card game (Similar to Snap). I've got it working, without problems, but I feel that there ought to be a more elegant solution. Given a set of win conditions:
Y beats R
R beats B
B beats Y
etc

I want to compare the two player's cards and assign both cards to the winner. Caveat: I'm teaching at secondary school level (no OOP) and want to be able to discuss the resulting code with students.

I've left the final condition as an elif, as I wanted to go back and add extra cards to the list of options

The if - elif chain works without problems; I was wondering if there was a more elegant solution.

    #I have code that randomly selects from a list, but this is the basic 
    #idea:
    p1=input("enter r,y or b")  
    p2=input("enter r,y or b")  

    stack1=[]  
    stack2=[]  

    if   p1=="r" and p2=="b":  
        stack1.extend([p1,p2])  
    elif p1=="y" and p2=="r":  
        stack1.extend([p1,p2])  
    elif p1 =="b" and p2 =="y":  
        stack1.extend([p1,p2])  
    elif p2 =="r" and p1 =="b":  
        stack2.extend([p1,p2])  
    elif p2 =="y" and p1 =="r":  
        stack2.extend([p1,p2])             
    elif p2 =="b" and p1 =="y":  
        stack2.extend([p1,p2])  

    print(stack1)  
    print(stack2)  

I've excerpted the code from the remainder - the cards are all randomly generated, so no user input is actually required.

Create a new dictionary with Y, R, B each mapping to 0, 1, 2.

win_map = {"Y": 0, "R": 1, "B": 2}

We can see a cyclic relationship here. 0 beats 1, 1 beats 2, and 2 beats 0. The first two cases are easy to determine using a simple > , but taking the third case into account needs another method. With a bit of ingenuity, we can see that we can "wrap" by adding 1 and using a modulo operation % . (0+1) % 3 == 1 , (1+1) % 3 == 2 , and (2+1) % 3 == 0 , and these 3 cases are the only cases where a winner is determined.

if (win_map[p1] + 1) % 3 == win_map[p2]: ...  # p1 wins
else if (win_map[p2] + 1) % 3 == win_map[p1]: ... # p2 wins

I am not sure how well this will be conveyed to students though, but it is a cleaner solution.

Note: this method will not work with more cards, as the cyclic relationship will be broken.

So your win conditions look like a collection of (winner, loser) pairs and comparing your (p1, p2) input to them looks like the simplest thing to do.

win_conditions = {
    ('y', 'r'),
    ('r', 'b'),
    ('b', 'y')
}

p1=input("enter r,y or b")
p2=input("enter r,y or b")

stack1=[]
stack2=[]

if (p1, p2) in win_conditions:
    stack1.extend([p1,p2])
elif (p2, p1) in win_conditions:
    stack2.extend([p1,p2])
else:
    raise ValueError('{} and {} cannot beat each other.'.format(p1, p2))

Note that the code can be simplified if you assume that the win conditions are exhaustive.

I think you will do your students a favor if you show them how to improve readability by encansulating low-level operations in functions with proper names so that the intent is more obvious.

def beats(p1, p2):
    return (p1, p2) in win_conditions

if beats(p1, p2):
    stack1.extend([p1,p2])
elif beats(p2, p1):
    stack2.extend([p1,p2])
else:
    raise ValueError('"{}" and "{}" cannot beat each other.'.format(p1, p2))

Maybe you can find a better name for whatever you want to achieve by extending the list.

"Standard" solution for small scale problem like yours is to put all possibilities into map:

result_map = { ('r', 'b'): 1, ('b', 'y'): 1, ('y', 'r'): 1, 
('b', 'r'): 2, ('y', 'b'): 2, ('r', 'y'): 2 }
v = result_map.get((p1, p2), None)
if v == 1:
    stack1.extend([p1, p2])
elif v == 2:
    stack2.extend([p1, p2])

Why like this? Because it gives you easy way to change win / loose condition (just change a dictionary), win / loose rules can be completely arbitrary and it is easy to follow code (image you've some weird if-else condition and someone else comes looking at this code and wonders, what is going on and why).

You're repeating the same thing too many times, there are two extend calls that are repeated 3 times each.

You could greatly simplify your code by using the or keyword:

# extend to "stack1"
if (p1 == "r" and p2 == "b") or (p1 == "y" and p2 == "r") or (p1 == "b" and p2 == "y"):
  stack1.extend([p1, p2])

# extend to "stack2"
elif (p2 == "r" and p1 == "b") or (p2 == "y" and p1 == "r") or (p2 == "b" and p1 == "y"):
  stack2.extend([p1, p2])

Good luck.

Just another way to compare, maybe it can be used in for loop or dict map to help refactor your code, if necessary and performance not important.

from operator import and_ 
from functools import partial

def compare(a, b, c, d):
    return and_(a == c, b == d) 

p1 = 'r'
p2 = 'b'
cmp_p1_p2 = partial(compare, p1, p2)
cmp_p2_p1 = partial(compare, p2, p1)

cmp_p1_p2('r', 'b')
# True
cmp_p2_p1('b', 'r')
# True

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM