Fluent Python2 【Chapter8_QA】

1. pytest.mark.parametrize() 该装饰器的作用是什么?

pytest.mark.parametrize是一个pytest框架中的装饰器,它的作用是允许你用不同的参数集多次运行同一个测试函数,从而实现参数化测试

通俗解释:

假设你正在测试一个计算平方的函数square(x)。你可以编写一个测试用例来检查输入2时输出是否正确,另一个测试用例检查输入3时是否正确,依此类推。

但是,使用parametrize装饰器, 你可以将这些测试用例合并成一个, 并且使用不同的输入参数多次执行同一个测试函数。

举例说明: 

import pytest

def square(x):
    return x ** 2

@pytest.mark.parametrize("input_x, expected", [
    (2, 4),
    (3, 9),
    (4, 16),
    (-2, 4),
])
def test_square(input_x, expected):
    assert square(input_x) == expected

在这个例子中: 

  1. 我们定义了一个square函数,用于计算平方。
  2. 然后使用@pytest.mark.parametrize装饰器修饰了test_square函数。
  3. parametrize的第一个参数是测试函数中的参数名称,在这里是"input_x, expected"
  4. 第二个参数是一个列表,每个元素都是一个元组,包含了要传递给测试函数的参数值。在这里,我们为不同的输入值x以及对应的期望输出expected提供了4组参数。
  5. test_square函数中,我们使用assert语句检查square(input_x)的结果是否等于expected

当运行这个测试用例时,pytest会自动使用提供的4组参数分别执行test_square函数4次。如果任何一次执行失败,测试用例就会被标记为失败。

使用parametrize的好处是:

  1. 节省了编写重复测试用例的工作
  2. 使测试用例更加紧凑,易于维护
  3. 能够测试更多的输入场景,从而增强测试覆盖率

总之,pytest.mark.parametrize让你可以用不同的参数输入多次执行同一个测试函数,从而实现参数化测试,提高了测试的效率和质量。

总结:这个装饰器是 pytest.mark.parametrize,它的作用是为测试函数提供参数化的能力,即允许在单个测试函数中多次运行,并对每次运行使用不同的参数组合。

 

2. 如下代码的理解

from pytest import mark

from messages import show_count

@mark.parametrize('qty, expected', [
    (1, '1 part'),
    (2, '2 parts'),
])
def test_show_count(qty, expected):
    got = show_count(qty, 'part')
    assert got == expected

def test_show_count_zero():
    got = show_count(0, 'part')
    assert got == 'no parts'

这段代码使用了Python测试框架pytest中的一个装饰器@mark.parametrize。它的作用是为同一个测试函数提供多组不同的输入参数和预期输出,从而实现数据驱动测试。

具体来说:

  1. @mark.parametrize是pytest框架提供的一种参数化装饰器,用于为测试函数提供不同的测试数据集。
  2. ('qty', 'expected')指定了要参数化的参数名称。
  3. [(1, '1 part'), (2, '2 parts')]是一个列表,包含了多组测试数据,每一组测试数据都是一个元组,对应着qtyexpected两个参数的值。
  4. 装饰器下面的test_show_count函数就是要测试的函数,它将使用参数化的数据进行测试。对于列表中的每一组测试数据,pytest将自动调用一次test_show_count函数,将对应的qtyexpected值传入。
  5. test_show_count函数内部,它调用了show_count(qty, 'part')函数,得到实际结果got,然后使用assert got == expected断言实际结果是否与预期结果相符。
  6. test_show_count_zero是另一个测试用例,它测试当qty=0时,show_count函数的行为是否正确。

通过参数化测试,可以用少量代码覆盖多种测试场景,提高了测试的效率和质量。这种数据驱动的方式特别适用于需要测试不同输入数据的函数或方法。

 

3. 那'qty, expected'是什么意思呢?

答:'qty, expected' 是参数化装饰器 @pytest.mark.parametrize 中的第一个参数,它表示测试函数的参数列表。在这个参数列表中,以逗号分隔的每个元素都代表一个参数,这些参数将在测试函数的每次运行中被赋予不同的值。

在示例中,'qty, expected' 表示测试函数 test_show_count 接受两个参数:qtyexpected。这两个参数将会在测试函数的每次运行中被赋予不同的值,这些值是由装饰器提供的参数组合确定的。

 

4. 那为什么这里只管test_show_count()而丝毫没提到test_show_count_zero()呢?

@pytest.mark.parametrize 装饰器应用在 test_show_count() 函数上,因为它是直接跟在装饰器后面的函数。在这种情况下,装饰器所提供的参数化数据只会应用到紧随其后的测试函数上,而不会影响其他函数。

test_show_count_zero() 函数虽然也是一个测试函数,但它没有紧跟在 @pytest.mark.parametrize 装饰器后面,因此不会受到参数化装饰器的影响。相反,它是一个独立的测试函数,需要自行提供参数和断言。

如果两个函数都要测试,就应应该改成下面这种写法

from pytest import mark
from message import show_count

@mark.parametrize('qty, expected', [
    (1, '1 part'),
    (2, '2 parts'),
])

def test_show_count(qty: int, expected: str) -> None:
    got = show_count(qty, 'part')
    assert got == expected


@mark.parametrize('qty, expected', [  # 一个装饰器仅作用于紧跟其后的函数
    (0, 'no part'),
    (2, '2 parts'),
])

def test_show_count_zero(qty: int, expected: str) -> None:
    got = show_count(0, 'part')
    assert got == 'no parts'

 

5. 那上述案例的 --disallow-untyped-defs 以及 --disallow-incomplete-defs的含义是什么?还有其他哪些常用参数?

--disallow-untyped-defs--disallow-incomplete-defs mypy 命令行工具的选项,用于控制类型检查的行为。

  • --disallow-untyped-defs:表示禁止未标记类型的函数和方法。如果使用了此选项,mypy 将会对所有函数和方法的参数和返回值进行类型检查,而不允许缺少类型注释。
  • --disallow-incomplete-defs:表示禁止不完整的类型注释。如果使用了此选项,mypy 将会对所有函数和方法的类型注释进行严格检查,不允许使用不完整或不正确的类型注释。

除了这两个选项外,mypy 还有一些其他常用的选项,例如:

  • --strict:启用严格模式,等同于设置 --disallow-untyped-defs--disallow-incomplete-defs 和其他一些选项。
  • --ignore-missing-imports:忽略缺少的导入,允许类型检查在找不到导入的情况下继续进行。
  • --warn-unused-ignores:警告未使用的类型忽略。
  • --warn-redundant-casts:警告冗余的类型转换。
  • --check-untyped-defs:检查未标记类型的函数和方法,但不会中止类型检查。

 

6. 如何理解 “为什么越一般的类型,接口越狭隘,即支持的操作越少。比如object类是新的操作比abc.Sequence少,abc.Sequence实现的操作比abc.MutableSequence少,abc.MutableSequence实现的操作比list更少?”

答:在 Python 中,越通用的类型往往提供了更少的操作,这是因为它们是更抽象的概念,不会对具体的实现进行假设。具体来说:

  1. object 类是 Python 中所有类的基类,它提供了最基本的功能,比如 __init____repr__,但不包含任何特定于容器或序列的操作,因为它并不假设对象具有容器或序列的特性。

  2. abc.Sequence 是一个抽象基类,它定义了序列对象的基本行为,比如可以通过索引访问元素、计算长度等。但是,它并没有具体指定如何实现这些行为,因此实现 abc.Sequence 的类需要提供这些操作的具体实现。

  3. abc.MutableSequenceabc.Sequence 的子类,它除了具有 abc.Sequence 的所有行为外,还提供了一些修改序列的方法,比如 appendinsertpop。因此,实现了 abc.MutableSequence 的类可以在序列中进行元素的添加、删除和修改等操作。

  4. list 类是 Python 中内置的具体序列类型,它实现了 abc.MutableSequence 接口,因此提供了所有 abc.MutableSequence 中定义的操作,同时还可以通过列表特有的方法进行操作,比如 extendremove 等。

总的来说,抽象基类提供了一种规范,定义了一组操作和行为,而具体的实现则可以根据需要选择性地实现这些操作,并可以额外提供自己的特定功能。这样的设计使得 Python 的类型系统更加灵活,可以适应各种不同的使用场景。

 

7. python中有个渐进式类型系统,为什么要取这么个名字?

答:"渐进式类型系统"(Progressive Typing System)这个术语可能有些让人困惑,因为它与传统的静态类型和动态类型系统有所不同。这个术语的含义主要是指在 Python 中可以逐步引入类型提示,从而使代码的类型更加明确,从而提供更好的类型检查和代码质量。这种类型系统的特点包括:

  1. 渐进性:指的是可以逐步地、逐步地在代码中引入类型提示,而不必一次性地为所有代码添加类型注释。这使得开发人员可以根据需要,逐步改进代码的类型安全性和可维护性,而不会对现有代码造成太大的影响。

  2. 灵活性:Python 的类型提示系统相对灵活,允许在需要时为函数、变量等添加类型提示,但并不强制要求每个部分都提供类型注释。这使得开发人员可以根据实际情况选择是否使用类型提示,以及何时以何种方式使用。

  3. 类型检查:虽然 Python 是一种动态类型语言,但可以使用类型检查工具(如 mypy)来对代码进行静态类型检查,以发现潜在的类型错误和一致性问题。这种类型检查是可选的,可以在开发过程中或在持续集成中启用,以提高代码的质量和可靠性。

总的来说,"渐进式类型系统"这个术语强调了在 Python 中引入类型提示的渐进性和灵活性,以及通过类型检查工具提供的类型检查功能,从而提高了代码的可维护性和可靠性。

 

8. from typing import Protocol, 如何理解这个Protocol?

from typing import Protocol                    # <1>

class GooseLike(Protocol):
    def honk(self, times: int) -> None: ...    # <2>
    def swim(self) -> None: ...


def alert(waterfowl: GooseLike) -> None:       # <3>
    waterfowl.honk(2)

Protocol 是Python中引入的一种类型注释形式,可以用于定义某个类或函数应该实现的接口。它的作用是为类型检查提供一种灵活和可扩展的方式,同时也是一种非常有用的文档化工具。

下面让我们通过一个例子来理解 Protocol 的概念和作用:

  1. from typing import Protocol 这一行导入了 Protocol 类型,它来自 Python 的标准库中的 typing 模块。
  2. class GooseLike(Protocol): 这里定义了一个名为 GooseLike 的 Protocol,它指定了任何实现了这个 Protocol 的类都必须实现 honk 和 swim 两个方法。Protocol 本身不定义任何实现,只是作为一个接口或契约的规范。
  3. def alert(waterfowl: GooseLike) -> None: 在这个函数中,参数 waterfowl 的类型被注释为 GooseLike。这意味着传递给 alert 函数的对象必须实现 GooseLike Protocol 中定义的方法,否则类型检查器会发出警告或错误。
  4. waterfowl.honk(2) 在函数体内,我们调用了 waterfowl 对象的 honk 方法,因为它是 GooseLike Protocol 规定的一部分。

现在让我们用一个通俗的例子来解释 Protocol:

想象一下,你正在开发一款模拟游戏,其中包括各种鸟类。不同的鸟类有不同的行为,比如鹅会"嘎嘎"叫、会游泳等。为了让游戏中的各种鸟类都能正常运行,你需要确保它们都实现了一些基本的行为,比如"嘎嘎"叫和游泳。

这时,你可以定义一个 BirdLike Protocol,它规定了任何实现这个 Protocol 的类都必须有 honkswim 两个方法。这样,无论你后续添加什么样的鸟类,只要它们都实现了 BirdLike Protocol,你就可以确保它们具有基本的行为,游戏也不会因为某些鸟类缺少必要的方法而崩溃。

而且,因为 Protocol 只是一个接口规范,它不包含任何实现细节,所以你可以自由地为每种鸟类定制不同的行为实现,比如不同的"嘎嘎"叫声或游泳方式。

总之,Protocol 为你提供了一种灵活且可扩展的方式来定义接口,确保代码的一致性和可维护性,同时也为其他开发人员提供了很好的文档支持。

 

下面通过一个具体的例子来说明 Protocol 的作用

假设我们正在开发一个农场模拟游戏,其中包括不同种类的动物。我们定义一个 FarmAnimal Protocol,规定任何实现这个 Protocol 的类都必须实现 make_soundfeed 两个方法。

from typing import Protocol

class FarmAnimal(Protocol):
    def make_sound(self) -> str:
        """返回动物的叫声"""
        ...

    def feed(self, food: str) -> None:
        """喂食动物"""
        ...

现在,我们可以定义一些具体的动物类,并让它们实现 FarmAnimal Protocol:

class Cow:
    def make_sound(self) -> str:
        return "Moo"

    def feed(self, food: str) -> None:
        print(f"Eating {food}...")

class Sheep:
    def make_sound(self) -> str:
        return "Baa"

    def feed(self, food: str) -> None:
        print(f"Munching on {food}...")

接下来,我们可以定义一个函数,它接受一个 FarmAnimal 对象作为参数,并调用它的 make_soundfeed 方法:

def interact_with_animal(animal: FarmAnimal) -> None:
    print(f"The animal says: {animal.make_sound()}")
    animal.feed("Hay")

现在,我们可以创建不同类型的动物对象,并将它们传递给 interact_with_animal 函数:

 

在这个例子中,FarmAnimal Protocol 定义了一个接口,规定了任何实现它的类都必须实现 make_soundfeed 方法。CowSheep 类都实现了这个 Protocol,因此它们可以被传递给 interact_with_animal 函数,而不会导致类型错误。

如果我们试图传递一个没有实现 FarmAnimal Protocol 的对象给 interact_with_animal 函数,类型检查器会发出警告或错误:

class Dog:
    def bark(self) -> str:
        return "Woof"

dog = Dog()
interact_with_animal(dog)  # 类型错误: "Dog" object has no attribute "make_sound"

通过这个例子,我们可以看到 Protocol 如何为我们提供了一种灵活且可扩展的方式来定义接口,确保代码的一致性和可维护性。它还为其他开发人员提供了很好的文档支持,使他们更容易理解代码的期望行为。

 

9. Callable如何理解?

from collections.abc import Callable 

def update(  # <1>
        probe: Callable[[], float],  # <2>
        display: Callable[[float], None]  # <3>
    ) -> None:
    temperature = probe()
    # imagine lots of control code here
    display(temperature)

def probe_ok() -> int:  # <4>
    return 42

def display_wrong(temperature: int) -> None:  # <5>
    print(hex(temperature))

update(probe_ok, display_wrong)  # type error  # <6>

def display_ok(temperature: complex) -> None:  # <7>
    print(temperature)

update(probe_ok, display_ok)  # OK  # <8>
这一行导入了 Callable 类型,它来自 Python 的标准库中的 collections.abc 模块。Callable 类型用于注释函数类型。
from collections.abc import Callable
  1. 这里定义了一个名为 update 的函数。
  2. probe 参数的类型被注释为 Callable[[], float]。这意味着 probe 必须是一个无参数的可调用对象(例如函数),且返回值类型为 float
  3. display 参数的类型被注释为 Callable[[float], None]。这意味着 display 必须是一个接受一个 float 类型参数并且没有返回值的可调用对象。
def update(
    probe: Callable[[], float], # <2>
    display: Callable[[float], None] # <3>
) -> None:

这部分代码调用了 probe 函数并将其返回值赋给 temperature 变量。然后,它假设有一些控制代码执行。最后,它调用 display 函数,并将 temperature 作为参数传递给它。

temperature = probe()
# imagine lots of control code here
display(temperature)

这里定义了一个名为 probe_ok 的函数,它返回一个整数 42。但是,它的返回值类型与 update 函数期望的 float 类型不匹配。

def probe_ok() -> int: # <4>
    return 42

这里定义了一个名为 display_wrong 的函数,它接受一个 int 类型的参数。但是,它的参数类型与 update 函数期望的 float 类型不匹配。

def display_wrong(temperature: int) -> None: # <5>
    print(hex(temperature))

这里尝试调用 update 函数,并将 probe_ok 和 display_wrong 作为参数传递给它。但是,由于它们的类型与 update 函数期望的类型不匹配,因此会引发类型错误。

update(probe_ok, display_wrong) # type error # <6>

这里定义了一个名为 display_ok 的函数,它接受一个 complex 类型的参数。虽然它的参数类型与 update 函数期望的 float 类型不匹配,但由于 Python 的duck typing特性,它仍然可以被接受。

def display_ok(temperature: complex) -> None: # <7>
    print(temperature)

这里调用 update 函数,并将 probe_ok 和 display_ok 作为参数传递给它。尽管 probe_ok 的返回值类型与 update 函数期望的类型不匹配,但由于 Python 的duck typing特性,该调用仍然可以成功执行。

update(probe_ok, display_ok) # OK # <8>

总的来说,这段代码展示了如何使用 Callable 类型注释来指定函数类型,以及在违反类型注释时会发生什么情况。它还说明了 Python 的duck typing特性,即只要对象的行为与期望相符,即使类型不完全匹配也可以被接受。

 

关于Callable的理解:

Callable 是 Python 中的一个类型提示工具,它来自于 typing 模块(在 Python 3.9 之前来自 collections.abc)。它的作用是用于注释函数类型,帮助静态类型检查器(如 mypy、pyright 等)了解该函数期望的参数类型和返回值类型。

使用 Callable 的主要原因和好处包括:

  1. 更好的代码可读性和文档化:通过显式注释函数签名,可以让开发者和其他阅读代码的人更容易理解该函数的用途和预期输入输出。
  2. 静态类型检查:配合类型检查器使用,可以在编译时而不是运行时捕获类型相关的错误,提高代码质量和健壮性。
  3. IDE 支持:现代 IDE 如 PyCharm、VSCode 等可以通过类型注释提供更好的代码补全、导航和重构等功能。
  4. 函数多态:使用 Callable 可以有效支持高阶函数(接受函数作为参数或返回函数)的类型提示。

如果不使用 Callable,你只能使用 Python 内置的注释语法,如 def foo(callback: Callable) -> None: 就无法正确表达期望的函数类型。你可能需要在代码中添加更多注释来解释参数和返回类型,从而降低代码可读性。同时也会失去静态类型检查和 IDE 增强功能。

举个简单的例子:

from typing import Callable

def apply_operation(operation: Callable[[float, float], float], x: float, y: float) -> float:
    return operation(x, y)

def add(a: float, b: float) -> float:
    return a + b

result = apply_operation(add, 2.0, 3.0)  # Returns 5.0

在这个例子中:

  1. apply_operation 是一个高阶函数,它接受一个函数作为参数(operation)和两个浮点数(xy)作为参数。
  2. operation参数的类型被注释为 Callable[[float, float], float]。这种类型提示表明,operation应该是一个可调用对象(如函数),该可调用对象接受两个float类型的参数,并返回一个float类型的结果。
  3. xy参数的类型都被注释为float
  4. apply_operation函数内部通过调用operation(x, y)执行传入的可调用对象,并将结果返回。
  5. 在调用apply_operation函数时,我们将add函数作为第一个参数传递。add函数符合Callable[[float, float], float]的签名要求,因为它接受两个float参数并返回它们的和(也是一个float值)。
  6. apply_operation(add, 2.0, 3.0)的结果是5.0,因为add(2.0, 3.0)计算出2.0 + 3.0 = 5.0

这个例子展示了如何使用Callable类型提示来明确指定一个高阶函数所期望的回调函数的参数类型和返回值类型,从而增强代码的可读性和类型安全性。如果传入一个不符合签名要求的可调用对象,静态类型检查器就会发出警告或错误。

 

10. complex是数学里的复数类型吗?和float, int是什么关系,为什么这里可以通过duck typing

complex在Python中是表示复数的内置数据类型。

在Python中,complexfloatint都是不同的数字类型,它们之间存在一些关系和区别:

  1. int表示整数,如1、-5、0等。
  2. float表示浮点数,如3.14、-0.001、1e6等。
  3. complex表示复数,由一个实部(real part)和一个虚部(imaginary part)组成,如3+4j、-2-1j等,其中j或J表示虚数单位。

它们之间的关系是:

  • intfloat的子类型,也就是说每个int值也是一个float值。
  • complex包含了float作为其实部。

之所以在之前的例子中,display_ok函数接受complex类型的参数也能通过duck typing,是因为complex对象可以被打印输出,和float对象具有相同的行为。

具体来说,当您执行print(x)时:

  • 如果xfloatint类型,它会打印出该数值的十进制表示形式。
  • 如果xcomplex类型,它会打印出该复数的形式,如(3+4j)

因此,尽管complexfloat的类型不同,但它们在打印输出这一操作上具有相同的"能力"。所以当update函数调用display_ok(temperature)时,无论temperaturefloat还是complex,都可以被打印输出,从而满足display_ok函数的要求。

这就是Python中duck typing的体现 - 只要一个对象的行为与期望的行为相匹配(在本例中是可打印输出),那么它就可以被接受并使用,不管它的实际类型是什么。

总之,complexfloatint是Python中不同的数字类型,但是由于它们在某些操作上具有相同的行为,所以可以通过duck typing在这些操作中互相替换使用。

 

11. from typing import TypeVar,这个TypeVar的概念的理解。

简单来说,对于TypeVar的作用理解,记住泛型+占位即可。

TypeVar 是一个用于定义类型变量的工具,它允许你在编写类型提示时,使用一个变量来表示特定类型。这在定义泛型类或者函数时非常有用。

通俗解释:

想象一下,你正在写一个可以处理多种类型数据的函数或类,例如列表、元组或字典。由于你在编写代码时不知道确切的类型,所以无法为它们提供准确的类型提示。这时TypeVar就派上用场了,它可以让你使用一个变量作为占位符,暂时代表某种类型。

使用举例: 

from typing import TypeVar, List

T = TypeVar('T')  # 创建一个名为'T'的类型变量,这里要注意: The argument to 'TypeVar()' must be a string equal to the variable name to which it is assigned

def repeat(x: T, n: int) -> List[T]:
    """返回一个由n个x元素组成的列表"""
    return [x] * n

print(repeat(42, 4))        # [42, 42, 42, 42]
print(repeat("hello", 3))   # ['hello', 'hello', 'hello']

在这个例子中,我们定义了一个repeat函数,它接受一个任意类型的对象x和一个整数n,并返回一个由nx组成的列表。通过使用TypeVar('T')来表示未知的类型,我们可以很方便地编写支持任意类型的泛型函数。

为什么要有TypeVar?没有它会带来什么不便?

如果没有TypeVar,我们在编写泛型代码时就无法利用类型提示的好处了。因为类型提示需要具体的类型,而泛型代码正是因为事先不知道具体类型而存在的。TypeVar让我们可以暂时使用变量占位,等到实际使用时再确定具体类型。

没有TypeVar,我们就无法为泛型代码添加类型提示,从而失去了:

  1. 代码可读性: 没有类型提示,代码就很难看出参数和返回值的类型。
  2. 静态类型检查: 没有类型提示,就无法利用类型检查器(如mypy)发现潜在的类型错误。
  3. 自动化重构: 有些IDE和工具依赖类型注解来自动重构代码,没有注解就无法使用这些功能。

总之,TypeVar是Python为泛型编程和类型提示提供的重要工具,让我们能够更好地利用静态类型检查和IDE工具,从而编写出更加健壮和可维护的代码。

 

12. 同为占位符,3个点...和pass究竟有何区别?

from typing import Protocol, Any

class SupportsLessThan(Protocol):
    def __len__(self, other: Any) -> bool:
        ...

在上述的示例代码中,3个点(...)pass语句的用途和区别如下:

1) 3个点(...)

在Python中,3个点(...)通常被称为Ellipsis对象。在类型提示的上下文中,它用来表示该方法或函数的实现由具体的子类或子类型来提供,允许在定义抽象基类、协议或其他接口时省略方法体的具体实现。

在你的例子中,...表示在实现了SupportsLessThan协议的具体类型中,必须覆盖并提供__lt__方法的具体实现。...只是一个占位符,不是可执行的代码。

2) pass

pass是一个空操作,它在Python中是一个合法的语句,几乎可以在任何地方使用,但是什么也不做,只是一个占位符,使代码可以通过语法检查。

当你需要一个空的代码块时通常会使用pass,例如在创建最小类或方法定义时。它主要用作临时占位符,以便在以后填充实际的逻辑实现。

总的来说,...用于标记抽象方法或协议中需要由子类实现的部分,而pass可以用于需要临时占位的任何地方,因为它本身不执行任何操作。

 

下面通过具体的两个例子来说明...pass的区别:

1). 使用...(省略号)

from typing import Protocol

class MyProtocol(Protocol):
    def my_method(self, value: int) -> int:
        ...

class MyClass:
    def my_method(self, value: int) -> int:
        return value * 2

instance = MyClass()
print(instance.my_method(3)) # 输出: 6

在这个例子中,我们定义了一个MyProtocol协议,其中my_method方法的实现被...(省略号)代替。

这意味着任何实现MyProtocol的具体类都必须提供my_method的实现。

MyClass就是一个实现了MyProtocol的具体类,它覆盖了my_method方法并提供了具体的实现逻辑。

 

2). 使用pass

class MyClass:
    def my_method(self, value):
        pass  # 暂时什么也不做,以后再实现具体逻辑

    def another_method(self):
        if False:
            pass  # 这里什么也不做,只是为了满足语法要求

instance = MyClass()
instance.my_method(3)  # 什么也不会发生,因为方法体是空的

在这个例子中,pass语句用于两个地方:

  1. my_method中,pass只是一个占位符,表示该方法目前没有实现任何逻辑,但将来可能会添加具体的实现。
  2. another_method中,pass用于创建一个空代码块,以满足if语句的语法要求。

可以看出,...是在定义协议或抽象基类时使用的,表示需要由具体的子类或子类型来提供实现细节。而pass则是一个通用的占位符,可以用在任何需要一个合法但为空的代码块的地方,它本身不执行任何操作。

 

13. Any类型的理解

from typing import Protocol, Any

class SupportsLessThan(Protocol):  # <1>
    def __lt__(self, other: Any) -> bool: ...  # <2>

Any类型在这里的作用是让__lt__方法可以接受任何类型的参数。如果不使用Any类型,Python解释器会强制要求定义other参数的明确类型。这样做的好处是可以让代码更加灵活和通用。

具体来说:

  1. 类型检查和类型安全

使用Any类型意味着在运行时,任何类型的值都可以被传递给__lt__方法,而不会引发类型错误。这为编写通用代码提供了极大的灵活性。但同时也意味着放弃了静态类型检查所带来的类型安全保证。

  1. 与其他库/框架的兼容性

有些库函数的参数可能无法事先确定具体的类型,这时使用Any可以避免与这些库发生类型冲突。

  1. 未知类型

在某些情况下,程序在运行时可能需要处理未知类型的数据,例如从文件中读取。这时使用Any可以推迟对数据类型的检查。

  1. 启用 duck typing

Python支持 duck typing,即只要对象具有所需的方法或属性,就可以将其视为特定类型。Any类型允许您在不显式指定对象类型的情况下利用这一功能。

总的来说,Any的确会牺牲一些类型安全性, 但也提供了更大的灵活性。在编写通用库或处理未知类型的情况下, 使用Any是合理的。但在日常编码时,还是应该尽可能明确指定预期的类型,以确保代码的健壮性。

 

 

14. from typing import TYPE_CHECKING,TYPE_CHECKING的作用如何理解?

 

 

15. 如何理解如下代码的name_index()函数

import sys
import re
import unicodedata
from collections.abc import Iterator

RE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1


def tokenize(text: str) -> Iterator[str]:
    """return iterable of uppercase words"""
    for match in RE_WORD.finditer(text):
        yield match.group().upper()


def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:
    index: dict[str, set[str]] = {}
    for char in (chr(i) for i in range(start, end)):
        if name := unicodedata.name(char, ''):
            for word in tokenize(name):
                index.setdefault(word, set()).add(char)
    return index


index = name_index(32, 65)
print(sorted(index['SIGN']))  # ['#', '$', '%', '+', '<', '=', '>']
print(index['DIGIT'])  # {'2', '7', '4', '6', '3', '0', '9', '1', '8', '5'}
print(sorted(index['DIGIT']))  # ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
print(index['DIGIT'] & index['EIGHT'])  # {'8'}

对于name_index()函数的理解:

  1. 首先,函数定义了两个参数startend,用于指定需要处理的Unicode字符范围。默认情况下,start为32(空格字符的码位),endSTOP_CODE(一个大于最大Unicode码位的值)。同时,它使用类型注解指定了返回值为dict[str, set[str]],即一个字典,键为字符串,值为字符串集合。
  2. 接下来,通过一个生成器表达式(chr(i) for i in range(start, end))构建一个字符序列,遍历指定范围内的Unicode字符。
  3. 对于每个字符char,使用unicodedata.name(char, '')获取其Unicode名称。如果该字符有名称,则将名称赋值给变量name。这里使用了Python 3.8引入的赋值表达式语法(name := ...)。
  4. 如果name不为空,则使用之前定义的tokenize函数将名称分解为单词。tokenize函数返回一个生成器,因此我们使用for循环遍历每个单词。
  5. 对于每个单词word,使用字典的setdefault方法确保word在索引字典index中都有一个对应的集合。如果该单词的集合不存在,则创建一个新的空集合。然后,将当前字符char添加到相应的单词集合中。
  6. 最后,返回构建好的索引字典index

这个函数的作用是构建一个反向索引,将Unicode字符名称中的单词映射到包含该单词的字符集合。例如,如果字符'A'的名称是'LATIN CAPITAL LETTER A',那么在最终的索引字典中,键'LATIN'、'CAPITAL'、'LETTER'和'A'将各自对应一个集合,并且这些集合都将包含字符'A'。

通过这个索引,我们可以方便地查找包含特定单词的所有字符,或者根据一个字符找到它的名称中包含的所有单词。这在处理Unicode字符时可能很有用,比如搜索、过滤或生成与特定主题相关的字符等。

 

15. Union[float, Decimal, Fraction]这三种类型的数据有何异同

floatDecimalFraction 都是用于表示数值的类型,但它们在精度、表示方式和用途上存在一些区别。

  1. float

float 是 Python 内置的浮点数类型,用于表示带有小数部分的数值。它采用二进制浮点数表示法(IEEE 754)。

  • 优点:计算速度快,适合科学计算和工程计算等对性能要求较高的场景。
  • 缺点:由于使用二进制浮点数的近似值表示,可能会出现精度丢失或舍入误差等问题,例如 0.1 + 0.2 != 0.3
  1. Decimal

Decimal 是 Python 标准库中的一个类,用于表示精确的十进制数值。它采用十进制浮点数表示法,每一位数字都是精确存储的。

  • 优点:可以精确表示十进制数,避免浮点数的舍入误差,适合金融计算和需要高精度的场景。
  • 缺点:计算速度较慢,占用内存较多。
  1. Fraction

Fraction 也是 Python 标准库中的一个类,用于表示精确的分数。它以分子和分母的形式存储,可以精确表示有理数。

  • 优点:可以精确表示有理数,避免舍入误差。
  • 缺点:不能表示无理数,计算效率较低,占用内存较多。

总的来说,三者的主要区别如下:

  1. 表示方式:
    • float 采用二进制浮点数表示法
    • Decimal 采用十进制浮点数表示法
    • Fraction 采用分数表示法
  2. 精度:
    • float 存在舍入误差,精度有限
    • Decimal 和 Fraction 可以精确表示对应的数值,不存在舍入误差
  3. 适用场景:
    • float 适合科学计算和工程计算等对性能要求较高的场景
    • Decimal 适合金融计算和需要高精度的场景
    • Fraction 适合需要精确表示有理数的场景,如分数计算
  4. 性能:
    • float 计算速度最快
    • Decimal 次之,适中
    • Fraction 计算速度最慢

因此,在选择数值类型时,需要根据具体的应用场景和需求来权衡精度、性能和表示方式。

如果对精度要求不是很高,可以使用 float; 如果需要精确表示十进制数,可以使用 Decimal; 如果需要精确表示有理数,可以使用 Fraction

 

16. 如下代码,参数化签名是错在哪里?

from collections.abc import Iterable
from typing import TypeVar

T = TypeVar('T')
def mode(data: Iterable[T]) -> T:
    pass

在这段代码中,TypeVar('T')是正确的,但在函数mode的类型注解中,返回值的类型注解-> T是错误的。

问题在于,mode函数应该返回一个出现最多次的元素,而该元素的类型取决于输入序列data中元素的类型。因此,返回值的类型应该与T相同,而不应该直接使用T

正确的参数化签名应该是: 

from collections.abc import Iterable
from typing import TypeVar, Iterator

T = TypeVar('T')

def mode(data: Iterable[T]) -> Iterator[T]:
    pass

在这个修正后的版本中,函数mode返回一个迭代器,其中每个元素的类型都是T。这样,无论输入序列data中元素的具体类型是什么,函数mode都可以正确地处理和返回相应类型的元素。

总的来说,在使用TypeVar进行参数化时,需要仔细考虑返回值的实际类型,而不能直接使用TypeVar本身作为返回值的类型注解。

-> Iterator[T]-> T的主要区别在于:

  1. -> T表示函数返回单个元素, 该元素的类型为T
  2. -> Iterator[T]表示函数返回一个迭代器, 该迭代器可以产生多个类型为T的元素。

在这个例子中, mode函数的目的是找出给定序列中出现次数最多的元素。由于可能存在多个"众数"(出现次数并列最多的元素), 因此函数需要返回一个包含所有众数的迭代器,而不是单个元素

使用-> Iterator[T]作为返回类型注解有以下优点:

  1. 它允许函数返回0个、1个或多个元素,而不局限于只返回单个元素。
  2. 无论输入序列data中元素的具体类型是什么,mode函数都可以正确地处理和返回相同类型的元素。这是因为类型变量T会根据输入序列data的元素类型进行实例化。
  3. 使用迭代器返回结果更加灵活,可以让调用者决定如何处理返回的多个元素。

因此,-> Iterator[T]更加准确地描述了这个mode函数的预期行为,即返回一个包含所有众数元素的迭代器,而不是单个元素。这种返回类型注解比直接使用-> T更加明确且通用。

 

17. Iterator[Never]的理解

Iterator[Never] 虽然在实际编程中很少使用,但还是有一些适用的场景。下面通过代码示例来说明几种使用场景:

  1. 模拟无限循环

有时我们需要编写一个无限循环,比如一个后台服务程序。可以使用 Iterator[Never] 来模拟这种情况:

from typing import Iterator

def infinite_loop() -> Iterator[Never]:
    while True:
        # 执行一些重复的任务
        pass

# 在其他地方使用 infinite_loop
loop: Iterator[Never] = infinite_loop()
  1. 表示不应返回值的函数

如果一个函数不应该返回任何值(比如它只执行一些副作用操作),可以使用 Iterator[Never] 作为返回类型注释,以确保它永远不会返回值:

from typing import Iterator, NoReturn

def shutdown_system() -> NoReturn:
    """关闭系统,不应返回"""
    # 执行关机操作
    ...
    raise SystemExit(0)  # 或者其他异常

def run_forever() -> Iterator[Never]:
    """一个永不结束的后台任务"""
    while True:
        # 执行一些后台任务
        ...
  1. 表示异常情况

如果某个函数或方法可能会抛出异常而不返回值,可以使用 Iterator[Never] 表示这种异常情况:

from typing import Iterator

def parse_data(data: bytes) -> Iterator[int]:
    for byte in data:
        if not 0 <= byte < 256:
            raise ValueError("Invalid byte value")
        yield byte

def parse_invalid_data() -> Iterator[Never]:
    data = b"\x90\xfe"  # 包含无效字节
    return parse_data(data)  # 类型为 Iterator[Never]

在上面的例子中,parse_invalid_data 函数返回的是 Iterator[Never],因为 parse_data 函数在遇到无效字节时会抛出 ValueError 异常,无法正常返回值。

总的来说,Iterator[Never] 类型注释在实际编程中的使用场景有限,主要用于特殊情况下的类型检查和异常建模。它增强了 Python 类型系统的表达能力,但需要权衡使用带来的复杂性。

 

18. assert关键字的理解

assert关键字在Python中用于断言(assertion),它用来做条件判断。通俗地讲,就是判断某个条件是否满足,如果条件不满足,就抛出AssertionError异常。它通常用于程序调试过程中,以确保程序的特定条件或预期的情况得到满足。

assert的含义:

assert关键字后面跟一个条件表达式,如果条件表达式的值为True(真值),那么程序正常继续执行;如果条件表达式的值为False(假值),则会抛出AssertionError异常。

通俗解释:

assert相当于在编程过程中插入一个"断言",如果遇到不符合预期的情况,就会触发异常,让程序停止运行,方便我们发现和修复错误。它就像是一个"警示开关",当条件不满足时,会给出警告或报错信息。

使用举例:

  1. 检查参数值是否符合预期
def get_grade(score):
    assert 0 <= score <= 100, "Invalid score value"  # 检查分数是否在合理范围内
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    ...
  1. 检查计算结果是否正确
result = sum([1, 2, 3, 4, 5])
assert result == 15, "Sum calculation error"  # 检查计算结果是否正确
  1. 在开发环境中启用断言,在生产环境中禁用
import os

# 设置断言模式
if os.environ.get('ENVIRONMENT') == 'DEVELOPMENT':
    # 开发环境启用断言
    assert_mode = True
else:
    # 生产环境禁用断言
    assert_mode = False

def divide(a, b):
    assert b != 0, "Divisor cannot be zero"  # 只在开发环境生效
    assert_mode and assert a >= 0 and b >= 0, "Only positive numbers allowed"  # 只在开发环境生效
    return a / b

总的来说,assert关键字可以帮助我们在编程时做一些简单的检查和验证,确保程序在特定条件下的正确性。

它在开发调试阶段非常有用,但在生产环境中可能需要禁用,以避免过多的性能开销。使用assert需要根据实际情况权衡利弊。

 

追问:关于第3条,是确定assert只能在开发环境中启用断言,在生产环境中禁用?

不完全是这样的。assert语句在Python中默认是始终有效的,无论是开发环境还是生产环境。

第3个例子中通过设置一个标志assert_mode来控制是否执行断言语句,只是一种常见的编程实践,而不是Python语言的强制要求。

在实际生产环境中,是否保留或禁用assert语句,需要考虑以下因素:

  1. 性能开销:执行assert语句会带来一定的运行时开销,在性能敏感的场景中可能需要禁用。
  2. 安全性:assert语句可能会透露一些内部实现细节,在某些安全敏感的应用中可能需要禁用。
  3. 可维护性:保留assert语句可以增加代码的可读性和可维护性,因为它们为代码提供了文档说明。
  4. 调试难度:如果生产环境出现异常,保留assert语句有利于快速定位和诊断问题。

因此,Python官方并不强制要求在生产环境中禁用assert语句。根据实际情况,可以选择保留或者通过编译器开关等方式禁用它们。比如CPython解释器提供了-O优化选项来禁用断言。

一些Python风格指南(如PEP8)建议,在生产环境中可以考虑禁用assert语句,但这只是一个推荐做法,而不是硬性要求。开发者可以根据具体的需求和场景来决定是否禁用。

 

19. 如下是我提供的两部分代码,分别是message.py和message_test.py,为什么我故意把expected修改错,但是mypy message_test.py却还是返回如下结果不报错??

## message.py代码:
"""
# tag::SHOW_COUNT_DOCTEST[]
>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no birds'

# end::SHOW_COUNT_DOCTEST[]
"""

# tag::SHOW_COUNT[]
def show_count(count: int, word: str) -> str:
    if count == 1:
        return f'1 {word}'
    count_str = str(count) if count else 'no'
    return f'{count_str} {word}s'
# end::SHOW_COUNT[]

## message_test.py代码:
from pytest import mark

from messages import show_count

@mark.parametrize('qty, expected', [
    (1, '1 part'),
    (2, '2 parts11'),
])
def test_show_count(qty, expected):
    got = show_count(qty, 'part')
    assert got == expected

def test_show_count_zero():
    got = show_count(0, 'part')
    assert got == 'no parts'

## 返回结果
(FluentPython2Env) D:\xx\xx\FluentPython2\08. Type Hints in Functions\message\hints_1>mypy message_test.py
Success: no issues found in 1 source file

在你的测试代码中,message_test.py 中的 test_show_count 函数使用了 @mark.parametrize 装饰器,它会按照参数化的方式运行测试用例。

你在参数中提供了两个测试用例,其中一个的期望值是错误的,但是这种参数化的方式在 mypy 静态类型检查时不会生效。因为 mypy 主要用于对类型进行检查,而不会执行测试用例。

因此,即使 expected 参数的值错误,mypy 仍然会通过,因为它主要关注类型的正确性,而不是运行测试用例。要确保测试用例的正确性,应该运行测试框架(如 pytest)来执行测试用例并检查结果。

 

20. 元组是否是可迭代的?以及any的用法,

理解下如下两端代码

a = tuple((1, 2, 3, 22, 33, 44, 55))
print(any(c not in range(256) for c in a)) # False
for c in a:
    print(c)

# output

1
2
3
22
33
44
55

如何理解这个f-String?

def rgb2hex(color: tuple[int, int, int]) -> str:
    if any(c not in range(256) for c in color):
        raise ValueError('Color components must be in range(256)')
    values = (f'{n % 256:02x}' for n in color) # ??
    return '#' + ''.join(values)

使用了一个生成器表达式 (f'{n % 256:02x}' for n in color),它会将 color 元组中的每个分量取模 256,并将结果格式化为两位的十六进制字符串。

这样做是为了确保每个分量都在 0 到 255 的范围内,并且结果是一个十六进制字符串的生成器。

关于f'{n % 256:02x}'

  • f'{...}': 这是 Python 3.6 引入的 f-string(格式化字符串)语法。它允许在字符串中包含 Python 表达式,并在字符串中直接引用变量或执行表达式的结果。
  • {n % 256:02x}:
    • n % 256: 这部分计算的结果是将变量 n 除以 256 取余数,确保结果在 0 到 255 之间。
    • :02x: 这部分是格式规范,用于指定输出的格式。具体含义如下:
      • :: 格式规范的起始符号
      • 02: 表示输出的字符串至少应该有两个字符的宽度,不足两位时在前面补零。
      • x: 表示以十六进制的形式输出整数。小写的 x 表示输出的字母为小写字母,如果使用大写的 X 则表示输出的字母为大写字母。

因此,f'{n % 256:02x}' 这个表达式的含义是将变量 n 转换为十六进制的字符串,并确保输出的字符串至少有两个字符的宽度,不足两位时在前面补零。

 

最后,使用 join 方法将生成器中的所有字符串连接起来,得到一个完整的十六进制颜色值字符串,并在前面加上 '#',表示颜色的十六进制表示。

 

21. 小细节,体会下bool的四则运算

a = 1
a += bool(2000) # bool(x) return false only when x = 0 
print(a)  # a = 2

 

22. print()和print('\n')以及print('\n', end='')的区别理解。focus on detail.

# tag::COLUMNIZE[]
from collections.abc import Sequence


def columnize(
        sequence: Sequence[str], num_columns: int = 0
) -> list[tuple[str, ...]]:
    if num_columns == 0:  # num_coloums=4, 不走这条路
        num_columns = round(len(sequence) ** 0.5)
    num_rows, reminder = divmod(len(sequence), num_columns)  # (6, 3) = divmod(27, 4)
    num_rows += bool(reminder)  # 7=6+1
    return [tuple(sequence[i::num_rows]) for i in range(num_rows)]


# end::COLUMNIZE[]


def demo() -> None:
    nato = (
        'Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India'
        ' Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo'
        ' Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu'
    ).split()

    for row in columnize(nato, 4):
        for word in row:
            print(f'{word:15}', end='')
        print() # 和下面的写法是等价的
        # print('\n', end='')

if __name__ == '__main__':
    demo()


# output
Alfa           Hotel          Oscar          Victor         
Bravo          India          Papa           Whiskey        
Charlie        Juliett        Quebec         X-ray          
Delta          Kilo           Romeo          Yankee         
Echo           Lima           Sierra         Zulu           
Foxtrot        Mike           Tango          
Golf           November       Uniform   

在这个代码中,print()函数的作用是将数据输出到控制台上。具体来说,在demo()函数中,print()函数被用于将经过格式化的单词输出成列,使其在控制台上呈现出类似表格的排列方式。

for row in columnize(nato, 4)循环中,print()函数被嵌套使用,用于在每一行输出一个单词,并通过end=''参数控制输出内容之间不换行。然后在每一行输出结束后,调用print()函数进行换行操作,以确保下一行的内容从新的一行开始输出。

总之,print()函数在这个上下文中的作用是将经过处理的单词列表输出到控制台上,以便用户能够查看到处理结果。

 

追问:那我输出print('\n')后,为什么输出结果,每一行都空了一行了呢?按道理输出形式和之前的print()应该一样,不应该不变吗

在 Python 中,print('\n') 会在输出时插入一个空行,因为 \n 是表示换行的特殊字符。当 print() 函数遇到 \n 时,它会将其解释为换行符,并在输出中添加一个空行。

这与 print() 函数的默认行为不同,print() 函数默认情况下不会在输出的末尾添加换行符,而是在每次调用 print() 函数时自动换行。

所以,当你在 demo() 函数中的循环中使用 print('\n') 时,它会在每次循环迭代时添加一个空行,导致输出的每一行之间都有一个空行。

如果想要输出的形式和之前的 print() 函数一样,应该使用 end='' 参数来阻止 print() 函数自动添加换行符,如下所示:

print('\n', end='')

这样就不会在输出中添加额外的空行了。

 

23. 如下demo()部分的代码,如何理解。

from collections.abc import Iterable

FromTo = tuple[str, str]  # <1>

def zip_replace(text: str, changes: Iterable[FromTo]) -> str:  # <2>
    for from_, to in changes:
        text = text.replace(from_, to)
    return text
# end::ZIP_REPLACE[]

def demo() -> None:
    import doctest
    failed, count = doctest.testmod()
    print(f'{count-failed} of {count} doctests OK')
    l33t = [(p[0], p[1]) for p in 'a4 e3 i1 o0'.split()]
    text = 'mad skilled noob powned leet'
    print(zip_replace(text, l33t))

以下是对demo()部分的分析理解:

def demo() -> None:  # 定义一个名为 demo 的函数,返回值为 None
    import doctest  # 导入 doctest 模块
    failed, count = doctest.testmod()  # 运行模块中的文档测试,并记录失败和总共测试的数量
    print(f'{count-failed} of {count} doctests OK')  # 打印 通过的【总数-错的】文档测试数量和总共测试的数量
    l33t = [(p[0], p[1]) for p in 'a4 e3 i1 o0'.split()]  # 生成一个列表 l33t,包含字符替换的元组
    text = 'mad skilled noob powned leet'  # 定义一个字符串 text
    print(zip_replace(text, l33t))  # 调用 zip_replace 函数,将 text 中的字符根据 l33t 中的规则进行替换,并打印结果

这段代码定义了一个名为 demo 的函数。函数内部首先导入了 doctest 模块,然后运行模块中的文档测试。文档测试是一种测试方法,它可以从文档字符串中提取测试用例并运行,以验证代码的正确性。

doctest.testmod() 函数返回两个值:failed 表示测试失败的数量,count 表示总共的测试数量。

接着,代码计算出通过的文档测试数量,并打印出来。然后,代码定义了一个名为 l33t 的列表,其中包含了一些字符替换的规则。

最后,代码定义了一个字符串 text,并调用 zip_replace 函数对其进行字符替换,然后将替换后的结果打印出来。

 

24. 了解下doctest的具体使用。[例如 replacer.py]

 

25. 

 

posted @ 2024-04-11 15:16  AlphaGeek  阅读(44)  评论(0)    收藏  举报