我是Java + Angular开发人员。我的新工作是担任高级Python开发人员……但是我一生中从未写过Python程序。为了自学语言,我开始编写一些简单的项目。

这是Python 3.7中基于控制台的Hangman游戏。游戏从列表中选择一个“秘密单词”,然后进入游戏循环,提示用户猜字母。经过6次错误的猜测后,游戏结束并且用户输了。否则,如果用户猜对了所有字母,则用户获胜。游戏结束后,系统会提示用户启动新游戏,该游戏将使用新的秘密单词重新运行该方法。

我省略了单词列表和drawing子手绘图,因为我是从这个要点中抄写它们的在GitHub上作为直接复制粘贴(将hangman数组重命名为HANGMAN_STAGES,并将单词列表重命名为WORDS)。如果需要的话,我可以在帖子中添加它,但似乎确实不需要60行。

 import random
import sys
from typing import Tuple

# Omitted declarations: https://gist.github.com/chrishorton/8510732aa9a80a03c829b09f12e20d9c
# HANGMAN_STAGES = [...]
# WORDS = ...

def run_game() -> None:
    """The main game loop. Will prompt the user if they would like to start a new game at the end."""
    print("WELCOME TO HANGMAN.")

    secret_word = pick_secret_word()
    guessed_letters = []
    incorrect_guesses = 0
    won = False
    round = 1

    while won == False and incorrect_guesses < 6:
        print('\n\nROUND ' + str(round))
        incorrect_guesses, won = process_turn(incorrect_guesses, secret_word, guessed_letters)
        round += 1

    print("\n\n")

    if won == False:
        print("GAME OVER! You lost.")
        draw_hangman(6)
    else:
        print("Congratulations! You won!", end=" ")

    print("The secret word was: " + secret_word)

    if play_again():
        run_game()


def pick_secret_word() -> str:
    """
    Chooses a new secret word from the list of available secret words. The word is chosen psuedo-randomly.
    :return: the new secret word
    """
    index = random.randint(0, len(WORDS))
    return WORDS[index].upper()


def process_turn(incorrect_guess_count: int, secret_word: str, guessed_letters: list) -> Tuple[int, bool]:
    """
    Processes a user's turn. First draws the current state of the game: current hangman, partially-guessed word, and
    list of previously guessed letters. Then prompts the user for their next guess, evaluates that guess to see if it
    was correct, and then updates the game state.

    :param incorrect_guess_count: the number of previous incorrect guesses
    :param secret_word: the secret word
    :param guessed_letters: the list of previously guessed letters
    :return: (updated number of inccorect guesses, True/False indication of whether the user has won)
    """
    draw_hangman(incorrect_guess_count)
    draw_secret_word(secret_word, guessed_letters)
    print_guessed_letters(guessed_letters)
    next_letter = prompt_for_guess(guessed_letters)
    return apply_guess(next_letter, secret_word, incorrect_guess_count, guessed_letters)


def print_guessed_letters(guessed_letters: list) -> None:
    """
    Sorts the list of previously-guessed letters and prints it to screen.

    :param guessed_letters: the list of previously guessed letters
    :return: Nothing
    """
    guessed_letters.sort()
    print("Guesses: " + str(guessed_letters))


def apply_guess(next_letter: str, secret_word: str, incorrect_guess_count: int, guessed_letters: list) -> Tuple[int, bool]:
    """
    Checks the validity of the user's guess. If the guess was incorrect, increments the number of incorrect guesses by
    1. If the user has guessed all of the letters in the secret word, return an indication that the user has won the
    game.

    :param next_letter: the user's guess
    :param secret_word: the secret word
    :param incorrect_guess_count: the number of previously incorrect guesses
    :param guessed_letters: the list of previously guessed letters
    :return: (the updated number of incorrected guesses, True/False indicating if the user has won the game)
    """
    guessed_letters.append(next_letter)
    correct, letters_remaining = check_guess_against_secret(next_letter, secret_word, guessed_letters)

    if correct == False:
        incorrect_guess_count += 1

    if letters_remaining == 0:
        return incorrect_guess_count, True

    return incorrect_guess_count, False


def check_guess_against_secret(next_letter: str, secret_word: str, guessed_letters: list) -> Tuple[bool, int]:
    """
    Determines if the user has guessed correctly. Also evaluates the secret word to determine if there are more letters
    left for the user to guess.

    :param next_letter: the user's guessed letter
    :param secret_word: the secret word
    :param guessed_letters: the list of previously guessed letters
    :return: (True/False indicating if the guess was correct, 0 if no letters left and positive integer otherwise)
    """
    correct = next_letter in secret_word

    letters_remaining = 0
    for letter in secret_word:
        # Known issue: if a letter is present in the secret multiple times, and is not guessed,
        # letters_remaining incremented by more than one.
        if letter not in guessed_letters:
            letters_remaining += 1

    return correct, letters_remaining


def prompt_for_guess(guessed_letters: list) -> str:
    """
    Prompts the user for their next guess. Rejects guesses that are more than a single letter, and guesses which were
    already made previously. Returns the (validated) guess.

    :param guessed_letters: the list of previously guessed letters
    :return: the user's next guess
    """
    guess = input("Your guess? ").strip().upper()
    if len(guess) > 1:
        print("Sorry, you can only guess one letter at a time.")
        return prompt_for_guess(guessed_letters)
    elif guess in guessed_letters:
        print("Sorry, you already guessed that letter.")
        return prompt_for_guess(guessed_letters)
    return guess


def draw_hangman(number_of_incorrect_guesses: int) -> None:
    """
    Draws the appropriate hangman stage, given the number of incorrect guesses. 0 or fewer will draw the empty scaffold.
    6 or more will draw the fully hanged man.

    :param number_of_incorrect_guesses: the number of incorrect guesses the player has made in the current game
    :return: Nothing
    """
    if (number_of_guesses < 0):
        number_of_guesses = 0
    if (number_of_guesses > 6):
        number_of_guesses = 6
    print(HANGMAN_STAGES[number_of_guesses])


def draw_secret_word(secret_word: str, guessed_letters: list) -> None:
    """
    Prints the secret word, with underscores representing unknown letters and with any correctly-guessed leters printed
    in the appropriate location within the word.

    :param secret_word: The secret word
    :param guessed_letters: All previous guesses
    :return: Nothing
    """
    for letter in secret_word:
        to_print = letter if letter in guessed_letters else '_'
        print(to_print, end=' ')
    print("\n")


def play_again() -> bool:
    """
    Prompts the user if they would like to play again. If the user enters something other than Y/y/N/n, it will continue
    prompting until the use enters a valid value. If the user indicates Y or y, this method returns True; N or n will
    return False

    :return: True if the user would like to start a new game; False otherwise
    """
    choice = ''
    while choice != "Y" and choice != "N":
        choice = input("Play again? (Y/N)").strip().upper()
    return choice == "Y"

run_game()

 


我所知道的是一个问题:


没有单元测试。我仍在尝试找出如何测试input()print()语句的方法
我尝试对所有方法进行pydoc docstring注释,但无法弄清楚如何正确记录返回的元组。
由于我的Java(双引号)和Angular / Typescript(单引号)双重背景,我的字符串引号用法有点不一致。
check_guess_against_secret方法无法准确计算出多少个唯一字符剩下的字母;秘密中重复的未猜测字母将被计数两次。但是,由于我们仅评估该值是否为0,因此可以将其交换为重构时的布尔值标志。

除了进行一般性的回顾之外,如果人们可以指出哪些地方可以我正在使用Java(或Typescript!)方式而不是Python方式进行操作。

评论

在Python中和在Java中一样,使用大写或小写进行不区分大小写的字符串比较是一种不好的做法,尽管公认的是Python使得正确地进行不必要的困难。您要使用str.casefold()。

为了帮助解决格式问题,例如单引号或双引号的一致性,我喜欢使用黑色(Python中也存在其他类似的工具)。与例如precommit结合使用,这将在您每次提交时自动处理很多小格式的事情。

@Voo感谢您的建议,我会调查一下。我打算研究与Java的equalsIgnoreCase()等效的方法,这听起来像是朝着正确方向迈出的一步

#1 楼

优点


使用Python时,您的代码遵循许多建议的样式。
键入Python。这是相当新的,但是我发现它非常有用。鉴于它支持渐进式输入,它还使我狡猾的元编程也能正常工作。
文档字符串,这些看起来不错。您似乎已选择使用Sphinx格式。如果您尚未选择文档生成器,则Sphinx看起来很适合您。
您似乎了解单行和多行docstring样式。

代码审查



与单例相比,应使用is。因为不能保证相等运算符执行您希望执行的检查。

>>> False == 0
True



执行真实和虚假的检查更加Pythonic。 />
# if foo == True:
if foo:
    ...



我不确定您是Java还是JavaScript开发人员,但是无论哪种情况,您都应该了解递归的局限性。递归为每个调用创建一个堆栈框架。如果您本身调用函数,则在制作第二帧时,第一帧仍然存在。一旦第二个退出,则第一个退出。

这意味着run_game只能运行有限的次数。用递归实现主循环是非常不明智的。


run_game应该分为两个功能,一个负责主循环,一个负责hang子手的主循环。

我更喜欢一个类来封装状态。我对Java不太了解,但是我听说Java非常喜欢OOP,也许专门致力于OOP会更好地描述这种关系。但是Python是不同的。如果可以更好地将某类描述为一个类,请使用一个类。如果最好使用函数,请使用函数。

再一次,我在网上阅读了Python屠宰的OOP,不可能在Python中遵循OOP。充其量是对科学的误解,也许您已经习惯了Java功能?

您选择的随机函数random.randint容易导致对IndexError的以下索引。您应该使用不包含最终值的random.randrange
使用random.choice而不是random.randintrandom.randrange更好。
使用'"都没关系,但坚持一个。仅当字符串包含您首选的分隔符时才使用other,仅作为一种语法糖。
您可以使用sortedguessed_letters进行排序。 []。只需使用str.join即可。
您可以使用集简化您的越野车check_guess_against_secret检查。鉴于您总是显示排序后的猜测列表,因此将guessed_letters作为列表并没有多大意义。
在if语句周围使用方括号是不Python的。除非您不需要它们。
如果输入不在0到6之间,请不要在draw_hangman中默默地失败,那么您应该修复损坏的代码,而不是猴子修补问题并祈求它永远不会出现。
默认情况下调用print会刷新流,因此非常昂贵。对此的简单解决方案是print(..., flush=False)。但是,为什么不只构建一个字符串并打印一次呢?您可以使用各种理解来使代码看起来也不错。
我希望在while True中使用play_again循环,而不是do-while循环。您可以返回干净地退出该函数。
您应该使用if __name__ == '__main__':防护程序来保护您的代码以免意外运行main。对我来说似乎很可怜。


您同时使用drawprint表示同一件事。它们是具有不同含义的不同词。
您有play_againprompt_for_guess。这两个功能都提示用户输入,但是play_agin不能告诉我们。
您有一个功能play_again听起来并不像一个功能。听起来应该只是一个普通的旧布尔变量。

对我来说,您的名字似乎不必要地冗长或晦涩难懂。 secret_word优于wordsecret

guessed_letters超过guesses

run_game有什么好处?在main上。


此外,我想不出_check_guess_against_secret的简称,因为它在做两件事。

总的来说,我认为您的代码一目了然。您已经有了文档,静态输入,并且已使代码符合PEP 8的要求。但是,当您真正深入研究它时,我认为您的代码并不出色。

import random
import sys
from typing import Tuple

# Omitted declarations: https://gist.github.com/chrishorton/8510732aa9a80a03c829b09f12e20d9c
# HANGMAN_STAGES = [...]
# WORDS = ...


class Hangman:
    _secret_word: str
    _guessed_letters: set
    _incorrect_guesses: int
    _round: int
    _won: bool

    def __init__(self, secret_word: str) -> None:
        self._secret_word = secret_word
        self._guessed_letters = set()
        self._incorrect_guesses = 0
        self._round = 1
        self._won = False

    def _turn(self) -> None:
        """
        Processes a user's turn. First draws the current state of the game: current hangman, partially-guessed word, and
        list of previously guessed letters. Then prompts the user for their next guess, evaluates that guess to see if it
        was correct, and then updates the game state.

        :param guessed_letters: the list of previously guessed letters
        """
        draw_hangman(incorrect_guess_count)
        self._draw_secret_word(secret_word, guessed_letters)
        self._print_guessed_letters()
        next_letter = self._prompt_for_guess()
        return self._apply_guess(next_letter)

    def _print_guessed_letters(self) -> None:
        """Print the guessed letters to the screen."""
        print('Guesses: ' + ', '.join(sorted(self.guessed_letters)))

    def _apply_guess(self, next_letter: str) -> None:
        """
        Checks the validity of the user's guess. If the guess was incorrect, increments the number of incorrect guesses by
        1. If the user has guessed all of the letters in the secret word, return an indication that the user has won the
        game.

        :param next_letter: the user's guess
        """
        self._guessed_letters.add(next_letter)
        correct, letters_remaining = self._check_guess_against_secret(next_letter, secret_word, guessed_letters)

        if not correct:
            self._incorrect_guess_count += 1

        if letters_remaining == 0:
            self._won = True

    def _check_guess_against_secret(self, next_letter: str) -> Tuple[bool, int]:
        """
        Determines if the user has guessed correctly. Also evaluates the secret word to determine if there are more letters
        left for the user to guess.

        :param next_letter: the user's guessed letter
        :return: (True/False indicating if the guess was correct, 0 if no letters left and positive integer otherwise)
        """
        return (
            next_letter in secret_word,
            len(set(self._secret_word) - self._guessed_letters)
        )

    def _prompt_for_guess(self) -> str:
        """
        Prompts the user for their next guess. Rejects guesses that are more than a single letter, and guesses which were
        already made previously. Returns the (validated) guess.

        :return: the user's next guess
        """
        while True:
            guess = input('Your guess? ').strip().upper()
            if len(guess) > 1:
                print('Sorry, you can only guess one letter at a time.')
                continue
            elif guess in guessed_letters:
                print('Sorry, you already guessed that letter.')
                continue
            return guess

    def _draw_secret_word(self) -> None:
        """
        Prints the secret word, with underscores representing unknown letters and with any correctly-guessed leters printed
        in the appropriate location within the word.

        :param secret_word: The secret word
        :param guessed_letters: All previous guesses
        :return: Nothing
        """
        print(
            ' '.join(
                letter if letter in self._guessed_letters else '_'
                for letter is self._secret_word
            )
            + '\n'
        )

    def run(self) -> None:
        while not self._won and self._incorrect_guesses < 6:
            print('\n\nROUND ' + str(round))
            self._turn()
            self._round += 1

        print('\n\n')

        if self._won:
            print('Congratulations! You won!', end=' ')
        else:
            print('GAME OVER! You lost.')
            draw_hangman(6)


def main() -> None:
    """The main game loop. Will prompt the user if they would like to start a new game at the end."""
    print('WELCOME TO HANGMAN.')
    while True:
        Hangman(pick_secret_word()).run()
        if not play_again():
            return


def pick_secret_word() -> str:
    """
    Chooses a new secret word from the list of available secret words. The word is chosen psuedo-randomly.
    :return: the new secret word
    """
    return random.choice(WORDS).upper()


def draw_hangman(number_of_incorrect_guesses: int) -> None:
    """
    Draws the appropriate hangman stage, given the number of incorrect guesses. 0 or fewer will draw the empty scaffold.
    6 or more will draw the fully hanged man.

    :param number_of_incorrect_guesses: the number of incorrect guesses the player has made in the current game
    :return: Nothing
    """
    if (number_of_guesses < 0
        or 6 < number_of_guesses
    ):
        raise ValueError('Hangman can only support upto 6 incorrect guesses.')
    print(HANGMAN_STAGES[number_of_guesses])


def play_again() -> bool:
    """
    Prompts the user if they would like to play again. If the user enters something other than Y/y/N/n, it will continue
    prompting until the use enters a valid value. If the user indicates Y or y, this method returns True; N or n will
    return False

    :return: True if the user would like to start a new game; False otherwise
    """
    while True:
        choice = input('Play again? (Y/N)').strip().upper()
        if choice in 'YN':
            return choice


if __name__ == '__main__':
    main()


评论


\ $ \ begingroup \ $
您能否将我链接到一些详细说明为什么人们应该更喜欢random.choice而不是randint和randrange的东西?我已经尝试使用Google搜索,但没有得到任何确定的信息。当我用Google搜索如何在编写原始整数时获取随机整数时,我看过的两篇文章(一篇在StackOverflow上,另一篇在其他地方)都推荐randint,这就是我使用它的原因。
\ $ \ endgroup \ $
–冷冻豌豆的传承
20 Jan 6 '20 at 3:59

\ $ \ begingroup \ $
关于使用类和集合……实际上,我正在使用一本书来自学Python概念,但还没有涉及到这一部分。我一定会继续阅读,并考虑到您的建议进行重构。 :)
\ $ \ endgroup \ $
–冷冻豌豆的传承
20年1月6日在4:11

\ $ \ begingroup \ $
@RoddyoftheFrozenPeas关于为什么random.choice()优于替代方案:主要是因为简单。使用rand.randint()然后选择索引是两个步骤,根据情况/同步性可能会导致IndexError。 random.choice()是第一步,非常简单(从列表中选择一个元素),除非列表为空(再也​​只有一步),否则没有IndexError的机会。 “从列表中获取一个随机元素”比“找到一个小于列表大小的随机索引,并在该索引处获取元素”是一个更简洁的表达。
\ $ \ endgroup \ $
–绿色披风的家伙
20 Jan 6 '20在6:28



\ $ \ begingroup \ $
@RoddyoftheFrozenPeas:滚动自己的采样函数时也很容易引入偏差;通常,我会比我自己更信任标准库的实现。 (当然,众所周知,PRNG和采样的标准库实现是错误的,有时甚至是错误的。)
\ $ \ endgroup \ $
–Jörg W Mittag
20 Jan 6 '20 at 10:48

#2 楼

可以使用input处理try来处理exceptions。例如:

try:
    user_in = int(input('Enter number: ')
    ...
except ValueError:
    print('Integers only!')


使用报价确实是您的选择,但最好保持一致。如果在整个代码中都使用'",请使其一者一一。使用三重引号之类的好处可以简化数据放置和计算。例如:

print('''
data_1 > {}
data_2 > {}
data_3 > {}
'''.format(data_1, sum(data_1), len(data_1))


关于密码和剩下的内容。 Python虽然很复杂,但在处理时间上却很繁琐,因此对理解很有用。

不久前,我看到了一篇类似的文章,它使用类将密切相关的功能保持在一起,这是理想的类。我看不出将它们用于简单游戏的意义,这可能类似于以下内容:

import random

words = ['cheese', 'biscuits', 'hammer', 'evidence'] 
word_choice = [i.lower() for i in random.choice(words)] # List comprehension
lives = 3
cor_guess = []
while True:
  print('Letters found: {}\nWord length: {}'.format(cor_guess, len(word_choice)))
  if lives == 0:
      print('Your dead homie!\nThe word was: ', word_choice)
      break
  guess = str(input('Guess a letter: '))
  if guess in word_choice:
      print(guess, 'is in the word!\n')
      cor_guess.append(guess)
  else:
      print('Your dying slowly!\n')
      lives -= 1
  ...
  ...


使用列表理解我已经从中选择了一个对象如果我需要使用大写字母等在文件中打开文件,然后获取该对象的每个对象,则将变量全部转换为小写字母以简化操作。这会在列表中填充一个单词,并以不同的字母分隔。然后可以将数据与用户输入进行比较。这样,数据开始就非常灵活,可以轻松地进行操作,并且遵循代码也容易得多。

评论


\ $ \ begingroup \ $
在您的try-catch示例中,如果输入不是整数,则会引发异常。但是,我所有的输入都是字符串,因此,除非我自己开始提出异常,否则我真的不了解如何以这种方式使用try-catch。 Python真的建议以这种方式对控制流使用异常处理吗?我一直都认为这是一种反模式和不良做法。
\ $ \ endgroup \ $
–冷冻豌豆的传承
20年1月6日在4:10

\ $ \ begingroup \ $
docs.python.org/3/tutorial/errors.html这是有关try和except子句的一些文档。在上面的代码中,我基本上强迫用户仅输入整数,并在检查条件是否满足的情况下停留在循环中。通常,try / exceptions用于特定的条件错误。它只是节省了创建条件类型函数的时间。例如,无论您在str(input(''))中输入什么内容,都将被视为字符串。然后,您必须检查是否输入.isdigit()或对其进行转换,并进行相应的处理。阅读一些文档非常有帮助
\ $ \ endgroup \ $
–倒钩
20年1月6日14:56



#3 楼

呈现代码时,应考虑代码的宽度。注释“处理用户的回合。首先绘制游戏的当前状态:当前的hang子手,部分被猜测的单词,并且”长116个字符。代码行“ def process_turn(incorrect_guess_count:int,secret_word:str,guessed_letters:list)-> Tuple [int,bool]:”长106个字符。您可以将函数声明分为几行:

def process_turn(
                     incorrect_guess_count: int, 
                     secret_word: str, 
                     guessed_letters: list
                 ) -> Tuple[int, bool]:


虽然Python允许您让函数调用稍后定义的另一个函数,但如果函数调用函数,则更容易阅读

应使用==将布尔变量与常量进行比较。 A == True只是返回AA == False返回not A。您的pick_secret_word()函数可以用单行'secret_word = random.choice(WORDS)代替。也许是个人喜好,但我发现使用函数过多。您花费大量时间来回传递参数,并记录每个参数的含义。

如果在回合中使用for循环而不是while循环,则没有增加round。另外,round是Python中的内置函数,因此您应该使用其他名称,例如turn。我正在使用Spyder,它对内置代码和关键字进行颜色编码。如果您不使用这样做的IDE,则可以考虑这样做。

6是一个“魔术数字”。您可以将其作为带有默认值的参数来放置。

如果用户没有对是否要再次玩游戏做出有效回应,应该向用户提供更多有关错误之处的反馈。您可以将Y以外的任何东西都当作“否”。

WORDSHANGMAN_STAGES在任何地方都没有定义。

def run_game(WORDS, HANGMAN_STAGES, max_turns = 26, max_guesses = 6) -> None:
    while True:
        print("WELCOME TO HANGMAN.")
        secret_word = random.choice(WORDS)
        guessed_letters = []
        incorrect_guesses = 0
        max_turns = 26
        max_guesses = 6
        letters_remaining = len(secret_word)
        for turn in max_turns:
            print('\n\nROUND ' + str(turn))    
            print(HANGMAN_STAGES[number_of_guesses])
            print(''.join([letter for letter in secret_word
                            if letter in guessed_letters 
                            else '_'])+'/n')
            guessed_letters.sort()
            print("Guesses: " + str(guessed_letters))
            while True:
                guess = input("Your guess? ").strip().upper()
                if len(guess) > 1:
                    print("Sorry, you can only guess one letter at a time.")
                    continue
                elif guess in guessed_letters:
                    print("Sorry, you already guessed that letter.")
                    continue
                break
            guessed_letters.append(guess)
            if guess in secret_word:
                if letters_remaining == 0:
                    print("\n\n")
                    print("Congratulations! You won!", end=" ")
                    break
                else:
                    letter_remaining -= 
                        sum([letter == guess for letter in secret_word])
            else:
                incorrect_guess +=1
                if incorrect_guesses == max_guesses:
                    print("\n\n")
                    print("GAME OVER! You lost.")
                    print(HANGMAN_STAGES[6]
                    break
        print("The secret word was: " + secret_word)
        choice = input("Press Y to play again").strip().upper()
        if choice != 'Y':
            break


评论


\ $ \ begingroup \ $
我使用120个字符作为行长限制,因为这是我惯用的,也是我的IDE设置的默认值。我对PEP-8的理解是,建议不要限制80个字符的限制。
\ $ \ endgroup \ $
–冷冻豌豆的传承
20年1月6日,下午3:41

\ $ \ begingroup \ $
re:WORDS和HANGMAN_STAGES-我在最初的问题中提到,它们是从GitHub上的公共Gist抄写而来的,我故意省略了它们,因为除了重命名变量之外,我没有做任何更改,并且感觉不到复制粘贴60行其他人的代码(不确定的许可)很有用。帖子中有一个到Gist的链接。
\ $ \ endgroup \ $
–冷冻豌豆的传承
20 Jan 6'4在4:13