欢迎,来自IP地址为:3.18.103.55 的朋友


是否对于 Python 可以使用 + 或 – 操作符来处理对象感到神奇呢?或者它是如何知道在打印对象时如何正确的显示它们?答案就在于 Python 的魔法方法,也称为 dunder(双下划线)方法。

魔法方法是在 Python 中特殊的方法,可让用户在定义对象如何响应各种操作的内置函数。它们使 Python 的面向对象编程成为如此强大和直观的原因。

在本教程中,我们将学习如何使用魔法方法来创建更优雅、更强大的代码。通过实际示例,将展示这些方法在实际场景中的工作原理。

阅读本教程的先期条件:

  • 基本了解 Python 语法和面向对象编程概念
  • 熟悉类、对象和继承
  • 了解内置 Python 数据类型(列表、字典等)
  • 建议安装有效的 Python 3 来尝试示例程序

什么是魔法方法?

Python 中的魔法方法是以双下划线(__)开头和结尾的特殊方法。当我们需要对对象使用某些操作或函数时,Python 会自动调用这些方法。

例如,当我们对两个对象使用 + 运算符时,Python 会自动在左操作数中查找 __add__ 方法。如果找到,它会使用右操作数作为参数来调用该方法。

这是一个简单的例子,展示了它的工作原理:

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

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2  # This calls p1.__add__(p2)
print(p3.x, p3.y)  # Output: 4 6

让我们分析一下上面的代码:

  1. 我们创建一个 Point 类,假设用来表示 2D 空间中的一个点
  2. __init__ 方法初始化 x 和 y 坐标
  3. __add__ 方法定义当我们添加两个点时会发生什么
  4. 当我们写入 p1 + p2 时,Python 会自动调用 p1.__add__(p2)
  5. 结果是一个坐标为 (4, 6) 的新 Point

这仅仅是个开始,Python 有许多神奇的方法,可让我们自定义对象在不同情况下的行为方式。让我们探索一些最有用的方法。

对象表示

在 Python 中使用对象时,我们经常需要将它们转换为字符串。当需要打印对象或尝试在交互式控制台中显示它时,就会发生这种情况。Python 为此提供了两种魔术方法:__str__ 和 __repr__。

str vs repr

__str__ 和 __repr__ 方法有不同的用途:

  • __str__:由 str() 函数和 print() 函数调用。它应该返回一个最终用户可读的字符串
  • __repr__:由 repr() 函数调用并在交互式控制台中使用。它应该返回一个字符串,理想情况下,该字符串可用于重新创建对象

以下示例显示了差异:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __str__(self):
        return f"{self.celsius}°C"

    def __repr__(self):
        return f"Temperature({self.celsius})"

temp = Temperature(25)
print(str(temp))      # Output: 25°C
print(repr(temp))     # Output: Temperature(25)

在此示例中:

  • __str__ 返回一个用户友好的字符串,显示带有度数符号的温度
  • __repr__ 返回一个字符串,显示如何创建对象,这对于调试很有用

当我们在不同的上下文中使用这些对象时,差异就会变得明显:

  • 打印温度时,就会看到用户友好的版本:25°C
  • 在 Python 控制台中检查对象时,就会看到详细版本:Temperature(25)

实际示例:自定义错误类

让我们创建一个提供更好调试信息的自定义错误类。此示例展示了如何使用 __str__ 和 __repr__ 使错误消息更有帮助:

class ValidationError(Exception):
    def __init__(self, field, message, value=None):
        self.field = field
        self.message = message
        self.value = value
        super().__init__(self.message)

    def __str__(self):
        if self.value is not None:
            return f"Error in field '{self.field}': {self.message} (got: {repr(self.value)})"
        return f"Error in field '{self.field}': {self.message}"

    def __repr__(self):
        if self.value is not None:
            return f"ValidationError(field='{self.field}', message='{self.message}', value={repr(self.value)})"
        return f"ValidationError(field='{self.field}', message='{self.message}')"

# Usage
try:
    age = -5
    if age < 0:
        raise ValidationError("age", "Age must be positive", age)
except ValidationError as e:
    print(e)  # Output: Error in field 'age': Age must be positive (got: -5)

此自定义错误类提供了几个好处:

  1. 它包含发生错误的字段名称
  2. 它显示导致错误的实际值
  3. 它提供用户友好且详细的错误消息
  4. 它通过包含所有相关信息使调试更容易

运算符重载

运算符重载是 Python 魔法方法最强大的功能之一。它允许用户定义对象与 +、-、* 和 == 等运算符一起使用时的行为。这使代码更加直观和易读。

算术运算符

Python 为所有基本算术运算提供了魔术方法。下表显示了哪种方法对应哪种运算符:

操作符 魔法方法 说明
+ __add__ 加法
__sub__ 减法
* __mul__ 乘法
/ __truediv__ 除法
// __floordiv__ 向下取整除法
% __mod__ 求模
** __pow__ 指数运算

比较运算符

同样,可以使用这些魔术方法定义如何比较对象:

操作符 魔法方法 说明
== __eq__ 等于
!= __nq__ 不等于
< __lt__ 小于
> __lt__ 大于
<= __le__ 小于等于
>= __ge__ 大于等于

实际示例:Money 类

让我们创建一个正确处理货币操作的 Money 类。此示例展示了如何实现多个运算符并处理边缘情况:

from functools import total_ordering
from decimal import Decimal

@total_ordering  # Implements all comparison methods based on __eq__ and __lt__
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = Decimal(str(amount))
        self.currency = currency

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot add different currencies: {self.currency} and {other.currency}")
        return Money(self.amount + other.amount, self.currency)

    def __sub__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot subtract different currencies: {self.currency} and {other.currency}")
        return Money(self.amount - other.amount, self.currency)

    def __mul__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount * Decimal(str(other)), self.currency)
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount / Decimal(str(other)), self.currency)
        return NotImplemented

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.currency == other.currency and self.amount == other.amount

    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot compare different currencies: {self.currency} and {other.currency}")
        return self.amount < other.amount

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __repr__(self):
        return f"Money({repr(float(self.amount))}, {repr(self.currency)})"

让我们分析一下 Money 类的主要特性:

  1. 精度处理:我们使用 Decimal 而不是 float 来避免货币计算中的浮点精度问题
  2. 货币安全:该类可防止不同货币之间的操作,以避免错误
  3. 类型检查:每个方法都使用 isinstance() 检查另一个操作数是否属于正确类型
  4. NotImplemented:当某个操作没有意义时,我们返回 NotImplemented 以让 Python 尝试反向操作
  5. @total_ordering:此装饰器自动实现基于 __eq__ 和 __lt__ 的所有比较方法

以下是 Money 类的使用方法:

# Basic arithmetic
wallet = Money(100, "USD")
expense = Money(20, "USD")
remaining = wallet - expense
print(remaining)  # Output: USD 80.00

# Working with different currencies
salary = Money(5000, "USD")
bonus = Money(1000, "USD")
total = salary + bonus
print(total)  # Output: USD 6000.00

# Division by scalar
weekly_pay = salary / 4
print(weekly_pay)  # Output: USD 1250.00

# Comparisons
print(Money(100, "USD") > Money(50, "USD"))  # Output: True
print(Money(100, "USD") == Money(100, "USD"))  # Output: True

# Error handling
try:
    Money(100, "USD") + Money(100, "EUR")
except ValueError as e:
    print(e)  # Output: Cannot add different currencies: USD and EUR

这个 Money 类演示了几个重要概念:

  1. 如何处理不同类型的操作数
  2. 如何实现正确的错误处理
  3. 如何使用 @total_ordering 装饰器
  4. 如何保持财务计算的精度
  5. 如何提供字符串和表示方法

容器方法

容器方法可让用户使对象的行为像内置容器(如列表、字典或集合)一样。当我们需要自定义行为来存储和检索数据时,这尤其有用。

序列协议

要使自定义对象表现得像一个序列(如列表或元组),就需要实现以下方法:

方法 说明 示例
__len__ 返回容器的长度 len(obj)
__getitem__ 允许使用 obj[key] 检索内容 obj[0]
__setitem__ 允许使用 obj[key] = value 设置内容 obj[0] = 42
__delitem__ 允许使用 del obj[key] 进行删除 del obj[0]
__iter__ 返回容器中 item in obj 的迭代器 for item in obj:
__contains__ 在 obj 中实现 in 运算符 42 in obj

映射协议

对于类似字典的行为,需要实现以下方法:

方法 说明 示例
__len__ 返回容器的长度 len(obj)
__getitem__ 允许使用 obj[key] 检索内容 obj[“key”]
__setitem__ 允许使用 obj[“key”] = value 设置内容 obj[“key”] = value
__delitem__ 删除 key 的键值对 del obj[“key”]
__iter__ 返回容器中 key 的迭代器 for key in obj:
__contains__ 在 obj 中实现 in 运算符 “key” in obj

实际示例:自定义缓存

让我们实现一个基于时间的缓存,该缓存会自动使旧条目过期。此示例展示了如何创建一个自定义容器,其行为类似于字典,但具有附加功能:

import time
from collections import OrderedDict

class ExpiringCache:
    def __init__(self, max_age_seconds=60):
        self.max_age = max_age_seconds
        self._cache = OrderedDict()  # {key: (value, timestamp)}

    def __getitem__(self, key):
        if key not in self._cache:
            raise KeyError(key)

        value, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            raise KeyError(f"Key '{key}' has expired")

        return value

    def __setitem__(self, key, value):
        self._cache[key] = (value, time.time())
        self._cache.move_to_end(key)  # Move to end to maintain insertion order

    def __delitem__(self, key):
        del self._cache[key]

    def __len__(self):
        self._clean_expired()  # Clean expired items before reporting length
        return len(self._cache)

    def __iter__(self):
        self._clean_expired()  # Clean expired items before iteration
        for key in self._cache:
            yield key

    def __contains__(self, key):
        if key not in self._cache:
            return False

        _, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            return False

        return True

    def _clean_expired(self):
        """Remove all expired entries from the cache."""
        now = time.time()
        expired_keys = [
            key for key, (_, timestamp) in self._cache.items()
            if now - timestamp > self.max_age
        ]
        for key in expired_keys:
            del self._cache[key]

让我们分析一下这个缓存的工作原理:

  1. 存储:缓存使用 OrderedDict 来存储键值对以及时间戳
  2. 过期:每个值都存储为 (值,时间戳) 的元组。访问值时,我们会检查它是否已过期
  3. 容器方法:该类实现了所有必要的方法,使其像字典一样运行:
    • __getitem__:检索值并检查过期时间
    • __setitem__:使用当前时间戳存储值
    • __delitem__:删除条目
    • __len__:返回未过期条目的数量
    • __iter__:遍历未过期的键
    • __contains__:检查键是否存在

以下是使用缓存的方法:

# Create a cache with 2-second expiration
cache = ExpiringCache(max_age_seconds=2)

# Store some values
cache["name"] = "DAEHUB"
cache["age"] = 30

# Access values
print("name" in cache)  # Output: True
print(cache["name"])    # Output: DAEHUB
print(len(cache))       # Output: 2

# Wait for expiration
print("Waiting for expiration...")
time.sleep(3)

# Check expired values
print("name" in cache)  # Output: False
try:
    print(cache["name"])
except KeyError as e:
    print(f"KeyError: {e}")  # Output: KeyError: 'name'

print(len(cache))  # Output: 0

此缓存实现提供了几个好处:

  1. 旧条目自动过期
  2. 类似字典的界面,易于使用
  3. 通过删除过期条目来提高内存效率
  4. 线程安全操作(假设单线程访问)
  5. 维护条目的插入顺序

属性访问

属性访问方法让用户可以控制对象如何处理获取、设置和删除属性。这对于实现属性、验证和日志记录特别有用。

getattr 和 getattribute

Python 提供了两种控制属性访问的方法:

  1. __getattr__:仅当属性查找失败时调用(即,当属性不存在时)
  2. __getattribute__:每次访问属性时调用,即使属性存在

两个方法的主要区别在于:__getattribute__ 在所有属性访问时都会被调用,而 __getattr__ 仅在通过正常方式无法找到属性时才会被调用。

下面是一个简单的示例,展示了两者的区别:

class AttributeDemo:
    def __init__(self):
        self.name = "DAEHUB"

    def __getattr__(self, name):
        print(f"__getattr__ called for {name}")
        return f"Default value for {name}"

    def __getattribute__(self, name):
        print(f"__getattribute__ called for {name}")
        return super().__getattribute__(name)

demo = AttributeDemo()
print(demo.name)      # Output: __getattribute__ called for name
#        DAEHUB
print(demo.age)       # Output: __getattribute__ called for age
#        __getattr__ called for age
#        Default value for age

setattr 和 delattr

同样,用户可以控制属性的设置和删除方式:

  1. __setattr__:设置属性时调用
  2. __delattr__:删除属性时调用

这些方法允许我们在修改属性时实现验证、日志记录或自定义行为。

实际示例:自动记录属性

让我们创建一个自动记录所有属性更改的类。这对于调试、审核或跟踪对象状态更改很有用:

import logging

# Set up logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class LoggedObject:
    def __init__(self, **kwargs):
        self._data = {}
        # Initialize attributes without triggering __setattr__
        for key, value in kwargs.items():
            self._data[key] = value

    def __getattr__(self, name):
        if name in self._data:
            logging.debug(f"Accessing attribute {name}: {self._data[name]}")
            return self._data[name]
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == "_data":
            # Allow setting the _data attribute directly
            super().__setattr__(name, value)
        else:
            old_value = self._data.get(name, "")
            self._data[name] = value
            logging.info(f"Changed {name}: {old_value} -> {value}")

    def __delattr__(self, name):
        if name in self._data:
            old_value = self._data[name]
            del self._data[name]
            logging.info(f"Deleted {name} (was: {old_value})")
        else:
            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

让我们分析一下这个类的工作原理:

  1. 存储:该类使用私有 _data 字典来存储属性值
  2. 属性访问:
    • __getattr__:从 _data 返回值并记录调试消息
    • __setattr__:将值存储在 _data 中并记录更改
    • __delattr__:从 _data 中删除值并记录删除
  3. 特殊处理:_data 属性本身的处理方式不同,以避免无限递归

该类的使用方法如下:

# Create a logged object with initial values
user = LoggedObject(name="DAE", email="mail@daehub.com")

# Modify attributes
user.name = "DAEHUB"  # Logs: Changed name: DAE -> DAEHUB
user.age = 30         # Logs: Changed age:  -> 30

# Access attributes
print(user.name)      # Output: DAEHUB

# Delete attributes
del user.email        # Logs: Deleted email (was: mail@daehub.com)

# Try to access deleted attribute
try:
    print(user.email)
except AttributeError as e:
    print(f"AttributeError: {e}")  # Output: AttributeError: 'LoggedObject' object has no attribute 'email'

此实现提供了多种好处:

  1. 自动记录所有属性更改
  2. 属性访问的调试级日志记录
  3. 缺少属性的清晰错误消息
  4. 轻松跟踪对象状态更改
  5. 适用于调试和审计

上下文管理器

上下文管理器是 Python 中一项强大的功能,可帮助我们正确管理资源。它们可确保正确获取和释放资源,即使发生错误也是如此。with 语句是使用上下文管理器的最常见方式。

enter 和 exit

要创建上下文管理器,您需要实现两个魔术方法:

  1. __enter__:进入 with 块时调用。它应该返回要管理的资源
  2. __exit__:退出 with 块时调用,即使发生异常。它应该处理清理。

__exit__ 方法接收三个参数:

  • exc_type:异常的类型(如果有)
  • exc_val:异常实例(如果有)
  • exc_tb:回溯(如果有)

实际示例:数据库连接管理器

让我们为数据库连接创建一个上下文管理器。此示例展示了如何正确管理数据库资源和处理事务:

import sqlite3
import logging

# Set up logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class DatabaseConnection:
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
        self.cursor = None

    def __enter__(self):
        logging.info(f"Connecting to database: {self.db_path}")
        self.connection = sqlite3.connect(self.db_path)
        self.cursor = self.connection.cursor()
        return self.cursor

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            logging.error(f"An error occurred: {exc_val}")
            self.connection.rollback()
            logging.info("Transaction rolled back")
        else:
            self.connection.commit()
            logging.info("Transaction committed")

        if self.cursor:
            self.cursor.close()
        if self.connection:
            self.connection.close()

        logging.info("Database connection closed")

        # Return False to propagate exceptions, True to suppress them
        return False
  1. 初始化:
    • 该类采用数据库路径
    • 它将连接和游标初始化为 None
  2. Enter 方法:
    • 创建数据库连接
    • 创建游标
    • 返回用于 with 块的游标
  3. Exit 方法:
    • 处理事务管理(提交/回滚)
    • 关闭游标和连接
    • 记录所有操作
    • 返回 False 以传播异常

以下是如何使用上下文管理器:

# Create a test database in memory
try:
    # Successful transaction
    with DatabaseConnection(":memory:") as cursor:
        # Create a table
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)

        # Insert data
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("DAEHUB", "mail@daehub.com")
        )

        # Query data
        cursor.execute("SELECT * FROM users")
        print(cursor.fetchall())  # Output: [(1, 'DAEHUB', 'mail@daehub.com')]

    # Demonstrate transaction rollback on error
    with DatabaseConnection(":memory:") as cursor:
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("RULTR", "mail@rultr.com")
        )
        # This will cause an error - table 'nonexistent' doesn't exist
        cursor.execute("SELECT * FROM nonexistent")
except sqlite3.OperationalError as e:
    print(f"Caught exception: {e}")

此上下文管理器提供了几个好处:

  1. 资源自动管理(例如:连接始终关闭)
  2. 借助事务安全性,可以适当地提交或回滚更改
  3. 异常被捕获并妥善处理
  4. 所有操作都记录下来以供调试
  5. with 语句使代码清晰简洁

可调用对象

__call__ 魔法方法可让用户使类的实例表现得像函数一样。这对于创建在调用之间保持状态的对象或使用附加功能实现类似函数的行为非常有用。

当我们尝试将类的实例当作函数来调用时,会调用 __call__ 方法。这是一个简单的例子:

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        return x * self.factor

# Create instances that behave like functions
double = Multiplier(2)
triple = Multiplier(3)

print(double(5))  # Output: 10
print(triple(5))  # Output: 15

此示例展示了 __call__ 如何让我们创建可保持状态(因子)且可像函数一样调用的对象。

实际示例:记忆化修饰器

让我们使用 __call__ 实现记忆化修饰器。此修饰器将缓存函数结果以避免冗余计算:

import time
import functools

class Memoize:
    def __init__(self, func):
        self.func = func
        self.cache = {}
        # Preserve function metadata (name, docstring, etc.)
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        # Create a key from the arguments
        # For simplicity, we assume all arguments are hashable
        key = str(args) + str(sorted(kwargs.items()))

        if key not in self.cache:
            self.cache[key] = self.func(*args, **kwargs)

        return self.cache[key]

# Usage
@Memoize
def fibonacci(n):
    """Calculate the nth Fibonacci number recursively."""
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Measure execution time
def time_execution(func, *args, **kwargs):
    start = time.time()
    result = func(*args, **kwargs)
    end = time.time()
    print(f"{func.__name__}({args}, {kwargs}) took {end - start:.6f} seconds")
    return result

# Without memoization, this would be extremely slow
print("Calculating fibonacci(35)...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

# Second call is instant due to memoization
print("\nCalculating fibonacci(35) again...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

让我们分析一下这个记忆化装饰器的工作原理:

  1. 初始化:
    • 将函数作为参数
    • 创建缓存字典来存储结果
    • 使用 functools.update_wrapper 保留函数的元数据
  2. 调用方法:
    • 从函数参数中创建唯一键
    • 检查结果是否在缓存中
    • 如果不在,则计算结果并存储
    • 返回缓存的结果
  3. 用法:
    • 用作任何函数的装饰器
    • 自动缓存重复调用的结果
    • 保留函数元数据和行为

这种实现的好处包括:

  1. 性能更好,因为它避免了冗余计算
  2. 透明度更高,因为它无需修改原始函数即可工作
  3. 它很灵活,可以与任何函数一起使用
  4. 它节省内存并缓存结果以供重复使用
  5. 它维护函数文档

高级魔法方法

现在让我们探索 Python 的一些更高级的魔法方法。这些方法让我们可以对对象创建、内存使用和字典行为进行细粒度的控制。

new 用于对象创建

__new__ 方法在 __init__ 之前调用,负责创建并返回类的新实例。这对于实现单例或不可变对象等模式非常有用。

以下是使用 __new__ 的单例模式示例:

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, name=None):
        # This will be called every time Singleton() is called
        if name is not None:
            self.name = name

# Usage
s1 = Singleton("DAE")
s2 = Singleton("HUB")
print(s1 is s2)  # Output: True
print(s1.name)   # Output: HUB (the second initialization overwrote the first)

让我们分析一下这个单例的工作原理:

  1. 类变量:_instance 存储类的单个实例
  2. new 方法:
    • 检查实例是否存在
    • 如果不存在则创建一个
    • 如果存在则返回现有实例
  3. init 方法:
    • 每次使用构造函数时调用
    • 更新实例的属性

用于内存优化的插槽

__slots__ 类变量限制实例可以具有哪些属性,从而节省内存。当我们拥有具有一组固定属性的类的多个实例时,这尤其有用。

以下是常规类和插槽类的比较:

import sys

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

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

# Compare memory usage
regular_people = [RegularPerson("DAEHUB" + str(i), 30, "mail@daehub.com") for i in range(1000)]
slotted_people = [SlottedPerson("DAEHUB" + str(i), 30, "mail@daehub.com") for i in range(1000)]

print(f"Regular person size: {sys.getsizeof(regular_people[0])} bytes")  # Output: Regular person size: 48 bytes
print(f"Slotted person size: {sys.getsizeof(slotted_people[0])} bytes")  # Output: Slotted person size: 56 bytes
print(f"Memory saved per instance: {sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])} bytes")  # Output: Memory saved per instance: -8 bytes
print(f"Total memory saved for 1000 instances: {(sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])) * 1000 / 1024:.2f} KB")  # Output: Total memory saved for 1000 instances: -7.81 KB

运行此代码会产生一个有趣的结果:

Regular person size: 48 bytes
Slotted person size: 56 bytes
Memory saved per instance: -8 bytes
Total memory saved for 1000 instances: -7.81 KB

令人惊讶的是,在这个简单的示例中,带槽实例实际上比常规实例大 8 个字节!这似乎与关于 __slots__ 节省内存的常见建议相矛盾。

那么这里发生了什么?__slots__ 的真正内存节省来自:

  1. 消除字典:常规 Python 对象将其属性存储在字典 (__dict__) 中,这会产生开销。sys.getsizeof() 函数不考虑此字典的大小
  2. 存储属性:对于属性较少的小对象,槽描述符的开销可能超过字典节省的成本
  3. 可扩展性:真正的好处出现在以下情况下:
    • 如果我们有许多实例(数千或数百万)
    • 如果我们的对象有许多属性
    • 如果我们正在动态添加属性

让我们看一下更完整的比较:

# A more accurate memory measurement
import sys

def get_size(obj):
    """Get a better estimate of the object's size in bytes."""
    size = sys.getsizeof(obj)
    if hasattr(obj, '__dict__'):
        size += sys.getsizeof(obj.__dict__)
        # Add the size of the dict contents
        size += sum(sys.getsizeof(v) for v in obj.__dict__.values())
    return size

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

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

regular = RegularPerson("DAEHUB", 30, "mail@daehub.com")
slotted = SlottedPerson("DAEHUB", 30, "mail@daehub.com")

print(f"Complete Regular person size: {get_size(regular)} bytes")  # Output: Complete Regular person size: 475 bytes
print(f"Complete Slotted person size: {get_size(slotted)} bytes")  # Output: Complete Slotted person size: 56 bytes

通过这种更精确的测量,我们会发现插槽对象通常使用更少的总内存,尤其是在添加更多属性时。

关于 __slots__ 的要点:

  1. 实际内存优势:主要内存节省来自消除实例 __dict__
  2. 动态限制:您不能向插槽对象添加任意属性
  3. 继承注意事项:将 __slots__ 与继承结合使用需要仔细规划
  4. 用例:最适合具有许多实例和固定属性的类
  5. 性能奖励:在某些情况下还可以提供更快的属性访问

默认字典值的缺失

当未找到键时,字典子类会调用 __missing__ 方法。这对于实现具有默认值或自动创建键的字典非常有用。

以下是为缺失键自动创建空列表的字典示例:

class AutoKeyDict(dict):
    def __missing__(self, key):
        self[key] = []
        return self[key]

# Usage
groups = AutoKeyDict()
groups["team1"].append("DAE")
groups["team1"].append("HUB")
groups["team2"].append("DAEHUB")

print(groups)  # Output: {'team1': ['DAE', 'HUB'], 'team2': ['DAEHUB']}

此实现提供了几个好处:

  1. 无需检查键是否存在,这更方便
  2. 自动初始化会根据需要创建默认值
  3. 减少字典初始化的样板
  4. 它更灵活,可以实现任何默认值逻辑
  5. 仅在需要时创建值,使其更节省内存

Python 的魔法方法提供了一种强大的方法,可让用户自定义的类表现得像内置类型,从而实现更直观、更富表现力的代码。在本教程中,我们探讨了这些方法的工作原理以及如何有效地使用它们。

但是,也需要注意,能力越大,责任越大。谨慎使用魔法方法,牢记其性能影响和最小意外原则。如果使用得当,魔法方法可以显著提高代码的可读性和表现力。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注