Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

重温 Python 中的石头剪刀布

Posted on 2022-04-13

重温 Python 中的石头剪刀布

当您第一次学习编程时,您会寻找(或者,可能是被分配)能够强化基本概念的项目。但是,一旦您获得了更多的知识和经验,您多久会从高级程序员的角度重新审视那些初学者项目?

在这篇文章中,我只想做到这一点。我想重温一个常见的初学者项目——用 Python 实现游戏“Rock Paper Scissors”——用我从近八年的 Python 编程经验中获得的知识。

?
你是初学者吗?不要点击离开!你仍然可以从这篇文章中学到很多东西。

目录

  • “石头剪刀布”的规则
  • 要求
  • “初学者”解决方案
  • 高级解决方案#1
  • 高级解决方案 #2
  • 高级解决方案#3
  • 结论
  • 是什么启发了这篇文章?

“石头剪刀布”的规则

在深入研究代码之前,让我们先概述一下“Rock Paper Scissors”是如何播放的。两名玩家每人选择三个项目之一:石头、纸或剪刀。玩家同时向对方透露他们的选择,获胜者由以下规则决定:

  1. 石头打剪刀
  2. 剪刀比纸
  3. 纸胜过岩石

长大后,我和我的朋友们用“石头剪刀布”解决了各种各样的问题。在单人视频游戏中谁先玩?谁得到最后一罐汽水?谁必须去收拾我们刚刚制造的烂摊子?重要的东西。

要求

让我们列出实现的一些要求。让我们专注于编写一个名为play()的函数,而不是构建一个完整的游戏,它接受两个字符串参数——每个玩家选择的"rock" 、 "paper"或"scissors" ——并返回一个字符串表示获胜者(例如, "paper wins" )或如果游戏结果为平局(例如, "tie" )。

以下是play()的调用方式及其返回内容的一些示例:

 >>> play("rock", "paper") 'rock wins' >>> play("scissors", "paper") 'scissors wins' >>> play("paper", "paper") 'tie'

如果两个参数中的一个或两个无效,这意味着它们不是"rock" 、 "paper"或"scissors"之一,那么play()应该引发某种异常。

play()也应该是可交换的。也就是说, play("rock", "paper")应该返回与play("paper", "rock")相同的内容。

“初学者”解决方案

要设置比较基准,请考虑初学者如何实现play()函数。如果这个初学者和我刚学编程时一样,他们可能会开始写一大堆if语句:

 def play(player1_choice, player2_choice): if player1_choice == "rock": if player2_choice == "rock": return "tie" elif player2_choice == "paper": return "paper wins" elif player2_choice == "scissors": return "rock wins" else: raise ValueError(f"Invalid choice: {player2_choice}") elif player1_choice == "paper": if player2_choice == "rock": return "paper wins" elif player2_choice == "paper": return "tie" elif player2_choice == "scissors": return "rock wins" else: raise ValueError(f"Invalid choice: {player2_choice}") elif player1_choice == "scissors": if player2_choice == "rock": return "rock wins" elif player2_choice == "paper": return "scissors wins" elif player2_choice == "scissors": return "tie" else: raise ValueError(f"Invalid choice: {player2_choice}") else: raise ValueError(f"Invalid choice: {player1_choice}")

严格来说,这段代码没有任何问题。它运行无误并满足所有要求。它也类似于谷歌搜索“rock paper scissors python”的一些高级实现。

不过,有经验的程序员会很快识别出许多代码异味。尤其是代码是重复的,有很多可能的执行路径。

高级解决方案#1

从更高级的角度实现“Rock Paper Scissors”的一种方法是利用 Python 的字典类型。字典可以根据游戏规则将项目映射到他们击败的项目。

让我们称这个字典loses_to (命名很难,你们大家):

 loses_to = { "rock": "scissors", "paper": "rock", "scissors": "paper", }

loses_to提供了一个简单的 API 来确定哪个物品输给了另一个物品:

 >>> loses_to["rock"] 'scissors' >>> loses_to["scissors"] 'paper'

字典有几个好处。您可以使用它来:

  1. 通过检查成员资格或引发KeyError来验证所选项目
  2. 通过检查一个值是否输给相应的键来确定赢家

考虑到这一点, play()函数可以编写如下:

 def play(player1_choice, player2_choice): if player2_choice == loses_to[player1_choice]: return f"{player1_choice} wins" if player1_choice == loses_to[player2_choice]: return f"{player2_choice} wins" if player1_choice == player2_choice: return "tie"

在这个版本中, play()在尝试访问无效键时利用了loses_to字典引发的内置KeyError 。这有效地验证了玩家的选择。因此,如果任何一个玩家选择了一个无效的项目——比如"lizard"或1234 —— play()就会引发KeyError :

 >>> play("lizard", "paper") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in play KeyError: 'lizard'

尽管KeyError不如带有描述性消息的ValueError有用,但它仍然可以完成工作。

新的play()函数比原来的函数简单得多。无需处理一堆显式案例,只需检查三种案例:

  1. player2_choice输给player1_choice
  2. player1_choice输给player2_choice
  3. player1_choice和player2_choice是一样的

然而,还有第四个隐藏的案例,你几乎必须眯着眼才能看到。当其他三种情况都不为真时,就会发生这种情况,在这种情况下play()返回None值。

但是……这种情况真的会发生吗?实际上,没有。它不能。根据游戏规则,如果玩家 1 没有输给玩家 2 ,玩家 2 也没有输给玩家 1,那么两个玩家肯定选择了相同的物品。

换句话说,我们可以从play()中删除最后一个if块,如果其他两个if块都没有执行,则只return "tie" :

 def play(player1_choice, player2_choice): if player2_choice == loses_to[player1_choice]: return f"{player1_choice} wins" if player1_choice == loses_to[player2_choice]: return f"{player2_choice} wins" return "tie"

我们已经做出了权衡。我们牺牲了清晰度——我认为与“初学者”版本相比,理解上述play()函数的工作原理需要更大的认知负荷——以缩短函数并避免无法访问的状态。

这种折衷值得吗?我不知道。纯度胜过实用性吗?

高级解决方案 #2

以前的解决方案效果很好。它比“初学者”解决方案可读且短得多。但它不是很灵活。也就是说,如果不重写一些逻辑,它就无法处理“Rock Paper Scissors”的变体。

例如,有一个名为“Rock Paper Scissors Lizard Spock”​​的变体,它有一套更复杂的规则:

  1. 岩石击败剪刀和蜥蜴
  2. 纸击败摇滚和斯波克
  3. 剪刀打纸和蜥蜴
  4. 蜥蜴击败斯波克和纸
  5. 斯波克打败剪刀和石头

你如何调整代码来处理这种变化?

首先,将loses_to字典中的字符串值替换为 Python 集。每个集合包含所有输给相应键的项目。下面是这个版本的loses_to使用原始“Rock Paper Scissors”规则的样子:

 loses_to = { "rock": {"scissors"}, "paper": {"rock"}, "scissors": {"paper"}, }

为什么要套?因为我们只关心给定密钥丢失了哪些项目。我们不关心这些项目的顺序。

要使play()适应新的loses_to字典,您所要做的就是将==替换为in以使用成员资格检查而不是相等检查:

 def play(player1_choice, player2_choice): # vv--- replace == with in if player2_choice in loses_to[player1_choice]: return f"{player1_choice} wins" # vv--- replace == with in if player1_choice in loses_to[player2_choice]: return f"{player2_choice} wins" return "tie"

花点时间运行此代码并验证一切是否仍然有效。

现在将loses_to替换为实现“Rock Paper Scissors Lizard Spock”​​规则的字典。这看起来像:

 loses_to = { "rock": {"scissors", "lizard"}, "paper": {"rock", "spock"}, "scissors": {"paper", "lizard"}, "lizard": {"spock", "paper"}, "spock": {"scissors", "rock"}, }

新的play()函数完美地适用于这些新规则:

 >>> play("rock", "paper") 'paper wins' >>> play("spock", "lizard") 'lizard wins' >>> play("spock", "spock") 'tie'

在我看来,这是选择正确数据结构的力量的一个很好的例子。通过使用集合来表示丢失到loses_to字典中的键的所有项目并将==替换为in ,您已经制定了更通用的解决方案,而无需添加一行代码。

高级解决方案#3

让我们退后一步,采取一种稍微不同的方法。我们将构建一个包含所有可能输入及其结果的表格,而不是在字典中查找项目以确定获胜者。

你仍然需要一些东西来代表游戏规则,所以让我们从之前解决方案中的loses_to字典开始:

 loses_to = { "rock": {"scissors"}, "paper": {"rock"}, "scissors": {"paper"}, }

接下来,编写一个函数build_results_table() ,它接受一个规则字典,如loses_to ,并返回一个将状态映射到其结果的新字典。例如,以下是build_results_table()在使用loses_to作为参数调用时应返回的内容:

 >>> build_results_table(loses_to) { {"rock", "scissors"}: "rock wins", {"paper", "rock"}: "paper wins", {"scissors", "paper"}: "scissors wins", {"rock", "rock"}: "tie", {"paper", "paper"}: "tie", {"scissors", "scissors"}: "tie", }

如果你认为有什么东西看起来在那里,你是对的。这本词典有两个问题:

  1. {"rock", "rock"}这样的集合不能存在。集合不能有重复的元素。在真实场景中,这个集合看起来像{"rock"} 。您实际上不必担心太多。我用两个元素编写了这些集合,以明确这些状态代表什么。
  2. 您不能将集合用作字典键。但是我们想使用集合,因为它们会自动为我们处理交换性。也就是说, {"rock", "paper"}和{"paper", "rock"}的计算结果相等,因此在查找时应该返回相同的结果。

解决这个问题的方法是使用 Python 的内置frozenset类型。像集合一样, frozensets成员检查,并且当且仅当两个集合具有相同的成员时,它们才与另一个set或frozenset进行比较。然而,与标准集不同, frozenset实例是不可变的。因此,它们可以用作字典键。

要实现build_results_table() ,您可以遍历loses_to字典中的每个键,并为集合中与键对应的每个字符串值构建一个frozenset实例:

 def build_results_table(rules): results = {} for key, values in rules.items(): for value in values: state = frozenset((key, value)) result = f"{key} wins" results[state] = result return results

这让你走到了一半:

 >>> build_results_table(loses_to) {frozenset({'rock', 'scissors'}): 'rock wins', frozenset({'paper', 'rock'}): 'paper wins', frozenset({'paper', 'scissors'}): 'scissors wins'}

但是,导致平局的州不包括在内。要添加这些,您需要为映射到字符串"tie"的rules字典中的每个键创建frozenset实例:

 def build_results_table(rules): results = {} for key, values in rules.items(): # Add the tie states results[frozenset((key,))] = "tie" # <-- New # Add the winning states for value in values: state = frozenset((key, value)) result = f"{key} wins" results[state] = result return results

现在build_results_table()返回的值看起来是正确的:

 >>> build_results_table(loses_to) {frozenset({'rock'}): 'tie', frozenset({'rock', 'scissors'}): 'rock wins', frozenset({'paper'}): 'tie', frozenset({'paper', 'rock'}): 'paper wins', frozenset({'scissors'}): 'tie', frozenset({'paper', 'scissors'}): 'scissors wins'}

为什么要经历所有这些麻烦?毕竟, build_results_table()看起来比之前解决方案中的play()函数更复杂。

你没有错,但我想指出这种模式非常有用。如果程序中可以存在的状态数量有限,您有时可以通过预先计算所有这些状态的结果来显着提高速度。这对于像“石头剪刀布”这样简单的东西来说可能有点过头了,但在有数十万甚至数百万个州的情况下可能会产生巨大的差异。

这种方法有意义的一个真实场景是强化学习应用中使用的Q 学习算法。在该算法中,维护了一张状态表——Q 表——将每个状态映射到一组预先确定的动作的概率。一旦代理受过训练,它就可以根据观察到的状态的概率进行选择和行动,然后采取相应的行动。

通常,会计算一个类似于build_results_table()生成的表,然后将其存储在一个文件中。当程序运行时,预先计算的表被加载到内存中,然后由应用程序使用。

因此,既然您有一个可以构建结果表的函数, loses_to表分配给outcomes变量:

 outcomes = build_results_table(loses_to)

现在您可以编写一个play()函数,该函数根据传递给 play 的参数在outcomes表中查找状态,然后返回结果:

 def play(player1_choice, player2_choice): state = frozenset((player1_choice, player2_choice)) return outcomes[state]

这个版本的play()非常简单。就两行代码!如果你愿意,你甚至可以将它写成一行:

 def play(player1_choice, player2_choice): return outcomes[frozenset((player1_choice, player2_choice))]

就个人而言,我更喜欢两行版本而不是单行版本。

您的新play()函数遵循游戏规则并且是可交换的:

 >>> play("rock", "paper") 'paper wins' >>> play("paper", "rock") 'paper wins'

如果使用无效的选择调用play()甚至会引发KeyError ,但由于outcomes字典的键是集合,因此该错误的帮助较小:

 >>> play("lizard", "paper") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 21, in play return outcomes[state] KeyError: frozenset({'lizard', 'paper'})

然而,模糊的错误可能不是问题。在本文中,您只实现play()函数。在“Rock Paper Scissors”的真正实现中,您很可能会捕获用户输入并在将用户的选择传递给play()之前对其进行验证。

那么,这个实现与以前的实现相比要快多少呢?下面是一些计时结果,用于比较使用 IPython 的%timeit魔术函数的各种实现的性能。 play1()是高级解决方案 #2部分中play() () 的版本,而play2()是当前版本:

 In [1]: %timeit play1("rock", "paper") 141 ns ± 0.0828 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) In [2]: %timeit play2("rock", "paper") 188 ns ± 0.0944 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

在这种情况下,使用结果表的解决方案实际上比以前的实现要慢。这里的罪魁祸首是将函数参数转换为frozenset的行。因此,尽管字典查找速度很快,并且构建将状态映射到结果的表可能会提高性能,但您需要小心避免昂贵的操作,这些操作最终可能会否定您期望获得的任何收益。

结论

我写这篇文章作为练习。既然我有很多经验,我很想知道如何在 Python 中处理像“Rock Paper Scissors”这样的初学者项目。我希望你觉得它很有趣。如果您现在有任何灵感来重新审视自己的一些初学者项目,那么我想我已经完成了我的工作!

如果您确实修改了自己的一些初学者项目,或者您过去曾这样做过,请在评论中告诉我它的进展情况。你有学到什么新东西吗?您的新解决方案与您作为初学者编写的解决方案有何不同?

是什么启发了这篇文章?

来自 Julia 世界的一位资深人士Miguel Raz Guzmán Macedo让我关注Mosè Giordano的博客文章。 Mosè 利用 Julia 的多重调度范式用不到 10 行代码编写了“Rock Paper Scissors”:

不到 10 行代码的剪刀石头布游戏

石头剪刀布是一种流行的手游戏。然而,一些书呆子可能更喜欢在他们的电脑上玩这个游戏,而不是实际握手。图片来源:Enzoklop、Wikimedia Commons、CC-BY-SA 3.0 我们可以用 Julia 编程语言不到 10 行代码编写这个游戏。蒂…

重温 Python 中的石头剪刀布摩西·佐丹奴摩西·佐丹奴

重温 Python 中的石头剪刀布

我不会详细介绍 Mosè 的代码是如何工作的。 Python 甚至不支持开箱即用的多分派。 (虽然你可以在plum package的帮助下使用它。)

Mosè 的文章让我的大脑开始运转,并鼓励我重新审视 Python 中的“Rock Paper Scissors”,思考如何以不同的方式处理这个项目。

然而,当我研究解决方案时,我想起了很久以前我为 Real Python 写过的一篇文章:

制作你的第一个 Python 游戏:石头、纸、剪刀! – 真正的 Python

在本教程中,您将学习从头开始用 Python 编写剪刀石头布。您将学习如何接受用户输入、让计算机选择随机动作、确定获胜者以及将代码拆分为函数。

重温 Python 中的石头剪刀布真正的 Python真正的 Python

重温 Python 中的石头剪刀布

事实证明,我在这里“发明”的前两个解决方案与 Real Python 文章的作者 Chris Wilkerson 提出的解决方案相似。

Chris 的解决方案功能更全面。它包括一个交互式游戏机制,甚至使用 Python 的Enum类型来表示游戏项目。那一定也是我第一次听说“Rock Paper Scissors Lizard Spock”​​的地方。


你喜欢这篇文章吗?通过订阅我的每周好奇代码通讯,随时了解我的所有内容的最新信息,尽早访问我的课程,并从 Python 和 Julia 社区中挑选的内容直接发送到您的收件箱。

✉️ 立即订阅

来源: https://davidamos.dev/revisiting-rock-paper-scissors-in-python/

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • Elad Gil
  • Ellie Huxtable
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mostly metrics
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme