Fluent Python2 【Chapter13_QA】

1. @runtime_checkable的理解?

举例:着重解释有无 @runtime_checkable 装饰器的差异

假设我们有一个 Person 类,表示一个人,有 nameage 两个属性。我们希望创建一个函数 check_adult() 来检查一个人是否成年。

如果一个人的年龄大于等于 18 岁,则视为成年,返回 True,否则返回 False

首先,我们来创建 Person 类:

class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

def check_adult(person: Person) -> bool:
    return person.age >= 18

adult = Person("Alice", 25)
child = Person("Bob", 15)

print(check_adult(adult))  # 输出:True
print(check_adult(child))  # 输出:False

这样就完成了我们的需求,但是现在让我们假设我们希望任何拥有 age 属性的对象都能够调用 check_adult() 函数而不仅仅是 Person 类的实例

为了达到这个目的,我们可以使用 @runtime_checkable 装饰器来声明 Person 类为可运行时检查的

from typing import Protocol, runtime_checkable

@runtime_checkable
class PersonProtocol(Protocol):
    age: int

def check_adult(person: PersonProtocol) -> bool: # 除了person的类型提示,其他均保持不变
    return person.age >= 18


class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age


adult = Person('Alice', 25)
child = Person('Bob', 16)

print(check_adult(adult))  # 输出:True
print(check_adult(child))  # 输出:False


class Animal: 
    def __init__(self, age):
        self.age = age

animal = Animal(16)
print(check_adult(animal))  # 输出:False


from collections import namedtuple

PersonTuple = namedtuple("PersonTuple", ["name", "age"])
person_tuple = PersonTuple(name="Frank", age=18)
print(check_adult(person_tuple))  # 输出:True


class Student:
    def __init__(self, age):
        self.age = age

student = Student(16)
print(check_adult(student))  # 输出:False

 

2. python动态协议、静态协议的区别?如何理解两者

在 Python 中,协议是一种约定或规范,用于描述对象的行为或功能。协议可以是动态的,也可以是静态的。

  1. 动态协议:

    • 定义:动态协议是在运行时检查的协议,它关注对象的行为。对象是否符合动态协议取决于其具有的方法或属性,而不是特定的类型或类继承关系
    • 特点:动态协议通常与鸭子类型(duck typing)相关联,即“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子”。
    • 示例:__len__() 方法是一个动态协议的示例。任何具有 __len__() 方法的对象都可以用于检索其长度,不管它们属于什么类或类型。例如,列表、元组、字符串等都实现了 __len__() 方法,因此它们都可以用于获取它们的长度。
  2. 静态协议:

    • 定义:静态协议是在编译时检查的协议,它关注对象的类型或类继承关系
    • 特点:静态协议通常涉及类型注解或基于类继承的形式。它要求对象的类型或类继承关系满足特定的条件,以便被视为符合该协议。
    • 示例:typing.Protocol 中定义的协议是静态协议的示例。通过在类中继承 typing.Protocol 类,可以定义一个静态协议,然后通过类型提示来强制要求对象符合该协议。例如,定义一个 Sortable 协议,要求对象具有 __lt__() 方法(用于定义小于关系),然后使用类型提示 def sort(obj: Sortable): 来确保传递给 sort() 函数的对象具有 __lt__() 方法。

总之,动态协议关注对象的行为,而静态协议关注对象的类型或类继承关系。动态协议在运行时进行检查,而静态协议在编译时进行检查。

 

3. python代码执行的顺序

在 Python 中,代码的执行过程通常包括以下几个步骤:

  1. 编写代码:首先,程序员编写 Python 代码,包括定义类、函数、变量等。

  2. 词法分析和语法分析:Python 解释器会对编写的代码进行词法分析和语法分析,将代码转换为抽象语法树(Abstract Syntax TreeAST)。

  3. 编译:在运行 Python 代码之前,解释器会将抽象语法树编译字节码(bytecode),这是一种中间形式的代码,不同于机器码,但比源代码更接近于计算机能够理解和执行的形式。

  4. 运行:一旦代码被编译成字节码,Python 解释器就开始执行字节码。在执行过程中,解释器会逐行解释字节码并执行相应的操作,包括创建对象、调用函数、执行循环等。

总的来说,Python 中的代码执行过程是先编写代码,然后经过词法分析和语法分析生成抽象语法树,接着将抽象语法树编译成字节码,最后解释器执行字节码并输出结果。

 

4. python的名义类型、结构类型的概念理解

在 Python 中,名义类型(Nominal Type)和结构类型(Structural Type)是两种用于描述类型的概念。

  1. 名义类型(Nominal Type)

    • 概念:名义类型是一种基于名称的类型,其类型标识符通常是通过类型的名称来确定的。在名义类型系统中,类型标识符是类型的名称本身,而不考虑类型的结构或行为

    • 通俗解释:名义类型是根据类型的名称来确定的,不关心类型的具体结构或行为。

    • 必要性:名义类型提供了一种明确的方式来标识类型,使得代码更加清晰和易于理解。

    • 用法举例:在 Python 中,类就是一种名义类型,例如:

class Dog:
    pass

dog_instance = Dog()

结构类型(Structural Type):

  • 概念:结构类型是一种基于类型的结构或成员的类型,其类型标识符通常是由类型的结构和行为决定的。在结构类型系统中,类型标识符是通过类型的结构和成员来确定的。

  • 通俗解释:结构类型是根据类型的结构和成员来确定的,而不关心类型的名称。

  • 必要性:结构类型允许我们根据类型的结构和行为进行类型判断,而不依赖于类型的名称,这在一些场景下更加灵活。

  • 用法举例:在 Python 中,collections.namedtuple 是一种结构类型,它根据指定的字段名称来创建新的命名元组类型,例如:

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
point_instance = Point(x=1, y=2)

在 Python 中,由于是一种动态类型语言,它并没有严格的名义类型或结构类型系统。然而,我们可以通过约定和使用一些特定的工具(如类型提示和类型检查器)来模拟名义类型和结构类型的概念,以提高代码的可读性和可维护性。

 

更通俗易懂的方式解释一下名义类型和结构类型。

  1. 名义类型:

    • 通俗解释:名义类型就好比我们日常生活中的身份证。身份证上有我们的姓名、身份证号码等信息,根据这些信息可以唯一确定一个人的身份。在编程中,名义类型就是根据类型的名称来确定的,就像我们根据一个人的姓名来确认他的身份一样。
    • 举例说明:假设有两个人,一个名叫小明,一个名叫小红。尽管他们长得不一样,但是只要他们的名字相同,我们就能确认他们是相同的人。
  2. 结构类型:

    • 通俗解释:结构类型就好比我们日常生活中的身份特征。人们可以根据一个人的身高、体重、五官等特征来认识他,而不一定要知道他的姓名。在编程中,结构类型就是根据类型的结构和成员来确定的,就像我们根据一个人的外貌特征来认识他一样。
    • 举例说明:假设有两个人,一个身高180cm,一个身高170cm。尽管他们的名字不同,但是只要他们的身高相同,我们就可以认为他们是相似的人。

 

5. __subclasshook__的概念的理解

__subclasshook__ 是Python中用于自定义抽象基类的类方法。它允许我们自定义判断某个类是否被视为抽象基类的子类的行为。

下面我将详细解释它的概念、作用,并用通俗的比喻和代码示例说明:

 

概念: Python中有一些特殊的基类,被称为抽象基类(Abstract Base Class,ABC)。这些基类不能被直接实例化,而是用于被其他类继承。

Python使用issubclass()isinstance()这两个函数来检查一个类是否是另一个类的子类或者一个实例是否属于某个类。__subclasshook__方法就是为了自定义这个检查过程而存在的。

 

作用: 当issubclass()isinstance()被调用时,Python会首先做标准的基于继承关系的检查。如果这一步检查返回False,Python再尝试调用被检查类的__subclasshook__()方法(如果存在的话)。

这就给了我们一次自定义检查逻辑的机会。因此,__subclasshook__的主要作用是:扩展issubclass()和isinstance()的语义,使其包含除了直接继承关系以外的其他语义。

 

通俗解释: 这个概念有点难以用日常生活类比,但我们可以用这样一个场景类比:假设一家公司为了保密,不希望外界知道员工的具体职位,只透露一个人是否为公司"成员"。

但成为公司"成员"的标准很复杂,不仅包括被直接雇佣,还包括一些特殊情况(如临时工、实习生等)。这时,公司可以自定义一个is_member()方法,在这个方法里写入检查一个人是否为"成员"的复杂逻辑。

当外界调用is_member()时,就可以根据定制的逻辑来判断了。这里is_member()就类似于__subclasshook__()

 

使用示例: 让我们看一个实际的代码示例,定义一个自定义的"可展示"类:

class Displayable(metaclass=abc.ABCMeta):  # 详见追问2解释
    
    @abc.abstractmethod
    def display(self): 
        pass

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Displayable:
            try:
                for B in C.__mro__:
                    if B is not C and issubclass(B, Displayable): # 详见追问1解释
                        return False
                    if callable(getattr(B, "display", None)):
                        return True
            except AttributeError:
                pass
        return NotImplemented

在上面的例子中,我们自定义了__subclasshook__方法。这个方法检查给定的类是否具有display方法, 如果有,则认为它是Displayable的子类。

我们可以通过以下方式测试:

class Console:
    def display(self): 
        print("Console displaying")

class NotRelevant:
    pass 

print(issubclass(Console, Displayable))  # True
print(issubclass(NotRelevant, Displayable)) # False

结果显示,虽然Console没有直接继承Displayable,但由于具有display方法,所以issubclass返回True

NotRelevant类因为没有display方法,所以判断为不是Displayable的子类。

通过__subclasshook__方法,我们扩展了issubclass的语义,使它不仅仅检查简单的继承关系,而是执行了我们自定义的更复杂的检查逻辑。

 

追问1:如下这段代码怎么理解?

if B is not C and issubclass(B, Displayable):
    return False

这行代码的目的是避免在继承层次中出现歧义。具体来说,如果 C 类的某个父类 B 也是 Displayable 的子类,那么就存在歧义,因为这种情况下我们无法确定是 C 类实现了 display 方法,还是从父类 B 继承了该方法。

为了避免这种歧义,Python 的做法是:如果在 C 类的继承层次中,除了 C 本身,还有其他类是 Displayable 的子类,那么就直接返回 False,表示不能把 C 视为 Displayable 的子类。

这是为了保持逻辑的一致性和清晰性。如果在继承层次中存在歧义,Python 选择直接返回 False,而不是继续执行其他检查逻辑。这样做的好处是,可以避免出现一些令人困惑的情况,

比如 C 类本身没有实现 display 方法,但由于某个父类实现了该方法,而被误认为是 Displayable 的子类。

通过这种方式,Python 确保了只有在 C 类及其父类中没有其他类是 Displayable 的子类时,才会继续执行下面的逻辑,检查 C 类或其父类中是否实现了 display 方法。

这样就避免了潜在的歧义,使得逻辑更加清晰和一致。

 

追问2:参数metaclass是什么含义

metaclass这个参数用于设置该类的元类(metaclass)元类是用来创建类的"类"

在Python中,一切都是对象,当然类也是一个对象。元类就是用来创建这个类对象的类。比如:

class Foo: pass

obj = Foo()  # 创建一个Foo类的实例对象

这里我们使用Foo类来创建了obj实例对象。但是Foo这个类本身也是一个对象,它是通过type这个元类创建的:

print(type(Foo))  # 输出: <class 'type'>

type就是Python的内置元类,如果你没有指定其他元类的话,Python就会使用type来创建类对象。

那为什么要指定metaclass参数呢?主要有两个原因:

  1. 拦截类的创建过程,修改或增强类的行为
  2. 给Python引入新的面向对象特性

比如在我们之前的例子中:

class Displayable(metaclass=abc.ABCMeta):
    ...

这里我们指定元类为abc.ABCMeta,而不是默认的type。这使得Displayable成为了一个抽象基类。

abc.ABCMeta会拦截类定义体的设置,检查其中是否包含抽象方法,如果有则不允许直接实例化同时它还会自动为子类补充一些特殊方法,比如我们之前看到的__subclasshook__

通过指定metaclass,我们可以很方便地为Python类添加一些特殊行为。而且由于metaclass的存在,Python的OOP特性也更加强大和灵活。

所以总的来说,metaclass参数用于指定创建该类的元类对象,从而拦截类的创建过程,修改或增强类的行为。很多Python的高级特性,比如抽象基类、数据描述符等都依赖于metaclass。

 

6. @runtime_checkable的理解,(相比问题1解释的更加透彻)

在Python中,@runtime_checkable 是一个特殊的装饰器,它是用于实现抽象基类(ABCs)的元编程工具。抽象基类是用来定义接口的类,而不是用来实例化的类。一个类如果定义了抽象方法(即没有实现的方法),那么它就是一个抽象基类。

概念

@runtime_checkable 装饰器允许你在运行时(runtime)检查一个类是否符合某种模式或协议。这意味着你可以在运行时检查一个对象是否实现了特定的方法,而无需关心这个类的继承关系。

作用

  • 接口定义:使用 @runtime_checkable 定义的抽象基类可以被用作接口,允许你检查一个对象是否符合这个接口的要求。
  • 协议检查:你可以检查一个对象是否实现了某个协议,而无需知道它的具体实现细节。
  • 动态类型检查:这允许你进行动态类型检查,即在运行时而不是在编译时检查类型。

通俗解释

想象一下,你有一群人,他们都有不同的能力,但是他们都有一些共同的特质。你可以定义一个协议(协议就是一组方法),比如说“会游泳的人”,然后你可以检查任何一个人是否符合这个协议。即使他们来自不同的家族或背景,只要他们实现了“会游泳的人”这个协议,他们就可以被认为是“会游泳的人”。

举例说明

下面是一个使用 @runtime_checkable 装饰器的例子:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Swimmable(Protocol):
    def swim(self):
        pass

class Person:
    def swim(self):
        print("I can swim!")

class Fish:
    def swim(self):
        print("I can swim very well!")

# 检查一个对象是否实现了 Swimmable 协议
person = Person()
fish = Fish()

assert isinstance(person, Swimmable)  # 检查 person 是否实现了 Swimmable 协议
assert isinstance(fish, Swimmable)    # 检查 fish 是否实现了 Swimmable 协议

在这个例子中,@runtime_checkable 装饰器用于定义 Swimmable 类为一个协议。

然后,你可以使用 isinstance() 函数来检查一个对象是否实现了这个协议。

即使 Person 和 Fish 类来自不同的继承树,只要它们实现了 swim 方法,它们就可以被认为是“会游泳的人”。

 

7. 为什么如下代码,在pycharm中写了后。当要准备执行代码randompickload.py时候,pycharm会多自动出来选项: Python test in randompickload.py, 为什么会有这种机制。我能够人为控制吗?

import random
from typing import Any, Iterable
from randompickload import LoadableRandomPicker


class SimplePicker:
    def __init__(self, items: Iterable) -> None:
        self.__items = list(items)
        random.shuffle(self.__items)

    def pick(self) -> Any:
        return self.__items.pop()


class LoadablePicker:
    def __init__(self, items: Iterable) -> None:
        self.load(items)

    def pick(self) -> Any:
        return self.__items.pop()

    def load(self, items) -> Any:  # LoadableRandomPicker这里返回的是None, 但是不影响测试通过
        self.__items = list(items)
        random.shuffle(self.__items)


def test_isinstance() -> None:
    popper = LoadablePicker([1])
    assert isinstance(popper, LoadableRandomPicker)


def test_isinstance_not() -> None:
    popper = SimplePicker([1])
    assert not isinstance(popper, LoadableRandomPicker)

在PyCharm中,当你编写完一个Python文件并准备执行它时,它会提供一系列的选项,包括单元测试(Unit Tests)。这是因为PyCharm集成了测试框架的支持,并且能够识别测试函数。

测试函数通常以 test_ 开头,PyCharm会自动识别这些函数并允许你运行它们。

在你的代码中,有两个函数 test_isinstance() 和 test_isinstance_not(),它们都以 test_ 开头,这正是PyCharm能够识别并允许你运行它们的原因。

这种机制是PyCharm的特性,旨在简化开发和测试流程。你可以通过以下方式控制它:

  1. 禁用测试功能:如果你不想看到测试选项,可以在PyCharm的设置中禁用这个功能。具体来说,你可以访问 File > Settings(Windows/Linux)或 PyCharm > Preferences(macOS),然后在搜索框中输入 “Test”,然后取消勾选相关的选项。

  2. 更改测试函数的命名:如果你不想让某些函数被视为测试函数,你可以更改它们的命名。测试函数通常以 test_ 开头,如果你不想让你的函数被视为测试函数,可以更改命名规则。

  3. 使用测试框架的特定功能:如果你在使用特定的测试框架(如pytest),你可能需要按照该框架的规范来编写测试函数。例如,在pytest中,测试函数通常以 test_ 开头,或者以 test_ 结尾。

总之,这种机制是PyCharm的一个特性,旨在简化开发和测试流程。你可以通过更改设置或函数命名来控制它。

 

8. 为什么如下代码,Vector2d类的__complex__方法没有被@abstractmethod装饰,也算符合SupportsComplex协议。

SupportsComplex的源码如下:

from typing import runtime_checkable, Protocol
from abc import abstractmethod

@runtime_checkable
class SupportsComplex(Protocol):
    """An ABC with one abstract method __complex__."""
    __slots__ = ()

    @abstractmethod
    def __complex__(self) -> complex:
        pass

vector2d_v4.py源码如下:

class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(array(self.typecode, self)))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __hash__(self):
        return hash(self.x) ^ hash(self.y)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(*memv)

    def __complex__(self):
        return complex(self.x, self.y)

    @classmethod
    def fromcomplex(cls, datum):
        return cls(datum.real, datum.imag)


from typing import SupportsComplex
v3 = Vector2d(1.5, 2.5)
print(isinstance(v3, SupportsComplex))  # True, why this return is True?

即使Vector2d类没有使用@abstractmethod修饰__complex__方法,它仍然符合SupportsComplex协议。在Python中,协议(Protocol)是一种用于类型检查的工具,它定义了一组应该由符合该协议的类实现的方法。

SupportsComplex协议通过@runtime_checkable装饰器变得可以在运行时检查。这意味着,如果一个类实现了SupportsComplex协议中定义的所有方法,那么它就符合该协议,即使它没有直接从该协议继承或使用@abstractmethod装饰器。

在上述例子中,Vector2d类实现了__complex__方法,这使得它符合SupportsComplex协议的要求。

因此,当您使用isinstance(v3, SupportsComplex)检查v3是否为SupportsComplex类型时,它返回True,因为v3是一个Vector2d实例,它提供了__complex__方法。

简而言之,符合协议的关键在于实现了协议所需的所有方法,而不在于是否使用了@abstractmethod装饰器或是否直接从协议继承了。

这是Python中协议的鸭子类型(duck typing)特性的体现:如果一个对象走路像鸭子,游泳像鸭子,叫声像鸭子,那么它就是鸭子。

 

追问:签名不一样也没事,有无装饰器装饰也没事,主要就是看有没有同名方法,然后确定是不是符合该协议,能这样理解吗

是的,这样理解是正确的。在Python中,协议(Protocol)是一种类型提示工具,它用于指定一组应该由符合该协议的类实现的方法。

协议的主要目的是为了静态类型检查,但它也可以在运行时使用isinstanceissubclass进行检查。

 

当一个类实现了协议中定义的所有方法时,它就符合该协议,即使方法的签名不完全匹配。这是因为Python的协议检查是基于方法的存在的,而不是基于方法的签名。

这种方法被称为“结构子类型”(structural subtyping)或“鸭子类型”(duck typing),因为它依赖于对象的方法和属性的结构,而不是对象的继承关系。

 

例如,即使Vector2d类的__complex__方法没有被@abstractmethod装饰,也没有被标记为abstractmethod

只要它存在并且有一个合适的签名(在这个例子中,它接受self作为参数并返回一个complex对象),Vector2d类就符合SupportsComplex协议。

需要注意的是,虽然运行时检查会忽略方法的签名,但静态类型检查器(如mypy)会根据协议中定义的签名来检查方法是否符合协议。

因此,如果方法的签名与协议中定义的不匹配,静态类型检查器可能会报错。

总之,只要一个类实现了协议中定义的所有方法,并且这些方法在运行时可以被调用,那么这个类就符合该协议。

这是Python中一种灵活的、基于结构的类型检查方式。

 

9. 如何理解__subclasshook__这个方法?

参考博客:__subclasscheck__ and __subclasshook__ in Python

 

__subclasshook__是Python中用于定制issubclass()行为的特殊方法。它允许我们定义一个类是否被视为另一个类的子类,即使它并不直接继承自该类。

概念: 当使用issubclass(C, B)检查C是否为B的子类时,Python首先检查C是否真的是B的子类(通过查看C的__bases__和B的__bases__属性)。

如果不是,它会查找B是否定义了__subclasshook__方法。如果定义了,Python会调用B.__subclasshook__(C)并使用其返回值来确定issubclass的结果。

作用: 这个特殊方法允许您扩展issubclass()的行为,使其能够识别某些不是直接继承自特定类的类。这在定义抽象基类或类型检查时非常有用。

通俗解释: 假设你有一个形状类(Shape),还有一个定制类(MyClass)。虽然MyClass并没有直接继承自Shape,但你希望MyClass也被视为Shape类的子类。

这时你可以在Shape类中定义__subclasshook__方法,当Python检查MyClass是否为Shape的子类时,这个方法会被调用并返回True或False。

举例说明:

from abc import ABC, ABCMeta, abstractmethod

class Shape(ABC):

    @abstractmethod
    def area(self): ...

    @abstractmethod
    def circumference(self): ...

    @classmethod
    def __subclasshook__(cls, other):
        return all(hasattr(other, method) for method in ['area', 'circumference'])

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def circumference(self):
        return 2 * (self.width + self.height)

print(issubclass(Rectangle, Shape))  # True

class Square(Rectangle):
    pass

print(issubclass(Square, Shape))  # True

class MyClass:
    def area(self):
        return 1

    def circumference(self):
        return 4

print(issubclass(MyClass, Shape))  # True

 

10. 如何理解__subclasscheck__这个方法?

这个方法同样需要定义在元类里面才有意义, 暂且按下不表,后面等到元类吃透后再研究。

 

posted @ 2024-02-19 14:15  AlphaGeek  阅读(34)  评论(0)    收藏  举报