探索 Python 类中的特殊方法

PythonPythonBeginner
立即练习

介绍

在这个 Lab 中,你将探索 Python 的一些特殊方法,它们通常被称为“dunder”方法,因为它们的名字带有双下划线。你将获得关于这些方法如何让你自定义类和对象的行为的实践理解。

你将学习使用 __new__ 方法来控制实例的创建,以及使用 __del__ 方法来进行对象的销毁。你还将了解如何使用 __slots__ 来优化内存使用和限制属性,以及如何使用 __call__ 方法使你的类实例像函数一样可调用。通过实践示例,你将学会编写更高效、更具表现力的 Python 代码。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 初级 级别的实验,完成率为 100%。获得了学习者 100% 的好评率。

理解和使用 __new__ 方法

在这一步,你将探索 __new__ 方法。虽然 __init__ 通常用于在对象创建后初始化其属性,但 __new__ 实际上是首先创建实例的方法。它在 __init__ 之前被调用。

以下是关键区别:

  • __new__ 是一个静态方法,它将类(cls)作为其第一个参数。它负责创建并返回类的一个新实例。
  • __init__ 是一个实例方法,它将实例(self)作为其第一个参数。它初始化新创建的对象,并且不返回任何内容。

通常你不需要重写 __new__,因为来自 object 类的默认实现已经足够了。然而,在高级场景中,例如实现单例模式(Singleton pattern)或创建不可变类型(immutable types)的实例时,它非常有用。

让我们看看 __new__ 的实际应用。你将创建一个 Dog 类,该类在实例创建过程中打印一条消息。

首先,在 IDE 左侧的文件浏览器中打开文件 dog_cat.py

将以下代码添加到 dog_cat.py 文件中。此代码定义了一个 Animal 基类和一个重写了 __new__ 方法的 Dog 子类。

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name
        print(f'Initializing {self._name} in Animal.')

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    ## 第一个参数是 cls,它引用类本身。
    ## 它还必须接受传递给构造函数的所有参数。
    def __new__(cls, name, age):
        print('A new Dog instance is being created.')
        ## 调用父类的 __new__ 方法来创建实例。
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print(f'Initializing {name} in Dog.')
        ## 调用父类的 __init__ 方法来设置 name。
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

## 创建一个 Dog 实例
print("Creating a Dog object...")
d = Dog('Buddy', 5)
print("Dog object created.")
print(f"Dog's name: {d._name}, Age: {d.age}")

保存文件 (Ctrl+S 或 Cmd+S)。

现在,在你的 IDE 中打开一个终端(你可以使用菜单 Terminal > New Terminal)。运行脚本以观察方法调用的顺序。

python ~/project/dog_cat.py

你将看到以下输出。请注意,__new__ 首先被调用来创建实例,然后是 __init__ 方法来初始化它。

Creating a Dog object...
A new Dog instance is being created.
Initializing Buddy in Dog.
Initializing Buddy in Animal.
Dog object created.
Dog's name: Buddy, Age: 5

这证明了 __new__ 控制对象的创建,而 __init__ 在之后对其进行配置。

实现和测试 __del__ 方法

在这一步,你将了解 __del__ 方法。这个方法被称为终结器(finalizer)或析构函数(destructor)。当一个对象的引用计数降为零时,它会被调用,这意味着它即将被 Python 的垃圾回收器(garbage collector)销毁。它通常用于清理任务,例如关闭网络连接或文件句柄。

你可以使用 del 语句来移除对一个对象的引用。当最后一个引用消失时,__del__ 会被自动调用。

让我们向 Dog 类添加一个 __del__ 方法,以查看对象何时被销毁。

再次打开 dog_cat.py 文件。用以下代码替换文件的全部内容。这个版本移除了创建 Dog 实例的代码(以避免在导入模块时创建它),并向 Dog 类添加了 __del__ 方法。

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    def __new__(cls, name, age):
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

    ## 将此方法添加到 Dog 类中
    def __del__(self):
        print(f'The Dog object {self._name} is being deleted.')

保存 dog_cat.py 文件。

现在,让我们创建一个单独的脚本来测试这种行为。从文件浏览器中打开文件 test_del.py

将以下代码添加到 test_del.py 中。此脚本将创建两个 Dog 实例,然后显式删除其中一个。

## File Name: test_del.py

from dog_cat import Dog
import time

print("Creating two Dog objects: d1 and d2.")
d1 = Dog('Tom', 3)
d2 = Dog('John', 5)

print("\nDeleting reference to d1...")
del d1
print("Reference to d1 deleted.")

## 垃圾回收器可能不会立即运行。
## 我们添加一个短暂的延迟,给它一些时间运行。
time.sleep(1)

print("\nScript is about to end. d2 will be deleted automatically.")

保存文件。现在,在终端中运行 test_del.py 脚本。

python ~/project/test_del.py

观察输出。在调用 del d1 之后,会出现关于 Tom__del__ 消息。关于 John 的消息出现在最后,因为当脚本结束时,d2 对象会被垃圾回收。

Creating two Dog objects: d1 and d2.

Deleting reference to d1...
The Dog object Tom is being deleted.
Reference to d1 deleted.

Script is about to end. d2 will be deleted automatically.
The Dog object John is being deleted.

注意:垃圾回收的确切时间可能会有所不同。__del__ 在对象被回收时调用,不一定是在使用 del 之后立即调用。

使用 __slots__ 控制属性

在这一步,你将了解 __slots__。默认情况下,Python 将实例属性存储在一个名为 __dict__ 的特殊字典中。这允许你随时向对象添加新属性。然而,这种灵活性会占用额外的内存。

通过在类中定义一个 __slots__ 属性,你可以指定实例可以拥有的固定属性列表。这主要有两个影响:

  1. 内存节省:Python 为每个实例使用更紧凑的内部结构,而不是 __dict__,这可以显著减少内存使用,尤其是在创建大量对象时。
  2. 属性限制:你不能再向实例添加未在 __slots__ 中列出的属性。这有助于防止拼写错误并强制执行严格的对象结构。

让我们创建一个示例来看看 __slots__ 是如何工作的。从文件浏览器中打开文件 slots_example.py

将以下代码添加到 slots_example.py

## File Name: slots_example.py

class Player:
    ## 使用 __slots__ 定义允许的属性
    __slots__ = ('name', 'level')

    def __init__(self, name, level):
        self.name = name
        self.level = level

## 创建一个 Player 实例
p1 = Player('Hero', 10)

## 访问允许的属性
print(f"Player name: {p1.name}")
print(f"Player level: {p1.level}")

## 现在,尝试添加一个不在 __slots__ 中的新属性
print("\nTrying to add a 'score' attribute...")
try:
    p1.score = 100
    print(f"Player score: {p1.score}")
except AttributeError as e:
    print(f"Caught an error: {e}")

## 另外,检查实例是否具有 __dict__ 属性
print("\nChecking for __dict__...")
try:
    print(p1.__dict__)
except AttributeError as e:
    print(f"Caught an error: {e}")

保存文件。现在,在终端中运行 slots_example.py 脚本。

python ~/project/slots_example.py

输出显示你可以为 namelevel 赋值,但尝试为 score 赋值会引发 AttributeError。它还确认了该实例没有 __dict__

Player name: Hero
Player level: 10

Trying to add a 'score' attribute...
Caught an error: 'Player' object has no attribute 'score'

Checking for __dict__...
Caught an error: 'Player' object has no attribute '__dict__'

这演示了 __slots__ 如何强制执行固定的属性集,并通过消除实例字典来优化内存。

使用 __call__ 使实例可调用

在这一步,你将探索 __call__ 方法。在 Python 中,可以使用括号 () 像函数一样“调用”的对象被称为可调用对象(callable objects)。函数和方法天生就是可调用的。

默认情况下,类实例是不可调用的。但是,如果你在类中定义了 __call__ 特殊方法,其实例就变得可调用了。当你像调用函数一样调用一个实例时,其 __call__ 方法中的代码就会被执行。这对于创建既能像函数一样工作,又能保持自身内部状态的对象非常有用。

让我们创建一个其实例可以被调用的类。从文件浏览器中打开文件 callable_instance.py

将以下代码添加到 callable_instance.py

## File Name: callable_instance.py

class Greeter:
    def __init__(self, greeting):
        ## 此状态存储在实例中
        self.greeting = greeting
        print(f'Greeter initialized with "{self.greeting}"')

    ## 定义 __call__ 方法使实例可调用
    def __call__(self, name):
        ## 当实例被调用时,此代码运行
        print(f"{self.greeting}, {name}!")

## 创建一个 Greeter 实例
hello_greeter = Greeter("Hello")

## 使用内置的 callable() 函数检查实例是否可调用
print(f"Is hello_greeter callable? {callable(hello_greeter)}")

## 现在,像调用函数一样调用该实例
print("\nCalling the instance:")
hello_greeter("Alice")
hello_greeter("Bob")

## 创建具有不同状态的另一个实例
goodbye_greeter = Greeter("Goodbye")
print("\nCalling the second instance:")
goodbye_greeter("Charlie")

保存文件。现在,在终端中运行 callable_instance.py 脚本。

python ~/project/callable_instance.py

输出显示 hello_greeter 实例确实是可调用的。每次调用它时,都会执行 __call__ 方法,使用在初始化期间设置的状态(self.greeting)。

Greeter initialized with "Hello"
Is hello_greeter callable? True

Calling the instance:
Hello, Alice!
Hello, Bob!
Greeter initialized with "Goodbye"

Calling the second instance:
Goodbye, Charlie!

这演示了 __call__ 如何让你创建具有状态的、类似函数的对象,这是面向对象编程中的一个强大特性。

总结

在这个实验中,你探索了 Python 中几个强大的特殊方法。你学习了如何使用 __new__ 来控制实例创建过程,在调用 __init__ 之前获得一个挂钩点。你实现了 __del__ 方法来定义对象被垃圾回收时运行的清理逻辑。你还使用了 __slots__ 来优化内存,并通过阻止实例 __dict__ 的创建来强制执行严格的属性模型。最后,你通过实现 __call__ 方法使你的对象表现得像函数一样。掌握了这些双下划线(dunder)方法,你就可以编写更灵活、更高效、更符合 Python 风格的类了。

Morty Proxy This is a proxified and sanitized view of the page, visit original site.