Fluent Python2 【Chapter5_QA】

1. 元编程技术的理解

概念:元编程是指在运行时动态地创建、操作或修改程序的代码结构,通常是指在程序中操作程序自身的能力。元编程技术使得程序能够在运行时检查和修改其结构、行为和状态,从而实现更灵活、可扩展和自适应的程序设计。

通俗解释:通俗地说,元编程就像是在编写一本书的同时,可以随时修改书的章节、内容或者重新排列页面顺序,而不需要停下来重新写一本新书。

举例: Python 中的装饰器就是一种元编程技术。装饰器允许你在不修改函数本身的情况下,动态地添加额外的功能或行为。这种灵活性使得你可以在不改变函数定义的前提下,为函数添加日志记录、性能分析、权限检查等功能。

 

2. 硬编码、软编码的理解

硬编码(Hardcoding)指的是在程序中直接使用具体的数值、字符串或其他常量,而不是通过变量或配置文件来表示。这意味着程序的逻辑和数据直接嵌入到代码中,使得修改和维护变得困难。

软编码(Softcoding)则相反,它指的是将常量值和参数分离出来,以变量、配置文件或其他外部资源来表示,从而使得程序更加灵活和易于维护。

硬编码的一个例子是在程序中直接使用文件路径或数据库连接字符串,而软编码的方式是将这些信息存储在配置文件中,然后在程序中读取配置文件来获取这些信息。另一个例子是在程序中直接使用特定的数值或字符串,而软编码的方式是将这些数值或字符串存储在变量中,以便于在程序中进行修改和调整。

虽然硬编码和软编码都可以用于任何编程语言,但在Python中尤其容易注意到这种区别,因为Python支持灵活的配置文件和外部资源的读取,使得软编码成为一种常见的做法。

 

3. 工厂函数的概念?优势在于?

  1. 概念:工厂函数是一种设计模式,它是一个用来创建其他对象的函数。在工厂函数模式中,我们将对象的创建和初始化过程封装在一个函数中,而不是直接在代码的各个地方进行对象的创建。

  2. 产生的必要性:工厂函数模式的出现主要是为了解决对象创建过程的复杂性和耦合性问题。通过将对象的创建逻辑封装在一个函数中,我们可以更容易地管理和维护代码,降低代码的耦合度,并提高代码的可扩展性和可维护性。

  3. 通俗解释:想象一下你在设计一个游戏,需要创建不同类型的角色对象,比如玩家角色、敌人角色等。如果直接在游戏代码的各个地方直接创建这些角色对象,会导致代码的重复和耦合性增加。而使用工厂函数,你可以将角色对象的创建过程封装在一个函数中,根据不同的参数来创建不同类型的角色对象,这样就可以更加灵活地管理和使用角色对象。

 举例说明工厂函数优势:
让我们来举一个更能体现工厂函数优势的例子。假设我们有一个图形类 Shape,它有两个子类 CircleRectangle,我们想根据输入的参数创建不同类型的图形对象。

首先我们来看看不使用工厂函数的情况:

class Shape:
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

# 不使用工厂函数
def create_shape(shape_type, *args, **kwargs):
    if shape_type == 'circle':
        return Circle(*args, **kwargs)
    elif shape_type == 'rectangle':
        return Rectangle(*args, **kwargs)

# 创建圆形和矩形对象
circle = create_shape('circle', 5)
rectangle = create_shape('rectangle', 3, 4)

print(circle.area())     # 输出:78.5
print(rectangle.area())  # 输出:12

上面的代码定义了一个 create_shape 函数来根据输入的参数创建不同类型的图形对象。但是,这种方式存在一些问题:

  1. 每次创建对象都要调用 create_shape 函数,如果有大量的对象创建,这种方式会显得很繁琐。
  2. 如果新增一个新的图形类,需要修改 create_shape 函数,违反了开放-封闭原则。

现在我们来看看使用工厂函数的情况:

class Shape:
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

# 使用工厂函数
class ShapeFactory:
    @staticmethod
    def create_circle(radius):
        return Circle(radius)
    
    @staticmethod
    def create_rectangle(width, height):
        return Rectangle(width, height)

# 创建圆形和矩形对象
circle = ShapeFactory.create_circle(5)
rectangle = ShapeFactory.create_rectangle(3, 4)

print(circle.area())     # 输出:78.5
print(rectangle.area())  # 输出:12

在这个例子中,我们定义了一个 ShapeFactory 工厂类,它包含了创建不同类型图形对象的静态方法。使用工厂函数的优势在于:

(1)将对象创建逻辑从客户端代码中解耦,客户端不需要知道如何创建对象,只需调用工厂方法即可。

(2)添加新的图形类时,只需在工厂类中添加相应的静态方法,而不需要修改客户端代码,符合开放-封闭原则。

(3)如果有多个工厂类,可以实现更复杂的逻辑,例如根据参数动态选择使用哪个工厂类来创建对象。

 

4. 关于@staticmethod和@classmethod方法的简单理解

当在类中定义方法时,通常第一个参数是 self,它代表类的实例。而对于类方法(即在方法定义上使用 @classmethod 装饰器的方法)来说,第一个参数通常是 cls,它代表类本身,即类对象。这个参数使得方法能够访问和修改类级别的属性和方法,而不仅仅局限于实例级别的属性和方法。

cls 参数的作用主要有两个:

  1. 访问类的属性和方法: 在类方法中,通过 cls 参数可以访问和修改类级别的属性和方法。这些属性和方法对于所有的实例都是共享的

  2. 实现工厂方法: 在工厂函数中,有时候需要动态地创建类的实例。使用类方法作为工厂方法,可以更加灵活地创建实例,而不受具体类的限制。

下面是一个使用类方法作为工厂方法的例子:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @classmethod
    def create_square(cls, side_length):
        # 使用类方法创建一个正方形,宽高相同
        return cls(side_length, side_length)

# 创建一个正方形
square = Rectangle.create_square(5)
print(square.width, square.height)  # 输出: 5 5

而当说一个方法与类的状态无关时,意味着该方法的执行不依赖于类中的任何属性或状态。换句话说,即使在没有创建类的实例的情况下,该方法也可以独立地执行

一个典型的例子是工具类中的某些功能性方法,它们不需要访问或修改类的属性,也不需要操作类的实例。这种方法可以被设计为静态方法 (@staticmethod),因为它们不依赖于实例或类的状态。

下面是一个使用静态方法的例子:

class MathUtils:
    @staticmethod
    def add(x, y):
        # 静态方法用于执行简单的数学操作
        return x + y

# 使用静态方法执行加法操作
result = MathUtils.add(3, 5)
print(result)  # 输出: 8

在这个例子中,add 方法是一个静态方法,它接受两个参数 xy,并返回它们的和。这个方法不需要访问类的属性或实例状态,因此它可以独立地执行,不受类的任何状态影响。

 

5. @dataclass的概念和用法

以下是一个示例,首先是不使用 @dataclass 装饰器的版本:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point(x={self.x}, y={self.y})'

    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y


p1 = Point(2, 2)
p2 = Point(2, 2)
print(p1)
print(p1 == p2)

然后是使用 @dataclass 装饰器的版本:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p1 = Point(2, 2)
p2 = Point(2, 2)
print(p1)
print(p1 == p2)

这两段代码其实完全等价,但是使用过@dataclass后,代码整体要明显简洁太多。

这两个版本的代码实现了相同的功能:定义了一个表示二维空间中点的类。下面是对比说明:

  1. 简洁性:

    • 不使用装饰器的版本中,需要编写 __init____repr____eq__ 等方法,较多的重复代码使得类定义显得冗长。
    • 使用 @dataclass 装饰器的版本中,只需简单地声明类属性,装饰器会自动为我们生成 __init____repr__ 等方法,大大减少了代码量。
  2. 可读性:

    • 不使用装饰器的版本中,类定义中的方法实现相对分散,阅读起来需要花费更多的精力。
    • 使用 @dataclass 装饰器的版本中,类的定义更加清晰、简洁,属性和方法都集中在一处,易于理解和维护。
  3. 自动比较:

    • 不使用装饰器的版本中,需要手动编写 __eq__ 方法来实现对象的比较。
    • 使用 @dataclass 装饰器的版本中,装饰器自动为我们生成了 __eq__ 方法,对象的比较变得更加简单和直观。

综上所述,使用 @dataclass 装饰器可以大大简化数据类的定义,减少重复代码,提高代码的可读性和易维护性。

 

6. 关于__post__init__()方法的理解。

__post_init__() 方法是 Python 中 dataclasses 模块提供的特殊方法之一,用于在实例对象创建后自动调用。它允许在对象初始化完成后执行一些额外的初始化操作,通常用于处理对象之间的关联性或执行一些需要在对象完全初始化之后才能进行的操作。

__init__() 方法不同,__post_init__() 方法是在对象的所有属性都已经被初始化之后才被调用的。这使得它成为执行一些需要依赖于其他属性值的初始化逻辑的理想位置。

下面是一个简单的示例,演示了如何使用 __post_init__() 方法:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int
    color: str

    def __post_init__(self):
        if self.color == 'red': #  实例化完成 __init__()之后的判断操作了
            self.x *= 2
            self.y *= 2

# 创建 Point 对象
p1 = Point(1, 2, 'red')
print(p1)  # 输出: Point(x=2, y=4, color='red')

p2 = Point(3, 4, 'blue')
print(p2)  # 输出: Point(x=3, y=4, color='blue')

在这个示例中,当颜色为 'red' 时,__post_init__() 方法将 xy 的值分别乘以 2。这样可以确保在对象创建后对颜色为 'red' 的点进行特殊处理。

 

7.  向前引用的概念的理解。此外,inspect.get_annotations()或typing.get_type_hints() 这两个函数可以解析类型提示中的向前引用是什么回事?

向前引用(Forward Reference)是指在代码中使用了一个还未定义的名称。通常情况下,Python会报未定义的名称错误,但在某些特定的情况下,向前引用是被允许的,并且在使用前会自动解析。

通俗解释: 想象你正在给一个朋友介绍两个新认识的人,比如Tom和Jerry。你可能会这样说:"Tom是Jerry最好的朋友,Jerry也是Tom最好的朋友。"在你描述Tom时提到了Jerry,在描述Jerry时又提到了Tom。这就是一种向前引用的情况,你在定义某个东西之前就已经使用了它。

举例:

# 向前引用的情况
class Node:
    def __init__(self, value, next=None):
        self.value = value
        self.next = next

class LinkedList:
    def __init__(self):
        self.head = None

    def append(self, value):
        new_node = Node(value)
        if self.head is None:
            self.head = new_node
            return
        last = self.head
        while last.next:
            last = last.next
        last.next = new_node

这里的关键是next参数的类型注解。虽然没有明确写出来,但根据Python的规则,如果一个参数没有类型注解,那么它的类型注解默认就是该参数名所对应的类型。

也就是说,对于next=None这一行,next参数的隐含类型注解实际上是 next: Node

换句话说,它的意思是:
class Node:
    def __init__(self, value, next: Node = None):
        self.value = value
        self.next = next

在这里,next: Node就构成了一种对Node类的引用。但是问题是,在定义__init__方法的时候,Node类还没有完全定义好。

通常如果我们尝试引用一个还未定义的名称,Python会报未定义的名称错误(NameError)。但是在这种类定义的情况下,Python的规则允许我们暂时使用这个还未定义的名称,等整个类定义完毕后,它会自动将这个名称解析为这个类本身。

这就是我们所说的"向前引用"的情况。在定义__init__时,Node还未定义,但是我们已经使用了Node作为next参数的类型注解。Python暂时允许这样做,等整个类定义完成后就会自动解析这个名称了。

总的来说,Node类在定义时,使用了自身Node作为next参数的类型注解,构成了一种特殊的"向前引用"情况,Python能够自动解析这种情况。

 

8. @dataclass(fronze=True)中,fronze=True的作用?

from dataclasses import dataclass

@dataclass(frozen=True) # 
class Coordinate:
    lat: float
    lon: float

coord1 = Coordinate(51.0572, 0.1275)
print(coord1)
coord1.lat = 40 # fronze=True时,这一行会报错;而fronze=False时,这一行不报错
print(coord1)

frozen=True 是 Python 中 dataclass 装饰器的一个参数,它让 dataclass 实例变成不可变对象(immutable)。

当一个 dataclass 被标记为 frozen=True 时,具有以下特点:

  1. 所有数据属性在创建实例后就不能再被修改。试图修改会引发 FrozenInstanceError 异常。
  2. 两个 frozen dataclass 实例可以直接使用 == 操作符来比较值是否相等,而不需要定义自己的 __eq__ 方法。
  3. frozen dataclass 的 __hash__ 方法由解释器自动生成,因此 frozen 实例可以作为字典的键或存储在集合中。
  4. 默认情况下会阻止继承该 frozen dataclass,但可以通过添加 @dataclass(frozen=True, unsafe_hash=True) 来允许继承。
  5. 适合作为键、常量或只读数据结构使用,提高了代码的安全性和可读性。

 

9. 枚举类Enum以及auto()的概念理解

Enum类和auto()函数是Python中用于定义枚举(enumeration)类型的工具。枚举类型是一种特殊的数据类型,它由一组有限的、不重复的实例(成员)组成。每个实例都有一个独一无二的名称和数值。

Enum类

Enum类是Python标准库中的一个类,用于定义新的枚举类型。当我们使用class语句继承Enum时,Python就会自动构建一个新的枚举类型,其中每个成员都是该类的一个实例。

例如:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

在这个例子中,我们定义了一个名为Color的枚举类型,它有三个成员:REDGREENBLUE。每个成员都被赋予了一个关联值(1、2和3)。

我们可以像使用常量一样使用枚举成员:

print(Color.RED)  # Output: Color.RED
print(Color.RED.value)  # Output: 1

auto()函数

在上面的例子中,我们手动为每个成员分配了一个值。然而,Python提供了auto()函数,可以自动为每个成员分配一个独一无二的值。

from enum import Enum, auto

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

print(Color.RED.value)  # Output: 1
print(Color.GREEN.value)  # Output: 2
print(Color.BLUE.value)  # Output: 3

在这个例子中,auto()函数为每个成员自动分配了从1开始的连续整数值。这样做可以确保每个成员都有一个唯一的值,而无需手动分配。

通俗解释

枚举类型可以被看作是一组有限的、命名的常量值。例如,我们可以使用枚举来表示一周中的七天、一年中的十二个月、一副牌中的四个花色等。使用枚举可以使代码更加可读和安全,因为它们提供了有意义的名称,而不是使用难以理解的数字或字符串常量。

auto()函数则为每个枚举成员自动分配一个递增的整数值,这样我们就不需要手动指定每个成员的值了。它使得定义枚举类型变得更加简单和方便。

总的来说,Enum类和auto()函数为Python提供了一种定义和使用枚举类型的简单而强大的方式,可以提高代码的可读性和可维护性。

 

10. default_factory如何理解?

default_factoryfield 函数的一个参数,它允许你指定一个可调用对象(通常是一个函数),该函数将被用于为字段提供默认值。

在你提供的示例中:

guests: list = field(default_factory=list)

default_factory=list 表示当创建一个新的 ClubMember 实例时,guests 字段将使用 list() 函数的返回值(一个新的空列表)作为默认值。

使用 default_factory 的主要优点是,它确保每个实例都有自己的独立的默认值。如果你直接将默认值设置为容器对象(如列表或字典),那么所有实例将共享同一个容器对象。这可能导致意外的副作用,因为对该容器的任何修改都会影响所有实例。

default_factoryfield 函数的一个参数,它允许你指定一个可调用对象(通常是一个函数),该函数将被用于为字段提供默认值。

同样还是上述示例中:

guests: list = field(default_factory=list)

这一行也定义了一个类字段guests。不同之处在于,它使用了field函数来指定guests的默认值。default_factory=list表示guests的默认值是一个空列表。如果不使用field函数,那么guests的默认值将是None

使用field函数定义字段的默认值是一种常见做法,因为它可以确保每个实例都有一个新的、独立的默认值。如果直接将默认值设置为[](如guests: list = [])的话,所有实例将共享同一个列表,这可能会导致一些意料之外的行为。

default_factoryfield 函数的一个参数,它允许你指定一个可调用对象(通常是一个函数), 该函数将被用于为字段提供默认值。

使用defalut_factory参数的优势由以下例子可证明:

假设我们有如下dataclass定义: 

from dataclasses import dataclass

@dataclass
class Student:
    name: str
    grades: list = [] # 错误做法,将所有实例共享同一个列表

student1 = Student('Alice')
student2 = Student('Bob')

print(student1.grades)  # 输出: []
print(student2.grades)  # 输出: []

student1.grades.append(90)
print(student1.grades)  # 输出: [90]
print(student2.grades)  # 输出: [90] 意外情况!

在上面的例子中,我们将grades字段的默认值直接设置为[](一个空列表)。当我们创建student1student2两个实例时,它们的grades字段初始值都是[]

但是,当我们给student1.grades添加一个90分的时候,student2.grades也被意外地修改了!这是因为两个实例共享同一个列表对象作为grades字段的值。

现在,我们使用default_factory来修复这个问题: 

from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    grades: list = field(default_factory=list)

student1 = Student('Alice')
student2 = Student('Bob')

print(student1.grades)  # 输出: []
print(student2.grades)  # 输出: []

student1.grades.append(90)
print(student1.grades)  # 输出: [90]
print(student2.grades)  # 输出: [] 正确

在这个版本中,我们使用grades: list = field(default_factory=list)来确保每个实例都有一个新的、独立的列表作为grades字段的默认值。

当我们创建student1student2时,它们的grades字段初始值都是[]。但是,这两个列表对象是不同的。因此,当我们给student1.grades添加90分时,student2.grades不会受到影响,它保持着初始的[]值。

这个例子再次说明了使用default_factory的优点: 它确保每个实例都有自己独立的默认值, 避免了意外的副作用,使代码更加安全和可预测

 

11. __post_init__方法的理解

__post_init__ 是 Python 数据类 (dataclass) 中的一个特殊方法,它在实例初始化后自动被调用。它的作用是允许你在对象完全初始化后执行一些额外的自定义操作。

作用:

  • 在实例创建后执行一些额外的操作,例如设置默认值、执行数据验证、初始化其他对象等。
  • 可以访问和修改实例的所有属性,因为在调用 __post_init__ 时,实例已经完全初始化。
  • 提供了一种在不覆盖 __init__ 方法的情况下自定义数据类实例初始化过程的方式。

通俗解释:

想象一下,你正在装修一个新房子。首先,你需要建立房子的基本结构,包括墙壁、地板和屋顶。这个过程就相当于 __init__ 方法,它创建了对象的基本属性。

但是,在房子完全建好之后,你可能还想进行一些额外的工作,比如装修室内、布置家具、安装电器等。这些额外的工作就可以在 __post_init__ 方法中完成。__post_init__ 就像是一个额外的装修工人,在房子的基本结构建立后,进行一些个性化的加工。

示例:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int = 0

    def __post_init__(self):
        if self.age < 0:
            raise ValueError("Age cannot be negative")
        self.age_last_birthday = self.age

p = Person("Alice", -10)  # 这将引发 ValueError

在上面的示例中,我们在 __post_init__ 方法中添加了一个年龄验证,如果年龄为负数,则引发 ValueError。我们还初始化了一个新的属性 age_last_birthday

__init__ 方法的区别:

__init__ 方法是实例化对象时首先被调用的方法,用于设置对象的初始状态。而 __post_init__ 方法是在对象完全初始化后被调用的,可以用于执行一些额外的操作或验证。

__init__ 方法通常用于初始化对象的基本属性,而 __post_init__ 方法则可以用于执行一些依赖于这些基本属性的操作,或者对属性进行额外的处理和验证

总的来说, __post_init__ 方法提供了一种在不覆盖 __init__ 方法的情况下自定义数据类实例初始化过程的方式, 增强了数据类的灵活性和可扩展性。

 

12. globals().items()含义的理解

globals().items() 是 Python 中一个内置函数,它的作用是返回一个由当前全局符号表中键值对构成的字典视图对象。让我们来详细解释一下这个函数的概念、作用,并通过示例来进行说明。

概念:

  • globals() 函数返回表示当前全局符号表的字典。全局符号表是一个存储全局变量和函数的字典。
  • items() 是字典对象的一个方法,它返回一个由字典键值对构成的视图对象。

作用:

  • 获取当前模块或脚本中定义的所有全局变量和函数。
  • 在某些情况下,可以方便地遍历和操作全局符号表中的项。

通俗解释: 想象一下,你有一个储物柜,里面存放了各种物品。globals() 就相当于这个储物柜本身,而 globals().items() 相当于你打开柜门,看到里面所有物品及其对应的位置。

每个物品都有一个名称(键)和一个实际的物品(值)。globals().items() 让你一次性获取到所有物品的名称和实际物品,这样你就可以方便地查看、操作或管理这些物品。

示例:

x = 10
y = 20

def add(a, b):
    return a + b

print(globals().items())
dict_items([('__name__', '__main__'), ('__doc__', None), ('__package__', None), ('__loader__', <class '_frozen_importlib.BuiltinImporter'>), ('__spec__', None), ('__annotations__', {}), ('__builtins__', <module '__builtin__' (built-in)>), ('x', 10), ('y', 20), ('add', <function add at 0x7f8e8c4d7e60>)])

在这个示例中,我们定义了两个全局变量 xy,以及一个函数 add()。调用 globals().items() 会返回一个字典视图对象,其中包含了当前模块中所有的全局变量和函数,包括内置的和用户定义的。

需要注意的是,globals().items() 返回的是一个视图对象,而不是一个列表或字典。视图对象是一种动态的数据结构,它反映了底层字典的变化。因此,如果你修改了全局符号表中的项,视图对象也会相应地更新。

总的来说,globals().items() 提供了一种方便的方式来获取和操作当前模块或脚本中定义的全局变量和函数。它在某些特殊情况下可能会很有用,但是在日常编程中并不经常使用。

 

 13. ClassVar()的概念的初步理解。【hackerclub_annotated.py】

from dataclasses import dataclass
from typing import ClassVar
from club import ClubMember

@dataclass
class HackerClubMember(ClubMember):
    all_handles: ClassVar[set[str]] = set()
    handle: str = ''

    def __post_init__(self):
        cls = self.__class__
        if self.handle == '':
            self.handle = self.name.split()[0]
        if self.handle in cls.all_handles:
            msg = f'handle {self.handle!r} already exists.'
            raise ValueError(msg)
        cls.all_handles.add(self.handle)

ClassVar是Python中的一个特殊类型注释,用于定义类级别的变量,即属于整个类而不是每个实例的变量。通俗地解释,ClassVar就像是给整个类"共享"的一个变量,所有实例都可以访问和修改它。

作用:

  1. 跨实例共享状态: ClassVar允许在类级别定义和管理状态,所有实例都可以访问和修改这个状态。这在需要维护一些全局状态或跨实例的统计数据时很有用。
  2. 避免数据冗余: 如果某些数据是所有实例共享的,使用ClassVar可以避免在每个实例中重复存储这些数据,从而节省内存。
  3. 类型安全: 使用ClassVar可以在类型检查时明确变量的作用域,提高代码可读性和可维护性。

优势:

  • 提高代码的可读性和可维护性
  • 减少内存占用
  • 方便管理跨实例的共享状态

示例解释:

在你提供的示例中,HackerClubMember类继承自ClubMember类。

  1. all_handles: ClassVar[set[str]] = set()定义了一个类变量all_handles,类型为set[str],初始值为空集合。这个变量属于整个HackerClubMember类,而不是每个实例。
  2. handle: str = ''则是每个实例都有自己的handle属性,用于存储成员的处理程序名称。
  3. __post_init__方法中,如果实例的handle为空,则使用name属性的第一个单词作为handle
  4. 接下来,代码检查新的handle是否已经存在于all_handles中。如果存在,则会引发ValueError异常。
  5. 如果handle不存在,则将其添加到cls.all_handles中,其中cls是当前类的引用。

这个示例的目的是确保每个HackerClubMember实例都有一个唯一的handle,并将所有handle存储在all_handles这个类变量中,以便跨实例访问和管理。通过使用ClassVar,我们可以避免在每个实例中重复存储所有handle,从而节省内存。同时,代码也更加清晰,可读性更好。

 

14. 举例说明使用ClassVar()的优势

好的,我们来比较一下有无使用ClassVar的区别:

示例1: 没有使用ClassVar

class HackerClubMember:
    def __init__(self, name):
        self.name = name
        self.handle = name.split()[0]
        self.all_handles = set()
        self._check_handle()

    def _check_handle(self):
        if self.handle in self.all_handles:
            raise ValueError(f'Handle {self.handle!r} already exists.')
        self.all_handles.add(self.handle)

在这个示例中,每个HackerClubMember实例都有自己的all_handles属性,用于存储所有成员的handle

缺点:

  • 浪费内存:每个实例都需要存储所有handle,造成内存浪费
  • 不利于状态共享:每个实例的all_handles都是独立的,无法进行状态共享和协作

示例2: 使用ClassVar

from typing import ClassVar

class HackerClubMember:
    all_handles: ClassVar[set[str]] = set()

    def __init__(self, name):
        self.name = name
        self.handle = name.split()[0]
        self._check_handle()

    def _check_handle(self):
        if self.handle in self.__class__.all_handles:
            raise ValueError(f'Handle {self.handle!r} already exists.') 
        self.__class__.all_handles.add(self.handle)

在这个示例中,all_handles被声明为一个ClassVar,属于整个HackerClubMember类。所有实例共享这个变量。

优点:

  • 节省内存:所有实例共享同一个all_handles,无需重复存储
  • 方便状态共享:所有实例都可以访问和修改all_handles的内容
  • 代码更简洁:将属于类的状态明确定义为ClassVar

通过这两个示例,我们可以看到使用ClassVar的主要优势是节省内存,方便状态共享,同时代码也更加干净和易于维护。适当使用ClassVar可以提高代码质量和性能。

 

15. 对于14中的所有实例共享这个变量,共享与否如何证明?

情形一:不含ClassVar()

class HackerClubMember:

    def __init__(self, name):
        self.name = name
        self.handle = name.split()[0]
        self.all_handles = set() # 不含ClassVar()的代码
        print(f"Creating instance: {self.name}, id(all_handles): {id(self.all_handles)}")
        self._check_handle()

    def _check_handle(self):
        if self.handle in self.all_handles:
            raise ValueError(f'Handle {self.handle!r} already exists.')
        self.all_handles.add(self.handle)
        print(f"After adding handle, id(all_handles): {id(self.all_handles)}")

member1 = HackerClubMember("Alice Smith")
# Output:
Creating instance: Alice Smith, id(all_handles): 2473243032288
After adding handle, id(all_handles): 2473243032288  # 这两个一样

member2 = HackerClubMember("Bob Johnson")
# Output:
Creating instance: Bob Johnson, id(all_handles): 2473243032512
After adding handle, id(all_handles): 2473243032512 #这俩个也一样,但是与上面的不一样,说明是不同对象

情形二:含有ClassVar()

from typing import ClassVar

class HackerClubMember:
    all_handles: ClassVar[set[str]] = set() # 采用了ClassVar()

    def __init__(self, name):
        self.name = name
        self.handle = name.split()[0]
        print(f"Creating instance: {self.name}, id(all_handles): {id(self.__class__.all_handles)}")
        self._check_handle()

    def _check_handle(self):
        if self.handle in self.__class__.all_handles:
            raise ValueError(f'Handle {self.handle!r} already exists.')
        self.__class__.all_handles.add(self.handle)
        print(f"After adding handle, id(all_handles): {id(self.__class__.all_handles)}")

member1 = HackerClubMember("Alice Smith")
# Output:
Creating instance: Alice Smith, id(all_handles): 2896316906208
After adding handle, id(all_handles): 2896316906208

member2 = HackerClubMember("Bob Johnson")
# Output:
Creating instance: Bob Johnson, id(all_handles): 2896316906208
After adding handle, id(all_handles): 2896316906208 # 这4个对象的id都一样,可见是同一个对象。

 

15. from typing import TypedDict, 这个TypedDict如何理解?

from typing import TypedDict

class Book(TypedDict):
    title: str
    author: str
    year: int
    
book: Book = {
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald", 
    "year": 1925
}

# 如果键值对类型不匹配,IDE会提示错误
wrong_book: Book = {
    "title": 123,  # 类型错误
    "author": "Jane Austen",
    "year": "1813" # 类型错误
}

在上面的例子中,我们定义了一个Book类型的TypedDict,它规定了title必须是字符串,author必须是字符串,year必须是整数。如果在赋值时出现类型不匹配的情况,IDE会给出类型错误提示。

TypedDict的使用可以增强代码的类型安全性,同时也为开发人员提供了更好的类型提示和自动补全体验,提高开发效率。

 

16. @dataclass(order=True), 理解下order参数排序的规则

from dataclasses import dataclass

@dataclass(order=True)
class Person:
    name: str
    age: int

p1 = Person("Alice", 30)
p2 = Person("Bob", 25)

print(p1 > p2) # False
print(sorted([p2, p1])) # [Person(name='Alice', age=30), Person(name='Bob', age=25)]

在您提供的示例中,@dataclass(order=True)是一个装饰器,它告诉 Python 根据数据类中定义的字段自动生成比较方法(__lt__, __le__, __gt__, __ge__)。这些比较方法可用于对数据类实例进行排序和比较操作。

具体来说,order=True会按照数据类中字段定义的顺序进行比较。在你的示例中,Person类首先比较name字段,如果name相同,则比较age字段。

当你运行 print(p1 > p2) 时,由于 p2.name 在字母顺序上大于 p1.name,所以返回 False。

在Python中,字符串的比较是基于字符的Unicode码位值进行的。具体来说:

  1. Python先比较两个字符串的第一个字符的Unicode码位值的大小
    • 如果相等,则比较第二个字符
    • 如果不等,较大的那个字符所在的字符串就被认为更大
  2. 如果其中一个字符串是另一个字符串的前缀,较长的那个字符串更大

让我们具体比较一下"Alice"和"Bob":

  • "A"的Unicode码位值是65
  • "B"的Unicode码位值是66
  • 由于66 > 65,所以"B"被认为大于"A"

因此,从第一个字符开始比较,"Bob"就被认为大于"Alice"

而当你运行 print(sorted([p2, p1]))时,sorted 函数使用生成的比较方法对列表中的元素进行排序。首先按 name 排序,由于 "Alice" < "Bob",所以 p1排在 p2 前面。因此,输出结果为 [Person(name='Alice', age=30), Person(name='Bob', age=25)]

总的来说,order=True让数据类可以根据字段的定义顺序自动生成比较方法,这样就可以方便地对数据类实例进行排序和比较操作了。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2024-04-06 23:48  AlphaGeek  阅读(53)  评论(0)    收藏  举报