自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Python `__slots__` 進(jìn)階指南:不止于節(jié)省內(nèi)存,從原理到實踐

開發(fā) 前端
__slots__? 不僅僅是一個性能優(yōu)化工具,它還能幫助我們寫出更清晰、更健壯的代碼。在設(shè)計數(shù)據(jù)密集型應(yīng)用時,合理使用 __slots__ 可以同時獲得性能和代碼質(zhì)量的提升。

相信不少 Python 開發(fā)者都聽說過 __slots__,知道它可以幫助節(jié)省內(nèi)存。但你是否思考過它背后的原理,以及在實際開發(fā)中的其他妙用?讓我們一起深入探討。

從一個性能問題說起

假設(shè)你的一個系統(tǒng)需要處理大量的訂單對象:

class Order:
    def __init__(self, order_id, symbol, price, quantity):
        self.order_id = order_id
        self.symbol = symbol
        self.price = price
        self.quantity = quantity

# 創(chuàng)建100萬個訂單對象
orders = [Order(i, "BTC", 30000, 1) for i in range(1_000_000)]

看起來很普通的代碼,但當(dāng)你用內(nèi)存分析工具一看,這些對象占用的內(nèi)存可能遠(yuǎn)超預(yù)期。為什么?

__dict__ 的開銷

在 Python 中,普通類的實例屬性都存儲在 __dict__ 字典中。這種設(shè)計非常靈活,允許我們動態(tài)添加屬性:

order = Order(1, "BTC", 30000, 1)
order.new_field = "動態(tài)添加的字段"  # 完全合法

但這種靈活性是有代價的:

  • 每個實例都要維護(hù)一個字典
  • 字典本身為了支持快速查找,會預(yù)分配一定的空間
  • 字典的開銷在對象數(shù)量大時會累積成可觀的內(nèi)存消耗

__slots__ 登場

讓我們改造一下 Order 類:

class Order:
    __slots__ = ['order_id', 'symbol', 'price', 'quantity']
    
    def __init__(self, order_id, symbol, price, quantity):
        self.order_id = order_id
        self.symbol = symbol
        self.price = price
        self.quantity = quantity

這個改動帶來了什么變化?

  • 內(nèi)存占用顯著降低(通??梢怨?jié)省 30% 到 50% 的內(nèi)存)
  • 屬性訪問速度提升(因為不需要字典查找)
  • 代碼更加"顯式",所有可能的屬性一目了然

__slots__ 的工作原理

當(dāng)我們使用 __slots__ 時,Python 會:

  1. 在類級別創(chuàng)建一個固定的內(nèi)存布局,類似 C 語言中的結(jié)構(gòu)體
  2. 不再為實例創(chuàng)建 __dict__ 和 __weakref__ 屬性(除非顯式添加到 __slots__ 中)
  3. 將屬性直接存儲在預(yù)分配的固定大小的數(shù)組中,而不是字典里

這帶來了兩個直接的好處:

  • 屬性訪問更快:直接通過數(shù)組偏移量訪問,不需要哈希查找
  • 內(nèi)存占用更少:

沒有 __dict__ 的開銷(每個實例至少節(jié)省一個字典的內(nèi)存)

屬性存儲更緊湊(類似 C 結(jié)構(gòu)體)

沒有哈希表的空間預(yù)留

讓我們用代碼驗證這些優(yōu)勢:

import sys
import time
import tracemalloc


class OrderWithDict:
    def __init__(self, order_id, symbol, price, quantity):
        self.order_id = order_id
        self.symbol = symbol
        self.price = price
        self.quantity = quantity


class OrderWithSlots:
    __slots__ = ['order_id', 'symbol', 'price', 'quantity']
    
    def __init__(self, order_id, symbol, price, quantity):
        self.order_id = order_id
        self.symbol = symbol
        self.price = price
        self.quantity = quantity


def measure_memory_and_speed(cls, n_objects=1_000_000):
    # 啟動內(nèi)存跟蹤
    tracemalloc.start()
    
    # 創(chuàng)建對象
    start_time = time.time()
    objects = [cls(i, "BTC", 30000, 1) for i in range(n_objects)]
    creation_time = time.time() - start_time
    
    # 測量內(nèi)存
    current, peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    
    # 測試屬性訪問速度
    start_time = time.time()
    for obj in objects:
        _ = obj.order_id
        _ = obj.symbol
        _ = obj.price
        _ = obj.quantity
    access_time = time.time() - start_time
    
    return {
        "內(nèi)存占用(MB)": peak / 1024 / 1024,
        "對象創(chuàng)建時間(秒)": creation_time,
        "屬性訪問時間(秒)": access_time
    }


def main():
    # 測試普通類
    print("測試普通類:")
    dict_results = measure_memory_and_speed(OrderWithDict)
    for k, v in dict_results.items():
        print(f"{k}: {v:.2f}")
    
    print("\n測試使用 __slots__ 的類:")
    slots_results = measure_memory_and_speed(OrderWithSlots)
    for k, v in slots_results.items():
        print(f"{k}: {v:.2f}")
    
    # 計算差異百分比
    print("\n性能提升:")
    for k in dict_results:
        improvement = (dict_results[k] - slots_results[k]) / dict_results[k] * 100
        print(f"{k}: 提升 {improvement:.1f}%")

    # 展示單個對象的大小差異
    normal_obj = OrderWithDict(1, "BTC", 30000, 1)
    slots_obj = OrderWithSlots(1, "BTC", 30000, 1)
    print(f"\n單個對象大小對比:")
    print(f"普通對象: {sys.getsizeof(normal_obj)} bytes")
    print(f"普通對象的__dict__: {sys.getsizeof(normal_obj.__dict__)} bytes")
    print(f"普通對象總大小: {sys.getsizeof(normal_obj) + sys.getsizeof(normal_obj.__dict__)} bytes")
    print(f"Slots對象: {sys.getsizeof(slots_obj)} bytes")
    try:
        print(f"Slots對象的__dict__: {sys.getsizeof(slots_obj.__dict__)} bytes")
    except AttributeError as e:
        print(f"Slots對象沒有__dict__屬性:{e}")

if __name__ == "__main__":
    main()

輸出如下:

測試普通類:
內(nèi)存占用(MB): 179.71
對象創(chuàng)建時間(秒): 1.08
屬性訪問時間(秒): 0.08

測試使用 __slots__ 的類:
內(nèi)存占用(MB): 95.79
對象創(chuàng)建時間(秒): 0.67
屬性訪問時間(秒): 0.07

性能提升:
內(nèi)存占用(MB): 提升 46.7%
對象創(chuàng)建時間(秒): 提升 37.5%
屬性訪問時間(秒): 提升 4.8%

單個對象大小對比:
普通對象: 48 bytes
普通對象的__dict__: 104 bytes
普通對象總大小: 152 bytes
Slots對象: 64 bytes
Slots對象沒有__dict__屬性:'OrderWithSlots' object has no attribute '__dict__'

這里注意到,使用了 __slots__ 的類沒有 __dict__ 屬性,這是因為它的屬性是直接存儲在數(shù)組中的。此外,直接對對象進(jìn)行 sizeof 操作,是不包含其 __dict__ 的大小的。

當(dāng)我們使用 sys.getsizeof() 測量單個對象大小時,它只返回對象的直接內(nèi)存占用,而不包括其引用的其他對象(如 __dict__ 中存儲的值)的大小。

不止于節(jié)省內(nèi)存

__slots__ 除了優(yōu)化性能,還能幫助我們寫出更好的代碼:

1. 接口契約

__slots__ 實際上定義了一個隱式的接口契約,明確告訴其他開發(fā)者,“這個類就這些屬性,不多不少”:

class Position:
    __slots__ = ['symbol', 'quantity']
    
    def __init__(self, symbol, quantity):
        self.symbol = symbol
        self.quantity = quantity

這比寫文檔更有效 - 代碼本身就是最好的文檔。

2. 防止拼寫錯誤

position = Position("BTC", 100)
position.quantiy = 200  # 拼寫錯誤,會立即拋出 AttributeError

如果沒有 __slots__,這個錯誤可能潛伏很久才被發(fā)現(xiàn)。

3. 更好的封裝

__slots__ 天然地限制了屬性的隨意添加,這促使我們思考類的設(shè)計是否合理:

class Account:
    __slots__ = ['id', 'balance', '_transactions']
    
    def __init__(self, id):
        self.id = id
        self.balance = 0
        self._transactions = []
    
    def add_transaction(self, amount):
        self._transactions.append(amount)
        self.balance += amount

__slots__ vs @dataclass:該用誰?

既然都是用于數(shù)據(jù)類的定義,@dataclass 和 __slots__ 是什么關(guān)系?讓我們先看一個例子:

from dataclasses import dataclass

# 普通dataclass
@dataclass
class TradeNormal:
    symbol: str
    price: float
    quantity: int

# 帶slots的dataclass
@dataclass
class TradeWithSlots:
    __slots__ = ['symbol', 'price', 'quantity']
    symbol: str
    price: float
    quantity: int

# 結(jié)合使用的推薦方式
@dataclass(slots=True)  # Python 3.10+
class TradeModern:
    symbol: str
    price: float
    quantity: int

關(guān)鍵點解析:

  • 默認(rèn)情況:@dataclass 裝飾器默認(rèn)不會使用 __slots__,每個實例依然會創(chuàng)建 __dict__
  • Python 3.10的改進(jìn):引入了 slots=True 參數(shù),可以自動為 dataclass 啟用 __slots__
  • 動態(tài)添加屬性的陷阱:
@dataclass
class Trade:
    symbol: str
    price: float

trade = Trade("BTC", 30000)
trade.quantity = 1  # 可以,但會創(chuàng)建 __dict__

@dataclass(slots=True)
class TradeLocked:
    symbol: str
    price: float
    
trade_locked = TradeLocked("BTC", 30000)
trade_locked.quantity = 1  # AttributeError!

最佳實踐:@dataclass 和 __slots__ 的協(xié)同使用

  • Python 3.10+ 的推薦用法:
@dataclass(slots=True, frozen=True)
class Position:
    symbol: str
    quantity: int
  • 早期Python版本的替代方案:
@dataclass
class Position:
    __slots__ = ['symbol', 'quantity']
    symbol: str
    quantity: int

如何選擇?

  • 使用 @dataclass(slots=True) 的場景:

類的屬性在定義后不會改變

需要類型提示和自動生成方法

Python 3.10+環(huán)境

注重內(nèi)存效率

  • 使用普通 @dataclass 的場景:

需要動態(tài)添加屬性

使用了某些需要 __dict__ 的庫(如某些ORM)

Python 3.10以下版本

開發(fā)階段,類的結(jié)構(gòu)還在調(diào)整

  • 直接使用 __slots__ 的場景:

極致的性能要求

類的結(jié)構(gòu)非常簡單

不需要dataclass提供的額外功能

注意事項和提示

  • 繼承關(guān)系:
@dataclass(slots=True)
class Parent:
    x: int

@dataclass(slots=True)
class Child(Parent):
    y: int
    # Child會自動繼承Parent的slots
  • 動態(tài)屬性檢查:
@dataclass(slots=True)
class Trade:
    symbol: str
    
    def __setattr__(self, name, value):
        if name not in self.__slots__:
            raise AttributeError(f"Cannot add new attribute '{name}'")
        super().__setattr__(name, value)

此外,某些涉及動態(tài)屬性的特性會受限:

class Frozen:
    __slots__ = ['x']
    
obj = Frozen()
# 以下操作將不可用:
# vars(obj)  # TypeError: vars() argument must have __dict__ attribute
# setattr(obj, 'y', 1)  # AttributeError
  • 性能優(yōu)化建議:

如果確定類的結(jié)構(gòu)不會改變,優(yōu)先使用 @dataclass(slots=True)

在性能關(guān)鍵的代碼路徑上,考慮使用性能分析工具驗證收益

數(shù)據(jù)類(如 DTO)且實例數(shù)量大時,用 __slots__ 是個好選擇

如果類的屬性集合是確定的,使用 __slots__ 可以獲得更好的代碼質(zhì)量

記住:過早優(yōu)化是萬惡之源,先保證代碼正確性和可維護(hù)性

總結(jié)

__slots__ 不僅僅是一個性能優(yōu)化工具,它還能幫助我們寫出更清晰、更健壯的代碼。在設(shè)計數(shù)據(jù)密集型應(yīng)用時,合理使用 __slots__ 可以同時獲得性能和代碼質(zhì)量的提升。

實際工作中,可以先寫普通的類,當(dāng)發(fā)現(xiàn)性能瓶頸或需要更嚴(yán)格的屬性控制時,再考慮引入 __slots__。畢竟,過早優(yōu)化是萬惡之源,而 __slots__ 的使用也確實會帶來一些靈活性的損失。

責(zé)任編輯:武曉燕 來源: Piper蛋窩
相關(guān)推薦

2024-07-07 21:49:22

2024-06-04 09:42:08

2020-02-25 17:40:52

Python循環(huán)內(nèi)存

2024-03-27 10:14:48

2021-08-10 09:04:43

內(nèi)存視圖 NumPy

2021-08-10 13:17:31

NumPy內(nèi)存Python

2011-04-06 14:20:50

Java編程

2017-06-26 09:40:50

Python代碼寫法

2017-07-07 16:57:35

代碼Python

2011-04-13 09:13:02

Java內(nèi)存

2025-04-07 03:02:00

電腦內(nèi)存數(shù)據(jù)

2021-05-11 07:51:30

React ref 前端

2023-02-07 08:55:04

進(jìn)程棧內(nèi)存底層

2023-03-06 08:46:12

2019-11-25 14:06:44

AI無人駕駛自動駕駛

2024-12-12 09:00:28

2018-05-17 15:18:48

Logistic回歸算法機(jī)器學(xué)習(xí)

2025-04-02 07:29:14

2025-03-17 01:55:00

TCP服務(wù)迭代

2024-04-15 16:14:57

點贊
收藏

51CTO技術(shù)棧公眾號