hav-cs50-merge-05

哈佛 CS50 中文官方笔记(六)

第三讲

原文:cs50.harvard.edu/python/notes/3/

  • 异常

  • 运行时错误

  • try

  • else

  • 创建一个获取整数的函数

  • pass

  • 总结

异常

  • 异常是我们编码中出现的错误。

  • 异常是在程序运行时出现的错误。

  • 在我们的文本编辑器中,输入 code hello.py 以创建一个新文件。按照以下方式输入(包括故意包含的错误):

    print("hello, world) 
    

    注意,我们故意省略了一个引号。

  • 在终端中运行 python hello.py 会产生错误。解释器报告了一个语法错误。语法错误通常意味着你应该仔细检查你输入的代码是否正确。

  • 你可以在 Python 的错误和异常文档中了解更多信息。

运行时错误

  • 运行时错误是指代码中意外行为产生的错误。例如,你可能希望用户输入一个数字,但他们输入了一个字符。由于用户的不当输入,你的程序可能会抛出错误。

  • 在你的终端窗口中,运行 code number.py。在你的文本编辑器中按照以下方式编写代码:

    x = int(input("What's x? "))
    print(f"x is {x}") 
    

    注意,通过包含 f,我们告诉 Python 将花括号中的内容作为 x 的值进行插值。此外,测试你的代码时,你可以想象如果有人输入一个字符串或字符而不是数字会发生什么。即使如此,用户也可能什么也不输入——只是简单地按回车键。

  • 作为程序员,我们应该采取防御性措施,以确保用户输入的是我们期望的内容。

  • 如果我们运行这个程序并输入“cat”,我们会看到 ValueError: invalid literal for int() with base 10: 'cat'。换句话说,int 函数无法将文本“cat”转换为数字。

  • 修复这种潜在错误的有效策略是创建“错误处理”来确保用户的行为符合我们的预期。

  • 你可以在 Python 的错误和异常文档中了解更多信息。

try

  • 在 Python 中,tryexcept 是在出现错误之前测试用户输入的方法。按照以下方式修改你的代码:

    try:
        x = int(input("What's x?"))
        print(f"x is {x}")
    except ValueError:
        print("x is not an integer") 
    

    注意,运行这段代码时,输入 50 将被接受。然而,输入 cat 将产生一个用户可见的错误,并指导他们为什么他们的输入不被接受。

  • 这仍然不是实现此代码的最佳方式。注意,我们试图执行两行代码。为了最佳实践,我们应该只尝试尽可能少的可能失败的代码行。按照以下方式调整你的代码:

    try:
        x = int(input("What's x?"))
    except ValueError:
        print("x is not an integer")
    
    print(f"x is {x}") 
    

    注意,虽然这实现了尽可能少尝试的目标,但我们现在面临一个新的错误!我们遇到了一个 NameError,其中 x 未定义。看看这段代码,考虑一下:为什么在某些情况下 x 未定义?

  • 的确,如果你检查x = int(input("What's x?"))中的操作顺序,从右到左,它可能会尝试将输入错误的字符赋值为整数。如果这失败了,x的赋值永远不会发生。因此,在代码的最后一行没有x可以打印。

else

  • 结果表明,还有另一种实现try的方法,可以捕获这种类型的错误。

  • 按照以下方式调整你的代码:

    try:
        x = int(input("What's x?"))
    except ValueError:
        print("x is not an integer")
    else:
        print(f"x is {x}") 
    

    注意,如果没有发生异常,它将运行else块中的代码。运行python number.py并输入50,你会注意到结果将被打印出来。再次尝试,这次输入cat,你会注意到程序现在捕获了错误。

  • 考虑改进我们的代码,注意我们对待用户有点无礼。如果用户不合作,我们目前只是简单地结束程序。考虑一下我们如何使用循环来提示用户输入x,如果他们不再次提示的话!

    while True:
        try:
            x = int(input("What's x?"))
        except ValueError:
            print("x is not an integer")
        else:
            break
    
    print(f"x is {x}") 
    

    注意,while True将无限循环。如果用户成功提供了正确的输入,我们可以跳出循环并打印输出。现在,一个输入错误的用户将被要求再次输入。

创建一个获取整数的函数

  • 当然,有很多时候我们希望从用户那里获取一个整数。按照以下方式修改你的代码:

    def main():
        x = get_int()
        print(f"x is {x}")
    
    def get_int():
        while True:
            try:
                x = int(input("What's x?"))
            except ValueError:
                print("x is not an integer")
            else:
                break
        return x
    
    main() 
    

    注意,我们正在展示许多优秀的特性。首先,我们抽象出了获取整数的能力。现在,整个程序归结为程序的前三条语句。

  • 即使如此,我们仍然可以改进这个程序。考虑一下你还能做什么来改进这个程序。按照以下方式修改你的代码:

    def main():
        x = get_int()
        print(f"x is {x}")
    
    def get_int():
        while True:
            try:
                x = int(input("What's x?"))
            except ValueError:
                print("x is not an integer")
            else:
                return x
    
    main() 
    

    注意,return不仅会跳出循环,还会返回一个值。

  • 有些人可能会争论你可以这样做:

    def main():
        x = get_int()
        print(f"x is {x}")
    
    def get_int():
        while True:
            try:
                return int(input("What's x?"))
            except ValueError:
                print("x is not an integer")
    
    main() 
    

    注意,这与我们代码的前一个版本做的是同样的事情,只是行数更少。

pass

  • 我们可以修改代码,使得我们的代码不会警告用户,而是简单地通过修改代码来再次询问他们的提示问题:

    def main():
        x = get_int()
        print(f"x is {x}")
    
    def get_int():
        while True:
            try:
                return int(input("What's x?"))
            except ValueError:
                pass
    
    main() 
    

    注意,我们的代码仍然可以工作,但不会反复通知用户他们的错误。在某些情况下,你可能希望非常清楚地告诉用户产生了什么错误。在其他时候,你可能会决定你只是想再次要求他们输入。

  • get_int函数的实现进行最后一次改进。目前,注意我们依赖于xmainget_int函数中都是通过荣誉系统来传递的。我们可能想传递一个当用户被要求输入时看到的提示。按照以下方式修改你的代码。

    def main():
        x = get_int("What's x? ")
        print(f"x is {x}")
    
    def get_int(prompt):
        while True:
            try:
                return int(input(prompt))
            except ValueError:
                pass
    
    main() 
    
  • 你可以在 Python 的文档中了解更多关于pass的信息。

总结

代码中不可避免地会出现错误。然而,你有机会利用今天所学的内容来帮助预防这些错误。在本节课中,你学习了关于……

  • 异常

  • 值错误

  • 运行时错误

  • try

  • else

  • pass

第四讲

原文:cs50.harvard.edu/python/notes/4/

  • 随机

  • 统计学

  • 命令行参数

  • slice 函数

  • API

  • 创建自己的库

  • 总结

  • 通常,库是你或其他人编写的代码片段,你可以在程序中使用它。

  • Python 允许你将函数或功能作为“模块”与他人共享。

  • 如果你从旧项目中复制粘贴代码,那么你很可能可以创建一个模块或库,并将其带入新项目。

随机

  • random 是 Python 内置的一个库,你可以将其导入到自己的项目中。

  • 作为程序员,站在前人的肩膀上更容易。

  • 那么,如何将模块加载到自己的程序中呢?你可以在程序中使用 import 关键字。

  • random 模块内部,有一个名为 random.choice(seq) 的内置函数。random 是你导入的模块。在该模块内部,有一个名为 choice 的函数。该函数接受一个 seq 或序列,它是一个列表。

  • 在你的终端窗口中输入 code generate.py。在你的文本编辑器中,按照以下方式编写代码:

    import random
    
    coin = random.choice(["heads", "tails"])
    print(coin) 
    

    注意,choice 函数内的列表有方括号、引号和逗号。由于你传入了两个项目,Python 会进行数学计算,并给出正面和反面的 50% 概率。运行你的代码,你会注意到这段代码确实运行得很好!

  • 我们可以改进我们的代码。from 允许我们非常具体地指定我们想要导入的内容。之前,我们的 import 代码行是导入 random 中所有函数的内容。然而,如果我们只想加载模块的一部分,应该如何修改代码呢?

    from random import choice
    
    coin = choice(["heads", "tails"])
    print(coin) 
    

    注意,我们现在可以只导入 randomchoice 函数。从那时起,我们不再需要编写 random.choice。现在我们只需编写 choice 即可。choice 已经明确加载到我们的程序中。这节省了系统资源,并且可能使我们的代码运行得更快!

  • 接下来,考虑 random.randint(a, b) 函数。此函数将在 ab 之间生成一个随机数。按照以下方式修改你的代码:

    import random
    
    number = random.randint(1, 10)
    print(number) 
    

    注意,我们的代码将随机生成一个介于 110 之间的数字。

  • 我们可以介绍 random.shuffle(x) 函数,该函数可以将列表打乱成随机顺序。

    import random
    
    cards = ["jack", "queen", "king"]
    random.shuffle(cards)
    for card in cards:
        print(card) 
    

    注意,random.shuffle 将就地打乱牌的顺序。与其他函数不同,它不会返回一个值。相反,它将 cards 列表作为参数,并在该列表内部打乱牌的顺序。运行你的代码几次,以查看代码的功能。

  • 我们现在有上述三种生成随机信息的方法。

  • 你可以在 Python 的 random 库文档中了解更多信息。随机

统计学

  • Python 内置了一个 statistics 库。我们如何使用这个模块呢?

  • mean 是这个库中的一个非常有用的函数。在你的终端窗口中,输入 code average.py。在文本编辑器窗口中,按照以下方式修改你的代码:

    import statistics
    
    print(statistics.mean([100, 90])) 
    

    注意,我们导入了一个名为 statistics 的不同库。mean 函数接受一个值列表。这将打印这些值的平均值。在你的终端窗口中,输入 python average.py

  • 考虑在你的程序中使用 statistics 模块的可能性。

  • 你可以在 Python 的 statistics统计)文档中了解更多信息。

命令行参数

  • 到目前为止,我们一直在程序中提供所有值。如果我们想能够从命令行获取输入怎么办?例如,而不是在终端中输入 python average.py,我们能否输入 python average.py 100 90 并得到 10090 之间的平均值?

  • sys 是一个模块,它允许我们在命令行中获取参数。

  • argvsys 模块中的一个列表,它记录了用户在命令行中输入的内容。

  • 注意,你将在下面的代码中看到 sys.argv 的使用。在终端窗口中,输入 code name.py。在文本编辑器中,按照以下方式编写代码:

    import sys
    
    print("hello, my name is", sys.argv[1]) 
    

    注意,程序将查看用户在命令行中输入的内容。目前,如果你在终端窗口中输入 python name.py David,你会看到 hello, my name is David。注意,sys.argv[1] 是存储 David 的位置。为什么是这样呢?好吧,在之前的课程中,你可能记得列表是从 0 个元素开始的。你认为当前 sys.argv[0] 中存储了什么?如果你猜到是 name.py,你就对了!

  • 我们现有的程序有一个小问题。如果用户没有在命令行中输入名字会怎样?自己试一试。在终端窗口中输入 python name.py。解释器会显示一个错误 list index out of range。原因是在 sys.argv[1] 中没有内容,因为没有输入任何东西!以下是我们可以保护我们的程序免受此类错误的方法:

    import sys
    
    try:
        print("hello, my name is", sys.argv[1])
    except IndexError:
        print("Too few arguments") 
    

    注意,如果用户忘记输入名字,程序会提示一个有用的提示,告诉他们如何使程序工作。然而,我们能否更加谨慎以确保用户输入正确的值?

  • 我们可以按照以下方式改进我们的程序:

    import sys
    
    if len(sys.argv) < 2:
        print("Too few arguments")
    elif len(sys.argv) > 2:
        print("Too many arguments")
    else:
        print("hello, my name is", sys.argv[1]) 
    

    注意,如果你测试你的代码,你会看到这些异常是如何被处理的,为用户提供更详细的建议。即使用户输入了过多的或过少的参数,用户也会得到关于如何修复问题的明确指示。

  • 目前,我们的代码在逻辑上是正确的。然而,将错误检查与代码的其余部分分开是非常好的。我们如何分离出错误处理?按照以下方式修改你的代码:

    import sys
    
    if len(sys.argv) < 2:
        sys.exit("Too few arguments")
    elif len(sys.argv) > 2:
        sys.exit("Too many arguments")
    
    print("hello, my name is", sys.argv[1]) 
    

    注意我们如何使用 sys 的内置函数 exit,它允许我们在用户引入错误时退出程序。现在我们可以确信程序将永远不会执行最后一行代码并触发错误。因此,sys.argv 提供了一种方式,用户可以通过命令行引入信息。sys.exit 提供了一种方式,程序可以在出现错误时退出。

  • 你可以在 Python 的 sys 库文档中了解更多信息sys

slice

  • slice 是一个命令,它允许我们取一个 list 并告诉解释器我们希望解释器将 list 的哪个位置视为开始和结束。例如,按照以下方式修改你的代码:

    import sys
    
    if len(sys.argv) < 2:
        sys.exit("Too few arguments")
    
    for arg in sys.argv:
        print("hello, my name is", arg) 
    

    注意,如果你在终端窗口中输入 python name.py David Carter Rongxin,解释器将输出不仅仅是预期的名字输出,还会输出 hello, my name is name.py。那么我们如何确保解释器忽略列表中当前存储的 name.py 的第一个元素呢?

  • slice 可以在我们的代码中用来从不同的位置开始列表!按照以下方式修改你的代码:

    import sys
    
    if len(sys.argv) < 2:
        sys.exit("Too few arguments")
    
    for arg in sys.argv[1:]:
        print("hello, my name is", arg) 
    

    注意,我们不是从 0 开始列表,而是使用方括号告诉解释器从 1 开始,使用 1: 参数到末尾。运行这段代码,你会注意到我们可以使用相对简单的语法来改进我们的代码。

  • Python 如此受欢迎的一个原因是,有大量的强大第三方库增加了功能。我们将这些作为文件夹实现的第三方库称为“包”。

  • PyPI 是一个包含所有当前可用的第三方包的仓库或目录。

  • cowsay 是一个允许牛与用户交谈的知名包。

  • Python 有一个名为 pip 的包管理器,它允许你快速将包安装到你的系统中。

  • 在终端窗口中,你可以通过输入 pip install cowsay 来安装 cowsay 包。在输出一些信息后,你现在可以在代码中使用这个包了。

  • 在你的终端窗口中输入 code say.py。在文本编辑器中,按照以下方式编写代码:

    import cowsay
    import sys
    
    if len(sys.argv) == 2:
        cowsay.cow("hello, " + sys.argv[1]) 
    

    注意,程序首先检查用户是否在命令行中至少输入了两个参数。然后,牛应该对用户说话。输入 python say.py David,你会看到一头牛对 David 说“hello”。

  • 进一步修改你的代码:

    import cowsay
    import sys
    
    if len(sys.argv) == 2:
        cowsay.trex("hello, " + sys.argv[1]) 
    

    注意,现在一个 t-rex 正在说“hello”。

  • 你现在可以看到如何安装第三方包。

  • 你可以在 PyPI 的 cowsay 条目中了解更多信息。

  • 你可以在 PyPI 找到其他第三方包。

API

  • API 或“应用程序程序接口”允许你连接到他人的代码。

  • requests 是一个允许你的程序表现得像网络浏览器的包。

  • 在你的终端中输入 pip install requests。然后,输入 code itunes.py

  • 结果表明,Apple iTunes 有自己的 API,你可以在程序中访问它。在你的网络浏览器中,你可以访问 itunes.apple.com/search?entity=song&limit=1&term=weezer 并下载一个文本文件。大卫通过阅读 Apple 的 API 文档构建了这个 URL。注意这个查询正在寻找一个与 term 称为 weezer 相关的 song,结果数量限制为 1。查看下载的文本文件,你可能会发现其格式与我们之前在 Python 中编写的类似。

  • 下载的文本文件格式称为 JSON,这是一种基于文本的格式,用于在应用程序之间交换基于文本的数据。实际上,Apple 正在提供我们可以用我们自己的 Python 程序解释的 JSON 文件。

  • 在终端窗口中,输入 code itunes.py。编写如下代码:

    import requests
    import sys
    
    if len(sys.argv) != 2:
        sys.exit()
    
    response = requests.get("https://itunes.apple.com/search?entity=song&limit=1&term=" + sys.argv[1])
    print(response.json()) 
    

    注意 requests.get 返回的值将被存储在 response 中。大卫阅读了关于此 API 的 Apple 文档后知道返回的是 JSON 文件。运行 python itunes.py weezer,你会看到 Apple 返回的 JSON 文件。然而,JSON 响应被 Python 转换成了字典。查看输出,可能会让人头晕。

  • 结果表明,Python 有一个内置的 JSON 库可以帮助我们解释接收到的数据。修改你的代码如下:

    import json
    import requests
    import sys
    
    if len(sys.argv) != 2:
        sys.exit()
    
    response = requests.get("https://itunes.apple.com/search?entity=song&limit=1&term=" + sys.argv[1])
    print(json.dumps(response.json(), indent=2)) 
    

    注意 json.dumps 是如何实现的,它利用 indent 使输出更易读。运行 python itunes.py weezer,你会看到相同的 JSON 文件。然而,这次它要容易阅读得多。现在注意,你会在输出中看到一个名为 results 的字典。在这个名为 results 的字典中,有多个键存在。查看输出中的 trackName 值。你在结果中看到了什么曲目名称?

  • 我们如何简单地输出那个特定曲目的名称?修改你的代码如下:

    import json
    import requests
    import sys
    
    if len(sys.argv) != 2:
        sys.exit()
    
    response = requests.get("https://itunes.apple.com/search?entity=song&limit=50&term=" + sys.argv[1])
    
    o = response.json()
    for result in o["results"]:
        print(result["trackName"]) 
    

    注意我们是如何将 response.json() 的结果存储在 o 中(就像小写字母 o)。然后,我们遍历 o 中的 results 并打印每个 trackName。同时注意我们如何将结果数量限制增加到 50。运行你的程序。查看结果。

  • 你可以通过 库的文档 了解更多关于 requests 的信息。

  • 你可以在 Python 的 JSON 文档中了解更多关于 JSON 的信息。

创建自己的库

  • 你作为一个 Python 程序员,有能力创建自己的库!

  • 想象一下你可能想要反复使用代码片段,甚至与他人分享的情况!

  • 在本课程中,我们编写了很多代码来表示“hello”。让我们创建一个包,以便我们可以表示“hello”和“goodbye”。在你的终端窗口中,输入 code sayings.py。在文本编辑器中,编写如下代码:

    def hello(name):
        print(f"hello, {name}")
    
    def goodbye(name):
        print(f"goodbye, {name}") 
    

    注意,这段代码本身对用户没有任何作用。然而,如果程序员将这个包导入到他们自己的程序中,上述函数创建的能力就可以在他们的代码中实现。

  • 让我们看看我们如何实现利用我们创建的这个包的代码。在终端窗口中,输入code say.py。在你文本编辑器中的这个新文件中,输入以下内容:

    import sys
    
    from sayings import goodbye
    
    if len(sys.argv) == 2:
        goodbye(sys.argv[1]) 
    

    注意,这段代码导入了sayings包中goodbye的能力。如果用户在命令行中至少输入了两个参数,它将输出goodbye以及命令行中输入的字符串。

总结

库扩展了 Python 的能力。一些库默认包含在 Python 中,只需导入即可。其他的是需要使用pip安装的第三方包。你可以为自己或他人创建自己的包!在本讲座中,你学习了关于……

  • 随机

  • 统计学

  • 命令行参数

  • 切片

  • API

  • 创建你自己的库

第五讲

原文:cs50.harvard.edu/python/notes/5/

  • 单元测试

  • assert

  • pytest

  • 测试字符串

  • 将测试组织到文件夹中

  • 总结

单元测试

  • 到目前为止,你很可能一直在使用 print 语句测试自己的代码。

  • 或者,你可能一直依赖 CS50 来为你测试代码!

  • 在工业界,编写代码来测试自己的程序是最常见的。

  • 在你的控制台窗口中,输入 code calculator.py。注意,你可能在前面的讲座中已经编写了这个文件。在文本编辑器中,确保你的代码如下所示:

    def main():
        x = int(input("What's x? "))
        print("x squared is", square(x))
    
    def square(n):
        return n * n
    
    if __name__ == "__main__":
        main() 
    

    注意,你可以使用一些明显的数字,例如 2,在你的机器上合理地测试上述代码。然而,考虑一下你为什么想要创建一个确保上述代码适当运行的测试。

  • 按照惯例,让我们通过输入 code test_calculator.py 创建一个新的测试程序,并在文本编辑器中修改你的代码如下:

    from calculator import square
    
    def main():
        test_square()
    
    def test_square():
        if square(2) != 4:
            print("2 squared was not 4")
        if square(3) != 9:
            print("3 squared was not 9")
    
    if __name__ == "__main__":
        main() 
    

    注意,我们在代码的第一行导入了 square 函数,来自 calculator.py

  • 在控制台窗口中,输入 python test_calculator.py。你会注意到没有任何输出。这可能意味着一切运行正常!或者,这也可能意味着我们的测试函数没有发现可能导致错误的“边缘情况”之一。

  • 目前,我们的代码测试了两个条件。如果我们想要测试更多的条件,我们的测试代码可能会很容易变得臃肿。我们如何在不扩展测试代码的情况下扩展我们的测试能力?

assert

  • Python 的 assert 命令允许我们告诉解释器某个断言是真的。我们可以将此应用于我们的测试代码,如下所示:

    from calculator import square
    
    def main():
        test_square()
    
    def test_square():
        assert square(2) == 4
        assert square(3) == 9
    
    if __name__ == "__main__":
        main() 
    

    注意,我们明确断言 square(2)square(3) 应该等于什么。我们的代码从四行测试减少到两行。

  • 我们可以通过以下方式故意破坏计算器代码:

    def main():
        x = int(input("What's x? "))
        print("x squared is", square(x))
    
    def square(n):
        return n + n
    
    if __name__ == "__main__":
        main() 
    

    注意,我们在平方函数中将 * 运算符改为了 +

  • 现在,在控制台窗口中运行 python test_calculator.py,你会注意到解释器抛出了一个 AssertionError。本质上,这是解释器告诉我们我们的某个条件没有满足。

  • 我们现在面临的一个挑战是,如果我们想要向用户提供更多描述性的错误输出,我们的代码可能会变得更加繁重。可能地,我们可以这样编写代码:

    from calculator import square
    
    def main():
        test_square()
    
    def test_square():
        try:
            assert square(2) == 4
        except AssertionError:
            print("2 squared is not 4")
        try:
            assert square(3) == 9
        except AssertionError:
            print("3 squared is not 9")
        try:
            assert square(-2) == 4
        except AssertionError:
            print("-2 squared is not 4")
        try:
            assert square(-3) == 9
        except AssertionError:
            print("-3 squared is not 9")
        try:
            assert square(0) == 0
        except AssertionError:
            print("0 squared is not 0")
    
    if __name__ == "__main__":
        main() 
    

    注意,运行此代码将产生多个错误。然而,它并没有产生上述所有错误。这是一个很好的说明,说明测试多个情况是有价值的,这样你可能会捕捉到存在编码错误的情况。

  • 上述代码说明了主要挑战:我们如何在不使用像上述那样数十行代码的情况下使测试代码更容易?

你可以在 Python 的文档中了解更多关于 assert 的信息:assert

pytest

  • pytest 是一个第三方库,允许你对程序进行单元测试。也就是说,你可以在程序中测试你的函数。

  • 要使用 pytest,请在控制台窗口中输入 pip install pytest

  • 在将 pytest 应用于我们自己的程序之前,按照以下方式修改你的 test_square 函数:

    from calculator import square
    
    def main():
        test_square()
    
    def test_square():
        assert square(2) == 4
        assert square(3) == 9
        assert square(-2) == 4
        assert square(-3) == 9
        assert square(0) == 0 
    

    注意上述代码断言了我们想要测试的所有条件。

  • pytest 允许我们直接通过它运行程序,这样我们可以更容易地查看测试条件的输出结果。

  • 在终端窗口中,输入 pytest test_calculator.py。你会立即注意到会提供输出。注意输出顶部附近的红色 F,表示你的代码中存在问题。此外,红色 E 提供了一些关于 calculator.py 程序中错误的信息。根据输出,你可以想象一个场景,其中 3 * 3 输出了 6 而不是 9。根据这个测试的结果,我们可以按照以下方式更正 calculator.py 代码:

    def main():
        x = int(input("What's x? "))
        print("x squared is", square(x))
    
    def square(n):
        return n * n
    
    if __name__ == "__main__":
        main() 
    

    注意我们在平方函数中将 + 运算符更改为 *,使其恢复到工作状态。

  • 再次运行 pytest test_calculator.py,注意没有错误产生。恭喜你!

  • 目前,pytest 在第一次测试失败后停止运行并不理想。再次,让我们将我们的 calculator.py 代码恢复到损坏状态:

    def main():
        x = int(input("What's x? "))
        print("x squared is", square(x))
    
    def square(n):
        return n + n
    
    if __name__ == "__main__":
        main() 
    

    注意我们在平方函数中将 * 运算符更改为 +,使其恢复到损坏状态。

  • 为了改进我们的测试代码,让我们将 test_calculator.py 中的代码分成不同的测试组:

    from calculator import square
    
    def test_positive():
        assert square(2) == 4
        assert square(3) == 9
    
    def test_negative():
        assert square(-2) == 4
        assert square(-3) == 9
    
    def test_zero():
        assert square(0) == 0 
    

    注意我们将相同的五个测试分成了三个不同的函数。像 pytest 这样的测试框架会运行每个函数,即使其中一个失败了。再次运行 pytest test_calculator.py,你会发现显示了许多更多的错误。更多的错误输出允许你进一步探索代码中可能产生问题的原因。

  • 在改进了测试代码后,将你的 calculator.py 代码恢复到完全工作状态:

    def main():
        x = int(input("What's x? "))
        print("x squared is", square(x))
    
    def square(n):
        return n * n
    
    if __name__ == "__main__":
        main() 
    

    注意我们在平方函数中将 + 运算符更改为 *,使其恢复到工作状态。

  • 再次运行 pytest test_calculator.py,你会发现没有错误发生。

  • 最后,我们可以测试我们的程序是否能够处理异常。让我们修改 test_calculator.py 来实现这一点。

 import pytest

  from calculator import square

  def test_positive():
      assert square(2) == 4
      assert square(3) == 9

  def test_negative():
      assert square(-2) == 4
      assert square(-3) == 9

  def test_zero():
      assert square(0) == 0

  def test_str():
      with pytest.raises(TypeError):
          square("cat") 

注意我们不再使用 assert,而是利用 pytest 库中的一个函数 raises,它允许你表达你期望抛出一个错误。我们需要将 import pytest 添加到程序顶部,然后使用 pytest.raises 并指定我们期望的错误类型。

  • 再次运行 pytest test_calculator.py,你会发现没有错误发生。

  • 总结来说,作为程序员,定义多少测试条件取决于你自己的判断!

你可以在 Pytest 的pytest文档中了解更多信息。

测试字符串

  • 回到过去,考虑以下hello.py的代码:

    def main():
        name = input("What's your name? ")
        hello(name)
    
    def hello(to="world"):
        print("hello,", to)
    
    if __name__ == "__main__":
        main() 
    

    注意,我们可能希望测试hello函数的结果。

  • 考虑以下test_hello.py的代码:

    from hello import hello
    
    def test_hello():
        assert hello("David") == "hello, David"
        assert hello() == "hello, world" 
    

    看到这段代码,你认为这种测试方法会有效吗?为什么这个测试可能不起作用?注意hello.py中的hello函数打印了一些内容:也就是说,它没有返回一个值!

  • 我们可以在hello.py中更改我们的hello函数,如下所示:

    def main():
        name = input("What's your name? ")
        print(hello(name))
    
    def hello(to="world"):
        return f"hello, {to}"
    
    if __name__ == "__main__":
        main() 
    

    注意,我们将hello函数更改为返回一个字符串。这意味着我们现在可以使用pytest来测试hello函数。

  • 运行pytest test_hello.py,我们的代码将通过所有测试!

  • 就像本课之前的测试案例一样,我们可以将测试分开进行:

    from hello import hello
    
    def test_default():
        assert hello() == "hello, world"
    
    def test_argument():
        assert hello("David") == "hello, David" 
    

    注意,上述代码将我们的测试分成多个函数,这样即使产生错误,它们也会全部运行。

将测试组织到文件夹中

  • 使用多个测试进行单元测试是如此常见,以至于你可以使用单个命令运行整个测试文件夹。

  • 首先,在终端窗口中,执行mkdir test以创建一个名为test的文件夹。

  • 然后,在终端窗口中输入code test/test_hello.py以在该文件夹内创建一个测试。注意test/指示终端在名为test的文件夹中创建test_hello.py

  • 在文本编辑窗口中,修改文件以包含以下代码:

    from hello import hello
    
    def test_default():
        assert hello() == "hello, world"
    
    def test_argument():
        assert hello("David") == "hello, David" 
    

    注意,我们正在创建一个测试,就像之前做的那样。

  • pytest不会允许我们仅使用这个文件(或一组文件)作为文件夹来运行测试,而不需要一个特殊的__init__文件。在你的终端窗口中,通过输入code test/__init__.py创建这个文件。注意,就像之前一样,test/以及init两边的双下划线。即使这个__init__.py文件为空,pytest也会知道包含__init__.py的整个文件夹包含可以运行的测试。

  • 现在,在终端中输入pytest test,你可以运行整个test文件夹中的代码。

你可以在 Pytest 的导入机制文档中了解更多信息。

总结

测试你的代码是编程过程中的一个自然部分。单元测试允许你测试代码的特定方面。你可以创建自己的程序来测试你的代码。或者,你可以利用像pytest这样的框架来自动运行你的单元测试。在本讲中,你学习了关于……

  • 单元测试

  • assert

  • pytest

第六讲

原文:cs50.harvard.edu/python/notes/6/

  • 文件输入/输出

  • open

  • with

  • CSV

  • 二进制文件和 PIL

  • 总结

文件输入/输出

  • 到目前为止,我们编写的所有程序都是在内存中存储信息。也就是说,一旦程序结束,从用户那里收集的所有信息或程序生成的所有信息都会丢失。

  • 文件输入/输出是程序将文件作为输入或创建文件作为输出的能力。

  • 首先,在终端窗口中输入 code names.py 并编写以下代码:

    name = input("What's your name?" )
    print(f"hello, {name}") 
    

    注意,运行此代码会产生预期的输出。用户可以输入一个名字,输出结果符合预期。

  • 然而,如果我们想允许输入多个名字呢?我们该如何实现?回想一下,list 是一种数据结构,允许我们将多个值存储到单个变量中。按照以下方式编写代码:

    names = []
    
    for _ in range(3):
        name = input("What's your name?" )
        names.append(name) 
    

    注意,用户将被提示三次输入。使用 append 方法将 name 添加到我们的 names 列表中。

  • 这段代码可以简化为以下形式:

    names = []
    
    for _ in range(3):
        names.append(input("What's your name?" )) 
    

    注意,这与之前的代码块有相同的结果。

    • 现在,让我们启用打印名字列表为排序列表的功能。按照以下方式编写代码:
    names = []
    
    for _ in range(3):
        names.append(input("What's your name?" ))
    
    for name in sorted(names):
        print(f"hello, {name}") 
    

    注意,一旦程序执行完毕,所有信息都会丢失。文件输入/输出允许你的程序存储这些信息,以便以后使用。

  • 你可以在 Python 的 sorted 文档中了解更多信息。

open

  • open 是 Python 内置的一个功能,允许你打开一个文件并在你的程序中使用它。open 函数允许你以读取或写入的方式打开一个文件。

  • 为了向你展示如何在程序中启用文件输入/输出,让我们回顾一下并编写以下代码:

    name = input("What's your name? ")
    
    file = open("names.txt", "w")
    file.write(name)
    file.close() 
    

    注意,open 函数以写入模式打开了一个名为 names.txt 的文件,这由 w 表示。上面的代码将打开的文件分配给一个名为 file 的变量。file.write(name) 这行代码将名字写入文本文件。之后的行关闭了文件。

  • 通过在终端中输入 python names.py 测试你的代码,你可以输入一个名字,并将其保存到文本文件中。然而,如果你使用不同的名字多次运行程序,你会注意到这个程序每次都会完全重写 names.txt 文件。

  • 理想情况下,我们希望能够将我们的每个名字追加到文件中。在终端窗口中输入 rm names.txt 来删除现有的文本文件。然后,按照以下方式修改你的代码:

    name = input("What's your name? ")
    
    file = open("names.txt", "a")
    file.write(name)
    file.close() 
    

    注意,我们代码中唯一的改变是将 w 改为 a 以实现“追加”。多次重新运行此程序,你会注意到名字将被添加到文件中。然而,你也会发现一个新的问题!

  • 在运行程序多次后检查您的文本文件,您会注意到名字是连在一起的。名字之间没有任何间隔。您可以修复这个问题。再次,通过在终端窗口中键入 rm names.txt 来删除现有的文本文件。然后,按照以下方式修改您的代码:

    name = input("What's your name? ")
    
    file = open("names.txt", "a")
    file.write(f"{name}\n")
    file.close() 
    

    注意到带有 file.write 的行已经被修改,为每个名字添加了行尾换行符。

  • 这段代码工作得相当好。然而,有方法可以改进这个程序。它很容易忘记关闭文件。

  • 您可以在 Python 的 open 文档中了解更多信息。

with

  • 关键字 with 允许您自动化文件的关闭。

  • 按照以下方式修改您的代码:

    name = input("What's your name? ")
    
    with open("names.txt", "a") as file:
        file.write(f"{name}\n") 
    

    注意到 with 下的行是缩进的。

  • 到目前为止,我们一直是在向文件中写入。如果我们想从文件中读取呢?为了启用此功能,按照以下方式修改您的代码:

    with open("names.txt", "r") as file:
        lines = file.readlines()
    
    for line in lines:
        print("hello,", line) 
    

    注意到 readlines 具有读取文件中所有行并将它们存储在名为 lines 的列表中的特殊能力。运行您的程序,您会注意到输出相当难看。似乎在应该只有一个换行符的地方有多个换行符。

  • 有许多方法可以解决这个问题。但是,这里有一个简单的方法可以修复我们代码中的这个错误:

    with open("names.txt", "r") as file:
        lines = file.readlines()
    
    for line in lines:
        print("hello,", line.rstrip()) 
    

    注意到 rstrip 具有移除每行末尾多余换行符的效果。

  • 尽管如此,这段代码还可以进一步简化:

    with open("names.txt", "r") as file:
        for line in file:
            print("hello,", line.rstrip()) 
    

    注意到运行这段代码是正确的。然而,请注意我们没有对名字进行排序。

  • 这段代码可以进一步改进,以允许对名字进行排序:

    names = []
    
    with open("names.txt") as file:
        for line in file:
            names.append(line.rstrip())
    
    for name in sorted(names):
        print(f"hello, {name}") 
    

    注意到 names 是一个空白列表,我们可以在这里收集名字。每个名字都会追加到内存中的 names 列表中。然后,内存中排序列表中的每个名字都会被打印出来。运行您的代码,您会看到名字现在已经被正确排序。

  • 如果我们想要存储的不仅仅是学生的名字呢?如果我们想存储学生的名字和他们的宿舍呢?

CSV

  • CSV 代表“逗号分隔值”。

  • 在您的终端窗口中,键入 code students.csv。确保您的新 CSV 文件看起来如下:

    Hermione,Gryffindor
    Harry,Gryffindor
    Ron,Gryffindor
    Draco,Slytherin 
    
  • 通过键入 code students.py 创建一个新的程序,并按照以下方式编写代码:

    with open("students.csv") as file:
        for line in file:
            row = line.rstrip().split(",")
            print(f"{row[0]} is in {row[1]}") 
    

    注意到 rstrip 移除了我们 CSV 文件中每行的末尾。split 告诉解释器在哪里找到我们 CSV 文件中每个值的末尾。row[0] 是我们 CSV 文件每行中的第一个元素。row[1] 是我们 CSV 文件每行中的第二个元素。

  • 上述代码有效地将我们的 CSV 文件中的每一行或“记录”分开。然而,如果您不熟悉这种语法,看起来可能有点晦涩。Python 有内置的能力可以进一步简化这段代码。按照以下方式修改您的代码:

    with open("students.csv") as file:
        for line in file:
            name, house = line.rstrip().split(",")
            print(f"{name} is in {house}") 
    

    注意,split 函数实际上返回两个值:逗号之前的一个和逗号之后的一个。因此,我们可以依赖这个功能一次分配两个变量而不是一个!

  • 假设我们再次希望提供这个列表作为排序后的输出?你可以按照以下方式修改你的代码:

    students = []
    
    with open("students.csv") as file:
        for line in file:
            name, house = line.rstrip().split(",")
            students.append(f"{name} is in {house}")
    
    for student in sorted(students):
        print(student) 
    

    注意,我们创建了一个名为 studentslist。我们将每个字符串 append 到这个列表中。然后,我们输出列表的排序版本。

  • 回想一下,Python 允许使用 dictionaries,其中键可以与值相关联。这段代码可以进一步改进

    students = []
    
    with open("students.csv") as file:
        for line in file:
            name, house = line.rstrip().split(",")
            student = {}
            student["name"] = name
            student["house"] = house
            students.append(student)
    
    for student in students:
        print(f"{student['name']} is in {student['house']}") 
    

    注意,我们创建了一个名为 student 的空字典。我们将每个学生的值添加到 student 字典中,包括他们的名字和学院。然后,我们将该学生添加到名为 studentslist 中。

  • 我们可以改进我们的代码来展示这一点,如下所示:

    students = []
    
    with open("students.csv") as file:
        for line in file:
            name, house = line.rstrip().split(",")
            student = {"name": name, "house": house}
            students.append(student)
    
    for student in students:
        print(f"{student['name']} is in {student['house']}") 
    

    注意,这样会产生期望的结果,但排除了学生的排序。

  • 不幸的是,我们无法像以前那样对学生的列表进行排序,因为每个学生现在都是一个列表中的字典。如果 Python 能够按学生的名字对 students 列表中的 student 字典进行排序,那将很有帮助。

  • 要在我们的代码中实现这一点,进行以下更改:

    students = []
    
    with open("students.csv") as file:
        for line in file:
            name, house = line.rstrip().split(",")
            students.append({"name": name, "house": house})
    
    def get_name(student):
        return student["name"]
    
    for student in sorted(students, key=get_name):
        print(f"{student['name']} is in {student['house']}") 
    

    注意,sorted 需要知道如何获取每个学生的键。Python 允许一个名为 key 的参数,我们可以定义学生列表将按什么“键”进行排序。因此,get_name 函数简单地返回 student["name"] 的键。运行这个程序,你现在会看到列表已按名字排序。

  • 尽管如此,我们的代码还可以进一步改进。恰好如果你只打算使用像 get_name 这样的函数一次,你可以按照以下方式简化你的代码。按照以下方式修改你的代码:

    students = []
    
    with open("students.csv") as file:
        for line in file:
            name, house = line.rstrip().split(",")
            students.append({"name": name, "house": house})
    
    for student in sorted(students, key=lambda student: student["name"]):
        print(f"{student['name']} is in {student['house']}") 
    

    注意我们如何使用一个 lambda 函数,一个匿名函数,它说:“嘿 Python,这里有一个没有名字的函数:给定一个 student,访问他们的 name 并将其作为 key 返回。”

  • 不幸的是,我们的代码有点脆弱。假设我们改变了我们的 CSV 文件,使得我们指明了每个学生的成长地。这对我们的程序会有什么影响?首先,按照以下方式修改你的 students.csv 文件:

Harry,"Number Four, Privet Drive"
Ron,The Burrow
Draco,Malfoy Manor 

注意,运行我们的程序会产生许多错误。

  • 现在我们处理的是家园(homes)而不是学院(houses),按照以下方式修改你的代码:

    students = []
    
    with open("students.csv") as file:
        for line in file:
            name, home = line.rstrip().split(",")
            students.append({"name": name, "home": home})
    
    for student in sorted(students, key=lambda student: student["name"]):
        print(f"{student['name']} is in {student['home']}") 
    

    注意,运行我们的程序仍然不能正常工作。你能猜到为什么吗?

  • 解释器产生的 ValueError: too many values to unpack 错误是由于我们之前创建这个程序时预期 CSV 文件是用逗号(,)分割的。我们可以花更多时间解决这个问题,但确实有人已经开发了一种“解析”(即读取)CSV 文件的方法!

  • Python 的内置 csv 库包含一个名为 reader 的对象。正如其名所示,我们可以使用 reader 来读取我们的 CSV 文件,即使“四号,紫杉巷”中有多余的逗号。readerfor 循环中工作,每次迭代时 reader 都会从我们的 CSV 文件中提供另一行。这一行本身是一个列表,其中列表中的每个值对应于该行中的一个元素。例如,row[0] 是给定行的第一个元素,而 row[1] 是第二个元素。

    import csv
    
    students = []
    
    with open("students.csv") as file:
        reader = csv.reader(file)
        for row in reader:
            students.append({"name": row[0], "home": row[1]})
    
    for student in sorted(students, key=lambda student: student["name"]):
        print(f"{student['name']} is from {student['home']}") 
    

    注意现在我们的程序按预期工作。

  • 到目前为止,我们一直依赖于我们的程序来具体决定 CSV 文件中的哪些部分是名字,哪些部分是家庭地址。然而,更好的设计是将这些直接嵌入到我们的 CSV 文件中,如下所示进行编辑:

    name,home
    Harry,"Number Four, Privet Drive"
    Ron,The Burrow
    Draco,Malfoy Manor 
    

    注意我们在 CSV 文件中明确指出,任何读取它的人都应预期每行都有一个名字值和一个家庭值。

  • 我们可以修改我们的代码,使用 csv 库中的一个名为 DictReader 的部分,以获得更大的灵活性来处理我们的 CSV 文件:

    import csv
    
    students = []
    
    with open("students.csv") as file:
        reader = csv.DictReader(file)
        for row in reader:
            students.append({"name": row["name"], "home": row["home"]})
    
    for student in sorted(students, key=lambda student: student["name"]):
        print(f"{student['name']} is in {student['home']}") 
    

    注意我们已经将 reader 替换为 DictReader,它每次返回一个字典。此外,注意解释器将直接访问 row 字典,获取每个学生的 namehome。这是一个编码防御的例子。只要设计 CSV 文件的人已经在第一行输入了正确的标题信息,我们就可以使用我们的程序访问这些信息。

  • 到目前为止,我们一直在读取 CSV 文件。如果我们想写入 CSV 文件怎么办?

  • 首先,让我们清理一下我们的文件。首先,在终端窗口中输入 rm students.csv 删除 students.csv 文件。此命令仅在你位于 students.csv 文件相同的文件夹中时有效。

  • 然后,在 students.py 文件中,按照以下方式修改你的代码:

    import csv
    
    name = input("What's your name? ")
    home = input("Where's your home? ")
    
    with open("students.csv", "a") as file:
        writer = csv.DictWriter(file, fieldnames=["name", "home"])
        writer.writerow({"name": name, "home": home}) 
    

    注意我们如何利用 DictWriter 的内置功能,它接受两个参数:要写入的 file 和要写入的 fieldnames。此外,注意 writerow 函数接受一个字典作为其参数。实际上,我们是在告诉解释器写入一个包含两个字段名为 namehome 的行。

  • 注意,你可以从和写入许多类型的文件。

  • 你可以在 Python 的 CSV 文档中了解更多信息。

二进制文件和 PIL

  • 我们今天还将讨论另一种类型的文件,即二进制文件。二进制文件简单来说就是由一串零和一组成的集合。这种类型的文件可以存储任何内容,包括音乐和图像数据。

  • 有一个流行的 Python 库叫做 PIL,它与图像文件配合得很好。

  • 动画 GIF 是一种流行的图像文件类型,其中包含多个图像文件,这些图像文件会按顺序反复播放,从而创建出简单的动画或视频效果。

  • 想象一下,我们有一系列服装,如下所示。

  • 这里是 costume1.gif

猫编号 1.

  • 这还有一个叫做 costume2.gif 的例子。注意腿部位置略有不同。

猫编号 2.

  • 在继续之前,请确保您已从课程网站下载了源代码文件。如果没有上述两张图像,您将无法编写以下代码。

  • 在终端窗口中输入 code costumes.py 并按照以下代码编写:

    import sys
    
    from PIL import Image
    
    images = []
    
    for arg in sys.argv[1:]:
        image = Image.open(arg)
        images.append(image)
    
    images[0].save(
        "costumes.gif", save_all=True, append_images=[images[1]], duration=200, loop=0
    ) 
    

    注意我们是从 PIL 中导入 Image 功能。注意第一个 for 循环只是简单地遍历提供的命令行参数中的图像,并将它们存储到名为 images 的列表中。1: 表示从 argv 的第二个元素开始切片。代码的最后几行保存了第一张图像,并将其与第二张图像一起附加,创建了一个动画 GIF。在终端中输入 python costumes.py costume1.gif costume2.gif。现在,在终端窗口中输入 code costumes.gif,你现在可以看到一个动画 GIF。

  • 您可以在 Pillow 的 PIL 文档中了解更多信息。

总结

现在,我们不仅看到了我们可以以文本方式编写和读取文件——我们还可以使用一和零来读写文件。我们迫不及待地想看看你将如何利用这些新能力取得成就。

  • 文件输入/输出

  • open

  • with

  • CSV

  • PIL

第七讲

原文:cs50.harvard.edu/python/notes/7/

  • 正则表达式

  • 大小写敏感

  • 清理用户输入

  • 提取用户输入

  • 总结

正则表达式

  • 正则表达式或“regexes”将使我们能够检查代码中的模式。例如,我们可能想要验证电子邮件地址的格式是否正确。正则表达式将使我们能够以这种方式检查表达式。

  • 首先,在终端窗口中输入 code validate.py。然后,在文本编辑器中按照以下方式编写代码:

    email = input("What's your email? ").strip()
    
    if "@" in email:
        print("Valid")
    else:
        print("Invalid") 
    

    注意,strip 方法将移除输入字符串开头或结尾的空白字符。运行此程序,您将看到只要输入了 @ 符号,程序就会将其视为有效。

  • 您可以想象,然而,有人可能会输入 @@ 独自存在,并且输入可能会被视为有效。我们可以认为电子邮件地址至少包含一个 @ 和一个 .。修改您的代码如下:

    email = input("What's your email? ").strip()
    
    if "@" in email and "." in email:
        print("Valid")
    else:
        print("Invalid") 
    

    注意,虽然这符合预期,但我们的用户可能是敌意的,简单地输入 @. 就会导致程序返回 valid

  • 我们可以改进程序的逻辑如下:

    email = input("What's your email? ").strip()
    
    username, domain = email.split("@")
    
    if username and "." in domain:
        print("Valid")
    else:
        print("Invalid") 
    

    注意 strip 方法是如何用来判断 username 是否存在以及 . 是否在 domain 变量中的。运行此程序,您输入的标准电子邮件地址可能会被认为是 valid。单独输入 malan@harvard,您会发现程序将此输入视为 invalid

  • 我们可以更加精确,修改我们的代码如下:

    email = input("What's your email? ").strip()
    
    username, domain = email.split("@")
    
    if username and domain.endswith(".edu"):
        print("Valid")
    else:
        print("Invalid") 
    

    注意 endswith 方法将检查 domain 是否包含 .edu。然而,恶意用户仍然可以破坏我们的代码。例如,一个用户可以输入 malan@.edu,它将被认为是有效的。

  • 事实上,我们可以自己不断迭代此代码。然而,结果证明,Python 有一个名为 re 的现有库,它包含许多内置函数,可以验证用户输入与模式是否匹配。

  • re 中最通用的函数之一是 search

  • search 函数遵循以下签名 re.search(pattern, string, flags=0)。根据此签名,我们可以修改我们的代码如下:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search("@", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意,这并没有增加程序的功能。事实上,这有点像是退步。

  • 我们可以进一步扩展程序的功能。然而,我们需要在 validation 方面扩展我们的词汇。结果证明,在正则表达式的世界中,有一些符号允许我们识别模式。到目前为止,我们只检查了特定的文本片段,如 @。事实上,许多特殊符号可以传递给解释器,用于进行验证。以下是一些模式的非详尽列表:

    .   any character except a new line
    *   0 or more repetitions
    +   1 or more repetitions
    ?   0 or 1 repetition
    {m} m repetitions
    {m,n} m-n repetitions 
    
  • 在我们的代码中实现此功能,修改如下:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(".+@.+", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意,我们不在乎用户名或域名是什么。我们关心的是模式。.+ 用于确定电子邮件地址左侧是否有任何内容,以及电子邮件地址右侧是否有任何内容。运行你的代码,输入 malan@,你会发现输入被认为是 无效 的,正如我们所希望的。

  • 如果我们在上面的代码中使用了正则表达式 .*@.*,你可以这样可视化它:状态机

    注意正则表达式的 状态机 描述。在左侧,解释器从左到右开始评估语句。一旦我们到达 q1 或第一个问题,解释器就会根据提供的表达式反复读取。然后,状态改变,现在正在查看 q2 或正在验证的第二个问题。再次,箭头指示表达式将根据我们的编程反复评估。然后,如双圆圈所示,达到状态机的最终状态。

  • 考虑到我们在代码中使用的正则表达式 .+@.+,你可以这样可视化它:状态机

    注意 q1 是用户提供的任何字符,包括作为 1 或多个字符重复的 'q2'。然后是 @ 符号。接着,q3 寻找用户提供的任何字符,包括作为 1 或多个字符重复的 q4

  • rere.search 函数以及类似的函数寻找模式。

  • 继续改进我们的代码,我们可以这样改进我们的代码:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(".+@.+.edu", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意,然而,你可以输入 malan@harvard?edu 并被认为是有效的。这是为什么?你可能会意识到,在验证的语言中,. 表示任何字符!

  • 我们可以修改我们的代码如下:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r".+@.+\.edu", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意我们如何利用“转义字符”或 \. 视为字符串的一部分,而不是验证表达式的一部分。测试你的代码,你会发现 malan@harvard.edu 被认为是有效的,而 malan@harvard?edu 是无效的。

  • 现在我们正在使用转义字符,是时候介绍“原始字符串”了。在 Python 中,原始字符串是不格式化特殊字符的字符串——相反,每个字符都被当作字面值。例如,想象一下 \n。我们在之前的讲座中看到,在一个普通字符串中,这两个字符变成一个:一个特殊的换行符。然而,在原始字符串中,\n 被视为不是 \n 这个特殊字符,而是单个 \ 和单个 n。在字符串前放置一个 r 告诉 Python 解释器将字符串视为原始字符串,类似于在字符串前放置一个 f 告诉 Python 解释器将字符串视为格式化字符串:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^.+@.+\.edu$", email):
        print("Valid")
    else:
        print("Invalid") 
    

    现在我们已经确保 Python 解释器不会将 \. 视为特殊字符。相反,它只是一个 \ 后跟一个 .——在正则表达式中,这意味着匹配一个字面量的 .

  • 你可以想象我们的用户仍然可能给我们制造麻烦!例如,你可以输入一个句子,如 My email address is malan@harvard.edu.,整个句子都会被视为有效。我们可以使我们的编码更加精确。

  • 恰好我们还有更多特殊符号可供使用:

    ^   matches the start of the string
    $   matches the end of the string or just before the newline at the end of the string 
    
  • 我们可以使用我们添加的词汇表按如下方式修改我们的代码:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^.+@.+\.edu$", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意这会使得验证表达式在开始和结束处寻找这个精确的模式匹配。输入一个句子,如 My email address is malan@harvard.edu.,现在被视为无效。

  • 我们提议我们可以做得更好!尽管我们现在正在寻找字符串开头的用户名、@ 符号和结尾的域名,但我们可以输入任意多的 @ 符号!malan@@@harvard.edu 被视为有效!

  • 我们可以按如下方式扩展我们的词汇表:

    []    set of characters
    [^]   complementing the set 
    
  • 使用这些新获得的能力,我们可以按如下方式修改我们的表达式:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^[^@]+@[^@]+\.edu$", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意到 ^ 表示匹配字符串的开始。在我们的表达式的末尾,$ 表示匹配字符串的末尾。[^@]+ 表示除了 @ 之外的任何字符。然后,我们有一个字面量 @[^@]+\.edu 表示除了 @ 之外的任何字符,后面跟着以 .edu 结尾的表达式。输入 malan@@@harvard.edu 现在被视为无效。

  • 我们还可以进一步改进这个正则表达式。结果证明,电子邮件地址有一些特定的要求!目前,我们的验证表达式过于宽容。我们可能只想允许在句子中通常使用的字符。我们可以按如下方式修改我们的代码:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^[a-zA-Z0-9_]+@[a-zA-Z0-9_]+\.edu$", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意 [a-zA-Z0-9_] 告诉验证,字符必须在 azAZ09 之间,并且可能包括一个 _ 符号。测试输入,你会发现许多潜在的用户错误可以被指示出来。

  • 幸运的是,程序员们已经将常见的模式内置到正则表达式中。在这种情况下,你可以按如下方式修改你的代码:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^\w+@\w+\.edu$", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意 \w[a-zA-Z0-9_] 相同。感谢辛勤工作的程序员们!

  • 这里有一些额外的模式我们可以添加到我们的词汇表中:

    \d    decimal digit
    \D    not a decimal digit
    \s    whitespace characters
    \S    not a whitespace character
    \w    word character, as well as numbers and the underscore
    \W    not a word character 
    
  • 现在,我们知道不仅仅是 .edu 电子邮件地址。我们可以按如下方式修改我们的代码:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^\w+@\w.+\.(com|edu|gov|net|org)$", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意 | 在我们的表达式中具有 or 的影响。

  • 在我们的词汇表中添加更多符号,以下是一些需要考虑的:

    A|B     either A or B
    (...)   a group
    (?:...) non-capturing version 
    

大小写敏感性

  • 为了说明如何处理关于大小写敏感性的问题,其中 EDUedu 以及类似的情况之间存在差异,让我们将我们的代码回滚到以下状态:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^\w+@\w+\.edu$", email):
        print("Valid")
    else:
        print("Invalid") 
    

    注意我们已经移除了之前提供的 | 语句。

  • 回想一下,在 re.search 函数中,有一个名为 flags 的参数。

  • 一些内置的标志变量是:

    re.IGNORECASE
    re.MULTILINE
    re.DOTALL 
    

    考虑你如何在你的代码中使用这些。

  • 因此,我们可以按照以下方式更改我们的代码。

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^\w+@\w+\.edu$", email, re.IGNORECASE):
        print("Valid")
    else:
        print("Invalid") 
    

    注意我们添加了第三个参数re.IGNORECASE。运行这个程序,输入MALAN@HARVARD.EDU,现在输入被认为是有效的。

  • 考虑以下电子邮件地址malan@cs50.harvard.edu。使用我们上面的代码,这将被认为是无效的。为什么可能是这样?

  • 由于多了一个额外的.,程序认为这是无效的。

  • 结果表明,我们可以从之前的学习词汇中,将一些想法分组在一起。

    A|B     either A or B
    (...)   a group
    (?:...) non-capturing version 
    
  • 我们可以按照以下方式修改我们的代码:

    import re
    
    email = input("What's your email? ").strip()
    
    if re.search(r"^\w+@(\w+\.)?\w+\.edu$", email, re.IGNORECASE):
        print("Valid")
    else:
        print("Invalid") 
    

    注意到(\w+\.)?告诉解释器这个新表达式可以出现一次或根本不出现。因此,malan@cs50.harvard.edumalan@harvard.edu都被认为是有效的。

  • 趣味的是,我们到目前为止对代码所做的编辑并没有完全涵盖所有可以做的检查以确保有效的电子邮件地址。确实,以下是确保输入有效电子邮件地址时必须输入的完整表达式:

    ^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ 
    
  • re库中还有其他你可能觉得有用的函数。re.matchre.fullmatch是你可能会发现极其有用的。

  • 你可以在 Python 的re文档中了解更多信息。

清理用户输入

  • 你永远不应该期望你的用户总是遵循你对干净输入的期望。实际上,用户经常会违反程序员的本意。

  • 有方法可以清理你的数据。

  • 在终端窗口中,输入code format.py。然后,在文本编辑器中,按照以下方式编写代码:

    name = input("What's your name? ").strip()
    print(f"hello, {name}") 
    

    注意我们实际上创建了一个“hello world”程序。运行这个程序并输入David,它运行得很好!然而,输入Malan, David时,你会注意到程序并没有按预期工作。我们如何修改我们的程序来清理这个输入?

  • 按照以下方式修改你的代码。

    name = input("What's your name? ").strip()
    if "," in name:
        last, first = name.split(", ")
        name = f"{first}  {last}"
    print(f"hello, {name}") 
    

    注意到last, first = name.split(", ")在名字中有,时执行。然后,名字被标准化为 first 和 last。运行我们的代码,输入Malan, David,你可以看到这个程序至少清理了一个用户输入意外内容的情况。

  • 你可能会注意到,在Malan,David中不输入空格会导致解释器抛出错误。既然我们现在知道了某些正则表达式语法,让我们将其应用到我们的代码中:

    import re
    
    name = input("What's your name? ").strip()
    matches = re.search(r"^(.+), (.+)$", name)
    if matches:
        last, first = matches.groups()
        name = first + "  " + last
    print(f"hello, {name}") 
    

    注意到re.search可以返回一组从用户输入中提取的匹配项。如果re.search返回匹配项,运行这个程序,输入David Malan,注意if条件没有执行,并且返回了名字。如果你通过输入Malan, David运行程序,名字也会正确返回。

  • 恰好我们可以使用matches.group请求特定的组。我们可以按照以下方式修改我们的代码:

    import re
    
    name = input("What's your name? ").strip()
    matches = re.search(r"^(.+), (.+)$", name)
    if matches:
        name = matches.group(2) + "  " + matches.group(1)
    print(f"hello, {name}") 
    

    注意在这个实现中,group不是复数(没有s)。

  • 我们可以将代码进一步优化如下:

    import re
    
    name = input("What's your name? ").strip()
    matches = re.search(r"^(.+), (.+)$", name)
    if matches:
        name = matches.group(2) + "  " + matches.group(1)
    print(f"hello, {name}") 
    

    注意group(2)group(1)是如何用空格连接在一起的。第一个组是逗号左边的部分。第二个组是逗号右边的部分。

  • 仍然要注意,如果输入Malan,David时没有空格,这仍然会破坏我们的代码。因此,我们可以进行以下修改:

    import re
    
    name = input("What's your name? ").strip()
    matches = re.search(r"^(.+), *(.+)$", name)
    if matches:
        name = matches.group(2) + "  " + matches.group(1)
    print(f"hello, {name}") 
    

    注意我们在验证语句中添加了*。现在这段代码将接受并正确处理Malan,David。此外,它还将正确处理前面有多个空格的David,Malan

  • 在之前的例子中,我们非常常见地使用re.search,其中matches是在代码行之后的。然而,我们可以组合这些语句:

    import re
    
    name = input("What's your name? ").strip()
    if matches := re.search(r"^(.+), *(.+)$", name):
        name = matches.group(2) + "  " + matches.group(1)
    print(f"hello, {name}") 
    

    注意我们如何合并两行代码。walrus := 操作符从右向左赋值,同时允许我们提出一个布尔问题。侧过头来看,你就会明白为什么这被称为 walrus 操作符。

  • 你可以在 Python 的re文档中了解更多信息。

提取用户输入

  • 到目前为止,我们已经验证了用户的输入并清理了用户的输入。

  • 现在,让我们从用户输入中提取一些具体信息。在终端窗口中,输入code twitter.py,然后在文本编辑器窗口中按如下方式编写代码:

    url = input("URL: ").strip()
    print(url) 
    

    注意,如果我们输入https://twitter.com/davidjmalan,它将显示用户输入的确切内容。然而,我们如何能够只提取用户名并忽略 URL 的其余部分?

  • 你可以想象我们如何简单地去除标准 Twitter URL 的开头部分。我们可以尝试如下操作:

    url = input("URL: ").strip()
    
    username = url.replace("https://twitter.com/", "")
    print(f"Username: {username}") 
    

    注意replace方法如何允许我们找到一项并将其替换为另一项。在这种情况下,我们正在找到 URL 的一部分并将其替换为空。输入完整的 URL https://twitter.com/davidjmalan,程序实际上输出了用户名。然而,这个当前程序有哪些不足之处?

  • 如果用户只是简单地输入twitter.com而没有包括https://等,会怎样?你可以想象出许多场景,用户可能会输入或遗漏输入 URL 的部分,这会导致程序输出奇怪的结果。为了改进这个程序,我们可以按如下方式编写代码:

    url = input("URL: ").strip()
    
    username = url.removeprefix("https://twitter.com/")
    print(f"Username: {username}") 
    

    注意我们如何利用removeprefix方法。这个方法将移除字符串的开头部分。

  • 正则表达式仅仅允许我们简洁地表达模式和目标。

  • re库中,有一个名为sub的方法。这个方法允许我们用其他内容替换模式。

  • sub方法的签名如下

    re.sub(pattern, repl, string, count=0, flags=0) 
    

    注意pattern指的是我们正在寻找的正则表达式。然后是一个repl字符串,我们可以用它来替换模式。最后是我们要进行替换的string

  • 在我们的代码中实现此方法后,我们可以按如下方式修改我们的程序:

    import re
    
    url = input("URL: ").strip()
    
    username = re.sub(r"https://twitter.com/", "", url)
    print(f"Username: {username}") 
    

    注意执行此程序并输入 https://twitter.com/davidjmalan 会产生正确的结果。然而,我们的代码中仍然存在一些问题。

  • 协议、子域以及用户可能在用户名之后输入 URL 任何部分的可能性,这些都是此代码仍然不是最佳方案的原因。我们可以进一步解决这些缺点,如下所示:

    import re
    
    url = input("URL: ").strip()
    
    username = re.sub(r"^(https?://)?(www\.)?twitter\.com/", "", url)
    print(f"Username: {username}") 
    

    注意在 url 中添加了 ^ 上标。注意 . 也可能被解释器错误地解释。因此,我们使用 \ 来转义它,使其变为 \.。为了容忍 httphttps,我们在 https? 的末尾添加一个 ?,使 s 可选。此外,为了适应 www,我们在代码中添加 (www\.)?。最后,以防用户决定完全省略协议,我们将 http://https:// 设置为可选,使用 (https?://)

  • 尽管如此,我们仍然盲目地期望用户输入的 URL 确实包含用户名。

  • 利用我们对 re.search 的了解,我们可以进一步改进我们的代码。

    import re
    
    url = input("URL: ").strip()
    
    matches = re.search(r"^https?://(www\.)?twitter\.com/(.+)$", url, re.IGNORECASE)
    if matches:
        print(f"Username:", matches.group(2)) 
    

    注意我们是如何在用户提供的字符串中搜索上述正则表达式的。特别是,我们使用 (.+)$ 正则表达式捕获 URL 末尾出现的内容。因此,如果用户没有输入不带用户名的 URL,则不会显示任何输入。

  • 进一步收紧我们的程序,我们可以利用我们的 := 操作符如下:

    import re
    
    url = input("URL: ").strip()
    
    if matches := re.search(r"^https?://(?:www\.)?twitter\.com/(.+)$", url, re.IGNORECASE):
        print(f"Username:", matches.group(1)) 
    

    注意到 ?: 告诉解释器它不需要捕获正则表达式中的那个位置的内容。

  • 尽管如此,我们可以更加明确地确保输入的用户名是正确的。使用 Twitter 的文档,我们可以在我们的正则表达式中添加以下内容:

    import re
    
    url = input("URL: ").strip()
    
    if matches := re.search(r"^https?://(?:www\.)?twitter\.com/([a-z0-9_]+)", url, re.IGNORECASE):
        print(f"Username:", matches.group(1)) 
    

    注意 [a-z0-9_]+ 告诉解释器只期望 a-z0-9_ 作为正则表达式的一部分。+ 表示我们期望一个或多个字符。

  • 你可以在 Python 的re文档中了解更多信息。

总结

现在,你已经学会了一种全新的正则表达式语言,可以用来验证、清理和提取用户输入。

  • 正则表达式

  • 区分大小写

  • 清理用户输入

  • 提取用户输入

第八讲

原文:cs50.harvard.edu/python/notes/8/

  • 面向对象编程

  • 抛出异常

  • 装饰器

  • 将课程中的先前工作联系起来

  • 类方法

  • 静态方法

  • 继承

  • 继承和异常

  • 运算符重载

  • 总结

面向对象编程

  • 编程有不同的范式。当你学习其他语言时,你将开始识别这些模式。

  • 到目前为止,你一直是按步骤进行过程式编程的。

  • 面向对象编程(OOP)是解决编程相关问题的有力解决方案。

  • 首先,在终端窗口中输入code student.py,然后按照以下方式编写代码:

    name = input("Name: ")
    house = input("House: ")
    print(f"{name} from {house}") 
    

    注意这个程序遵循的是一种过程式、按步骤的范式:就像你在课程的前几部分看到的那样。

  • 借鉴前几周的工作,我们可以创建函数来抽象掉程序的一部分。

    def main():
        name = get_name()
        house = get_house()
        print(f"{name} from {house}")
    
    def get_name():
        return input("Name: ")
    
    def get_house():
        return input("House: ")
    
    if __name__ == "__main__":
        main() 
    

    注意get_nameget_house如何抽象掉main函数的一些需求。此外,注意代码的最后一行是如何告诉解释器运行main函数的。

  • 我们可以通过将学生存储为tuple来进一步简化我们的程序。tuple是一系列值。与list不同,tuple不能被修改。在精神上,我们正在返回两个值。

    def main():
        name, house = get_student()
        print(f"{name} from {house}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return name, house
    
    if __name__ == "__main__":
        main() 
    

    注意get_student返回name, house

  • tuple打包,以便我们能够将两个项目返回到名为student的变量中,我们可以按如下方式修改我们的代码。

    def main():
        student = get_student()
        print(f"{student[0]} from {student[1]}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return (name, house)
    
    if __name__ == "__main__":
        main() 
    

    注意(name, house)明确地告诉阅读我们代码的人,我们在一个返回值中返回两个值。此外,注意我们如何使用student[0]student[1]来索引tuple

  • tuple是不可变的,这意味着我们无法更改这些值。不可变性是我们进行防御性编程的一种方式。

    def main():
        student = get_student()
        if student[0] == "Padma":
            student[1] = "Ravenclaw"
        print(f"{student[0]} from {student[1]}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return name, house
    
    if __name__ == "__main__":
        main() 
    

    注意,这段代码会产生错误。由于tuple是不可变的,我们无法重新分配student[1]的值。

  • 如果我们想要给其他程序员提供灵活性,我们可以使用list如下。

    def main():
        student = get_student()
        if student[0] == "Padma":
            student[1] = "Ravenclaw"
        print(f"{student[0]} from {student[1]}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return [name, house]
    
    if __name__ == "__main__":
        main() 
    

    注意列表是可变的。也就是说,housename的顺序可以被程序员切换。你可能会决定在某些需要提供更多灵活性但以代码安全性为代价的情况下使用它。毕竟,如果这些值的顺序可以更改,与你一起工作的程序员可能会在将来犯错误。

  • 在这个实现中也可以使用字典。回想一下,字典提供键值对。

    def main():
        student = get_student()
        print(f"{student['name']} from {student['house']}")
    
    def get_student():
        student = {}
        student["name"] = input("Name: ")
        student["house"] = input("House: ")
        return student
    
    if __name__ == "__main__":
        main() 
    

    注意在这个例子中,返回了两个键值对。这种方法的优点是我们可以使用键来索引这个字典。

  • 尽管如此,我们的代码还可以进一步改进。请注意,存在一个不必要的变量。我们可以移除 student = {},因为我们不需要创建一个空字典。

    def main():
        student = get_student()
        print(f"{student['name']} from {student['house']}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return {"name": name, "house": house}
    
    if __name__ == "__main__":
        main() 
    

    注意我们可以在 return 语句中使用 {} 大括号来创建字典并在同一行返回它。

  • 我们可以在我们的代码字典版本中为 Padma 提供一个特殊案例。

    def main():
        student = get_student()
        if student["name"] == "Padma":
            student["house"] = "Ravenclaw"
        print(f"{student['name']} from {student['house']}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return {"name": name, "house": house}
    
    if __name__ == "__main__":
        main() 
    

    注意,与之前代码的迭代类似,我们可以利用键名来索引我们的学生字典。

  • 在面向对象编程中,类提供了一种创建我们自己的数据类型并为其命名的方法。

  • 类就像是一种数据类型的模具——在那里我们可以发明我们自己的数据类型并为其命名。

  • 我们可以按照以下方式修改我们的代码来实现我们自己的名为 Student 的类:

    class Student:
        ...
    
    def main():
        student = get_student()
        print(f"{student.name} from {student.house}")
    
    def get_student():
        student = Student()
        student.name = input("Name: ")
        student.house = input("House: ")
        return student
    
    if __name__ == "__main__":
        main() 
    

    注意按照惯例,Student 是大写的。进一步,注意 ... 简单地意味着我们将在稍后返回并完成代码的这一部分。进一步,注意在 get_student 中,我们可以使用语法 student = Student() 创建一个 Student 类的 student。进一步,注意我们利用“点表示法”来访问这个 student 变量的属性。

  • 任何时候你创建一个类并利用这个蓝图来创建东西,你就创建了一个“对象”或“实例”。在我们的代码中,student 是一个对象。

  • 此外,我们可以为期望在类 Student 的对象内部拥有的属性打下一些基础。我们可以按照以下方式修改我们的代码:

    class Student:
        def __init__(self, name, house):
            self.name = name
            self.house = house
    
    def main():
        student = get_student()
        print(f"{student.name} from {student.house}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        student = Student(name, house)
        return student
    
    if __name__ == "__main__":
        main() 
    

    注意在 Student 中,我们标准化了这个类的属性。我们可以在 class Student 中创建一个函数,称为“方法”,它决定了类 Student 的对象的行为。在这个函数中,它接收传递给它的 namehouse 并将这些变量分配给这个对象。进一步,注意构造函数 student = Student(name, house)Student 类中调用这个函数并创建一个 studentself 指的是刚刚创建的当前对象。

  • 我们可以将代码简化如下:

    class Student:
        def __init__(self, name, house):
            self.name = name
            self.house = house
    
    def main():
        student = get_student()
        print(f"{student.name} from {student.house}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return Student(name, house)
    
    if __name__ == "__main__":
        main() 
    

    注意 return Student(name, house) 如何简化了我们之前代码中的迭代,其中构造函数语句单独占一行。

  • 你可以在 Python 的文档中了解更多信息。

raise

  • 面向对象编程鼓励你将类的所有功能封装在类定义中。如果出了问题怎么办?如果有人输入了随机的数据怎么办?如果有人试图创建一个没有名字的学生怎么办?请按照以下方式修改你的代码:

    class Student:
        def __init__(self, name, house):
            if not name:
                raise ValueError("Missing name")
            if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]:
                raise ValueError("Invalid house")
            self.name = name
            self.house = house
    
    def main():
        student = get_student()
        print(f"{student.name} from {student.house}")
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return Student(name, house)
    
    if __name__ == "__main__":
        main() 
    

    注意我们现在检查是否提供了名字并且指定了合适的宿舍。结果证明我们可以创建自己的异常,通过 raise 通知程序员用户可能创建的错误。在上面的例子中,我们使用特定的错误消息引发 ValueError

  • 碰巧的是,Python 允许你创建一个特定的函数,通过它可以打印对象的属性。按照以下方式修改你的代码:

    class Student:
        def __init__(self, name, house, patronus):
            if not name:
                raise ValueError("Missing name")
            if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]:
                raise ValueError("Invalid house")
            self.name = name
            self.house = house
            self.patronus = patronus
    
        def __str__(self):
            return f"{self.name} from {self.house}"
    
    def main():
        student = get_student()
        print(student)
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        patronus = input("Patronus: ")
        return Student(name, house, patronus)
    
    if __name__ == "__main__":
        main() 
    

    注意 def __str__(self) 提供了一种在调用时返回学生的方式。因此,现在作为程序员,你可以打印对象、其属性或与该对象相关的几乎所有内容。

  • __str__ 是 Python 类自带的一个内置方法。碰巧的是,我们也可以为类创建自己的方法!按照以下方式修改你的代码:

    class Student:
        def __init__(self, name, house, patronus=None):
            if not name:
                raise ValueError("Missing name")
            if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]:
                raise ValueError("Invalid house")
            if patronus and patronus not in ["Stag", "Otter", "Jack Russell terrier"]:
                raise ValueError("Invalid patronus")
            self.name = name
            self.house = house
            self.patronus = patronus
    
        def __str__(self):
            return f"{self.name} from {self.house}"
    
        def charm(self):
            match self.patronus:
                case "Stag":
                    return "🐴"
                case "Otter":
                    return "🦦"
                case "Jack Russell terrier":
                    return "🐶"
                case  _:
                    return "🪄"
    
    def main():
        student = get_student()
        print("Expecto Patronum!")
        print(student.charm())
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        patronus = input("Patronus: ") or None
        return Student(name, house, patronus)
    
    if __name__ == "__main__":
        main() 
    

    注意我们定义了自己的方法 charm。与字典不同,类可以有内置的函数,称为方法。在这种情况下,我们定义了 charm 方法,其中特定的案例有特定的结果。此外,注意 Python 有能力在我们的代码中直接使用表情符号。

  • 在继续前进之前,让我们移除我们的守护神代码。按照以下方式修改你的代码:

    class Student:
        def __init__(self, name, house):
            if not name:
                raise ValueError("Invalid name")
            if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]:
                raise ValueError("Invalid house")
            self.name = name
            self.house = house
    
        def __str__(self):
            return f"{self.name} from {self.house}"
    
    def main():
        student = get_student()
        student.house = "Number Four, Privet Drive"
        print(student)
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return Student(name, house)
    
    if __name__ == "__main__":
        main() 
    

    注意我们只有两个方法:__init____str__

装饰器

  • 属性可以被用来加固我们的代码。在 Python 中,我们使用以 @ 开头的函数“装饰器”来定义属性。按照以下方式修改你的代码:

    class Student:
        def __init__(self, name, house):
            if not name:
                raise ValueError("Invalid name")
            self.name = name
            self.house = house
    
        def __str__(self):
            return f"{self.name} from {self.house}"
    
        # Getter for house
        @property
        def house(self):
            return self._house
    
        # Setter for house
        @house.setter
        def house(self, house):
            if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]:
                raise ValueError("Invalid house")
            self._house = house
    
    def main():
        student = get_student()
        print(student)
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return Student(name, house)
    
    if __name__ == "__main__":
        main() 
    

    注意我们是如何在名为 house 的函数上方写上 @property 的。这样做定义了 house 为我们类的一个属性。有了 house 属性,我们就能定义如何设置和检索我们类的一些属性,例如 _house。确实,我们现在可以通过 @house.setter 定义一个名为“setter”的函数,每当设置 house 属性时都会被调用——例如,使用 student.house = "Gryffindor"。在这里,我们让我们的 setter 为我们验证 house 的值。注意,如果 house 的值不是哈利·波特的任何一个学院,我们会抛出一个 ValueError,否则我们会使用 house 更新 _house 的值。为什么是 _house 而不是 househouse 是我们类的一个属性,用户通过它尝试设置我们的类属性。_house 是那个类属性本身。前导下划线 _ 表示用户不需要(实际上也不应该!)直接修改这个值。_house 应该通过 house setter 来设置。注意 house 属性只是简单地返回 _house 的值,这是我们通过 house setter 可能已经验证过的类属性。当用户调用 student.house 时,他们通过我们的 house “getter” 获取 _house 的值。

  • 除了房子的名字,我们还可以保护我们学生的名字。按照以下方式修改你的代码:

    class Student:
        def __init__(self, name, house):
            self.name = name
            self.house = house
    
        def __str__(self):
            return f"{self.name} from {self.house}"
    
        # Getter for name
        @property
        def name(self):
            return self._name
    
        # Setter for name
        @name.setter
        def name(self, name):
            if not name:
                raise ValueError("Invalid name")
            self._name = name
    
        @property
        def house(self):
            return self._house
    
        @house.setter
        def house(self, house):
            if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]:
                raise ValueError("Invalid house")
            self._house = house
    
    def main():
        student = get_student()
        print(student)
    
    def get_student():
        name = input("Name: ")
        house = input("House: ")
        return Student(name, house)
    
    if __name__ == "__main__":
        main() 
    

    注意,和之前的代码类似,我们为名称提供了 getter 和 setter。

  • 你可以在 Python 的 方法 文档中了解更多信息。

连接到本课程中的先前工作

  • 尽管在课程的前几部分没有明确说明,但你一直在使用类和对象。

  • 如果你深入研究int的文档,你会发现它是一个具有构造函数的类。它是创建int类型对象的蓝图。你可以在 Python 的int文档中了解更多信息,链接为Python 的int文档

  • 字符串也是一个类。如果你使用过str.lower(),你就是在使用str类中的方法。你可以在 Python 的str文档中了解更多信息,链接为Python 的str文档

  • list也是一个类。查看list的文档,你可以看到其中包含的方法,如list.append()。你可以在 Python 的list文档中了解更多信息,链接为Python 的list文档

  • dict也是 Python 中的一个类。你可以在 Python 的dict文档中了解更多信息,链接为Python 的dict文档

  • 要了解你一直是如何使用类的,请打开你的控制台,输入code type.py,然后按照以下方式编写代码:

    print(type(50)) 
    

    注意,通过执行这段代码,它将显示50的类是int

  • 我们也可以将此应用于str,如下所示:

    print(type("hello, world")) 
    

    注意,执行这段代码将表明这是str类。

  • 我们也可以按照以下方式应用于list

    print(type([])) 
    

    注意,执行这段代码将表明这是list类。

  • 我们也可以使用 Python 内置的list类的名称来应用于list,如下所示:

    print(type(list())) 
    

    注意,执行这段代码将表明这是list类。

  • 我们也可以将此应用于dict,如下所示:

    print(type({})) 
    

    注意,执行这段代码将表明这是dict类。

  • 我们也可以使用 Python 内置的dict类的名称来应用于dict,如下所示:

    print(type(dict())) 
    

    注意,执行这段代码将表明这是dict类。

类方法

  • 有时候,我们希望给类本身添加功能,而不是给该类的实例添加。

  • @classmethod是一个函数,我们可以用它来给整个类添加功能。

  • 这里是一个使用类方法的例子。在你的终端窗口中,输入code hat.py并按照以下方式编写代码:

    import random
    
    class Hat:
        def __init__(self):
            self.houses = ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]
    
        def sort(self, name):
            print(name, "is in", random.choice(self.houses))
    
    hat = Hat()
    hat.sort("Harry") 
    

    注意,当我们把学生的名字传递给排序帽子时,它会告诉我们学生被分配到了哪个学院。注意hat = Hat()实例化了hatsort功能始终由类的实例处理。通过执行hat.sort("Harry"),我们向Hat的特定实例的sort方法传递了学生的名字,我们称之为hat

  • 然而,我们可能希望运行sort函数而不创建特定的排序帽子实例(毕竟只有一个)。我们可以修改我们的代码如下:

    import random
    
    class Hat:
    
        houses = ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]
    
        @classmethod
        def sort(cls, name):
            print(name, "is in", random.choice(cls.houses))
    
    Hat.sort("Harry") 
    

    注意到 __init__ 方法被移除,因为我们不需要在我们的代码中的任何地方实例化一顶帽子。因此,self 就不再相关,并被移除。我们指定这个 sort 为一个 @classmethod,用 cls 替换 self。最后,注意在代码的末尾,根据惯例,Hat 被大写,因为这是我们的类名。

  • 返回到 students.py,我们可以修改我们的代码如下,解决一些与 @classmethod 相关的遗漏机会:

    class Student:
        def __init__(self, name, house):
            self.name = name
            self.house = house
    
        def __str__(self):
            return f"{self.name} from {self.house}"
    
        @classmethod
        def get(cls):
            name = input("Name: ")
            house = input("House: ")
            return cls(name, house)
    
    def main():
        student = Student.get()
        print(student)
    
    if __name__ == "__main__":
        main() 
    

    注意到 get_student 被移除,并创建了一个名为 get@classmethod。现在,可以调用此方法而无需首先创建一个学生。

静态方法

  • 结果表明,除了与实例方法不同的 @classmethod 之外,还有其他类型的函数。

  • 使用 @staticmethod 可能是你希望探索的事情。虽然本课程没有明确涵盖,但你欢迎去学习更多关于静态方法和它们与类方法的区别。

继承

  • 继承可能是面向对象编程中最强大的特性。

  • 恰好可以创建一个“继承”其他类的方法、变量和属性的类。

  • 在终端中,执行 code wizard.py。编写如下代码:

    class Wizard:
        def __init__(self, name):
            if not name:
                raise ValueError("Missing name")
            self.name = name
    
        ...
    
    class Student(Wizard):
        def __init__(self, name, house):
            super().__init__(name)
            self.house = house
    
        ...
    
    class Professor(Wizard):
        def __init__(self, name, subject):
            super().__init__(name)
            self.subject = subject
    
        ...
    
    wizard = Wizard("Albus")
    student = Student("Harry", "Gryffindor")
    professor = Professor("Severus", "Defense Against the Dark Arts")
    ... 
    

    注意到有一个名为 Wizard 的类和一个名为 Student 的类。此外,还有一个名为 Professor 的类。学生和教授都有名字。学生和教授都是巫师。因此,StudentProfessor 继承了 Wizard 的特性。在“子”类 Student 中,Student 可以从“父”或“超”类 Wizard 继承,如 super().__init__(name) 运行 Wizardinit 方法。最后,注意代码的最后几行创建了一个名为 Albus 的巫师,一个名为 Harry 的学生,等等。

继承和异常

  • 虽然我们刚刚介绍了继承,但我们一直在使用异常时使用它。

  • 恰好异常有一个层次结构,其中包含子类、父类和祖父母类。这些在下图中展示:

    BaseException
     +-- KeyboardInterrupt
     +-- Exception
          +-- ArithmeticError
          |    +-- ZeroDivisionError
          +-- AssertionError
          +-- AttributeError
          +-- EOFError
          +-- ImportError
          |    +-- ModuleNotFoundError
          +-- LookupError
          |    +-- KeyError
          +-- NameError
          +-- SyntaxError
          |    +-- IndentationError
          +-- ValueError
     ... 
    
  • 你可以在 Python 的 异常 文档中了解更多信息。

运算符重载

  • 一些运算符,如 +-,可以被“重载”,以便它们可以拥有超出简单算术的更多能力。

  • 在你的终端窗口中,输入 code vault.py。然后,编写如下代码:

    class Vault:
        def __init__(self, galleons=0, sickles=0, knuts=0):
            self.galleons = galleons
            self.sickles = sickles
            self.knuts = knuts
    
        def __str__(self):
            return f"{self.galleons} Galleons, {self.sickles} Sickles, {self.knuts} Knuts"
    
        def __add__(self, other):
            galleons = self.galleons + other.galleons
            sickles = self.sickles + other.sickles
            knuts = self.knuts + other.knuts
            return Vault(galleons, sickles, knuts)
    
    potter = Vault(100, 50, 25)
    print(potter)
    
    weasley = Vault(25, 50, 100)
    print(weasley)
    
    total = potter + weasley
    print(total) 
    

    注意到 __str__ 方法返回一个格式化的字符串。此外,注意到 __add__ 方法允许两个保险库值的相加。self+ 运算符左侧的内容。other+ 运算符右侧的内容。

  • 你可以在 Python 的 运算符重载 文档中了解更多信息。

总结

现在,你已经通过面向对象编程学习了一个全新的能力级别。

  • 面向对象编程

  • raise

  • 类方法

  • 静态方法

  • 继承

  • 运算符重载

posted @ 2025-11-08 11:25  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报