Uurige Pythoni __slots__ funktsiooni, et drastiliselt vähendada mälukasutust ja suurendada atribuutidele juurdepääsu kiirust. Põhjalik juhend võrdlusaluste, kompromisside ja parimate tavadega.
Pythoni __slots__: Süvauurimine mälu optimeerimisse ja atribuutide kiirusesse
Tarkvaraarenduse maailmas on jõudlus ülimalt tähtis. Pythoni arendajate jaoks tähendab see sageli delikaatset tasakaalu keele uskumatult suure paindlikkuse ja ressursitõhususe vajaduse vahel. Üks levinumaid väljakutseid, eriti andmemahukates rakendustes, on mälukasutuse haldamine. Kui loote miljoneid või isegi miljardeid väikeseid objekte, loeb iga bait.
Siin tuleb mängu Pythoni vähem tuntud, kuid võimas funktsioon: __slots__. Seda peetakse sageli maagiliseks lahenduseks mälu optimeerimiseks, kuid selle tõeline olemus on nüansirikkam. Kas see on ainult mälu säästmine? Kas see teeb teie koodi tõesti kiiremaks? Ja millised on selle kasutamise varjatud kulud?
See põhjalik juhend viib teid süvitsi Pythoni __slots__ funktsiooni. Me analüüsime, kuidas tavalised Pythoni objektid kulisside taga töötavad, hindame __slots__ reaalse maailma mõju mälule ja kiirusele, uurime selle üllatavaid keerukusi ja kompromisse ning pakume selge raamistiku otsustamaks, millal – ja millal mitte – seda võimsat optimeerimisvahendit kasutada.
Vaikimisi: Kuidas Pythoni objektid salvestavad atribuute funktsiooniga `__dict__`
Enne kui suudame hinnata, mida __slots__ teeb, peame esmalt mõistma, mida see asendab. Vaikimisi on iga Pythoni kohandatud klassi eksemplaril spetsiaalne atribuut nimega __dict__. See on sõna otseses mõttes sõnastik, mis salvestab kõik eksemplari atribuudid.
Vaatame lihtsat näidet: klass 2D punkti esitamiseks.
import sys
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
# Looge eksemplar
p1 = Point2D(10, 20)
# Atribuudid salvestatakse __dict__ abil
print(p1.__dict__) # Väljund: {'x': 10, 'y': 20}
# Kontrollime __dict__ enda suurust
print(f"Punkti Point2D eksemplari __dict__ suurus: {sys.getsizeof(p1.__dict__)} baiti")
Väljund võib teie Pythoni versioonist ja süsteemi arhitektuurist veidi erineda (nt 64 baiti Python 3.10+ puhul väikese sõnastiku jaoks), kuid peamine järeldus on see, et sellel sõnastikul on oma mälujälg, mis on eraldi eksemplari objektist endast ja selle sisaldatavatest väärtustest.
Paindlikkuse jõud ja hind
See __dict__ lähenemisviis on Pythoni dünaamilisuse nurgakivi. See võimaldab teil igal ajal lisada eksemplarile uusi atribuute, mida sageli nimetatakse "monkey-patching"-uks:
# Lisage lennult uus atribuut
p1.z = 30
print(p1.__dict__) # Väljund: {'x': 10, 'y': 20, 'z': 30}
See paindlikkus on fantastiline kiireks arendamiseks ja teatud programmeerimismustrite jaoks. Kuid sellel on hind: mälu üldkulu.
Pythoni sõnastikud on kõrgelt optimeeritud, kuid on oma olemuselt keerukamad kui lihtsamad andmestruktuurid. Nad peavad säilitama räsiliikluse tabeli, et tagada kiire võtmeotsing, mis nõuab täiendavat mälu potentsiaalsete räsipõrkumiste haldamiseks ja tõhusaks suuruse muutmiseks. Kui loote miljoneid Point2D eksemplare, millest igaüks kannab oma __dict__, koguneb see mälu üldkulu kiiresti.
Kujutage ette rakendust, mis töötleb 3D-mudelit 10 miljoni tipuga. Kui igal tipuobjektil on __dict__ suurusega 64 baiti, siis kulub 640 megabaiti mälu ainult sõnastikele, isegi enne tegelike täisarvu või ujukoma väärtuste arvesse võtmist, mida need salvestavad! See on probleem, mille lahendamiseks __slots__ loodi.
Tutvustame `__slots__`: mälu säästev alternatiiv
__slots__ on klassi muutuja, mis võimaldab teil selgesõnaliselt deklareerida atribuudid, mis eksemplaril on. Määratledes __slots__, ütlete Pythonile sisuliselt: "Selle klassi eksemplaridel on ainult need konkreetsed atribuudid. Te ei pea nende jaoks __dict__ looma."
Sõnastiku asemel reserveerib Python eksemplari jaoks mälus kindla koguse ruumi, just piisavalt, et salvestada osutid deklareeritud atribuutide väärtustele, sarnaselt C structi või ennikuga.
Refaktoreerime oma Point2D klassi, et kasutada __slots__.
class SlottedPoint2D:
# Deklareerige eksemplari atribuudid
# See võib olla ennik (kõige tavalisem), loend või mis tahes stringide iteratiivne objekt.
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
Pealtnäha näeb see peaaegu identne välja. Kuid kulisside taga on kõik muutunud. __dict__ on kadunud.
p_slotted = SlottedPoint2D(10, 20)
# Katse pääseda __dict__ juurde põhjustab vea
try:
print(p_slotted.__dict__)
except AttributeError as e:
print(e) # Väljund: 'SlottedPoint2D' objektil puudub atribuut '__dict__'
Mälu säästu võrdlusalus
Tõeline "vau" moment saabub siis, kui võrdleme mälukasutust. Selle täpseks tegemiseks peame mõistma, kuidas objekti suurust mõõdetakse. sys.getsizeof() teatab objekti põhisuuruse, kuid mitte nende asjade suurust, millele see viitab, nagu __dict__.
import sys
# --- Tavaline klass ---
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
# --- Slotted klass ---
class SlottedPoint2D:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# Looge kumbagi üks eksemplar, et võrrelda
p_normal = Point2D(1, 2)
p_slotted = SlottedPoint2D(1, 2)
# Slotted eksemplari suurus on palju väiksem
# Tavaliselt on see objekti põhisuurus pluss osuti iga pesa jaoks.
size_slotted = sys.getsizeof(p_slotted)
# Tavalise eksemplari suurus sisaldab selle põhisuurust ja osutit selle __dict__ juurde.
# Kogusuurus on eksemplari suurus + __dict__ suurus.
size_normal = sys.getsizeof(p_normal) + sys.getsizeof(p_normal.__dict__)
print(f"Ăśhe SlottedPoint2D eksemplari suurus: {size_slotted} baiti")
print(f"Ühe Point2D eksemplari kogu mälujälg: {size_normal} baiti")
# Nüüd vaatame mõju skaalal
NUM_INSTANCES = 1_000_000
# Päris rakenduses kasutaksite sellist tööriista nagu memory_profiler
# protsessi kogu mälukasutuse mõõtmiseks.
# Saame säästu hinnata meie ühe eksemplari arvutuse põhjal.
size_diff_per_instance = size_normal - size_slotted
total_memory_saved = size_diff_per_instance * NUM_INSTANCES
print(f"\nLoote {NUM_INSTANCES:,} eksemplari...")
print(f"Mälu, mis on säästetud eksemplari kohta, kasutades __slots__: {size_diff_per_instance} baiti")
print(f"Hinnanguline kogu säästetud mälu: {total_memory_saved / (1024*1024):.2f} MB")
Tüüpilises 64-bitises süsteemis võite oodata mälusäästu 40-50% eksemplari kohta. Tavaline objekt võib võtta 16 baiti selle põhja jaoks + 8 baiti __dict__ osuti jaoks + 64 baiti tühja __dict__ jaoks, kokku 88 baiti. Slotted objekt kahe atribuudiga võib võtta ainult 32 baiti. See ~56-baidine erinevus eksemplari kohta tähendab 56 MB säästetud miljoni eksemplari kohta. See ei ole mikro-optimeerimine; see on põhjalik muudatus, mis võib muuta teostamatu rakenduse teostatavaks.
Teine lubadus: Kiirem atribuutidele juurdepääs
Lisaks mälu säästmisele reklaamitakse __slots__ ka jõudluse parandamiseks. Teooria on hea: väärtusele juurdepääs fikseeritud mälu nihkest (nagu massiivi indeks) on kiirem kui räsiotsingu sooritamine sõnastikus.
__dict__Juurdepääs:obj.xhõlmab sõnastiku otsingut võtme'x'jaoks.__slots__Juurdepääs:obj.xhõlmab otsest mälu juurdepääsu kindlale pesale.
Kuid kui palju kiirem see praktikas on? Kasutame Pythoni sisseehitatud timeit moodulit, et teada saada.
import timeit
# Seadistuskood, mida käitatakse üks kord enne ajastamist
SETUP_CODE = """
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint2D:
__slots__ = 'x', 'y'
def __init__(self, x, y):
self.x = x
self.y = y
p_normal = Point2D(1, 2)
p_slotted = SlottedPoint2D(1, 2)
"""
# Testige atribuutide lugemist
read_normal = timeit.timeit("p_normal.x", setup=SETUP_CODE, number=10_000_000)
read_slotted = timeit.timeit("p_slotted.x", setup=SETUP_CODE, number=10_000_000)
print("--- Atribuutide lugemine ---")
print(f"Aeg __dict__ juurdepääsuks: {read_normal:.4f} sekundit")
print(f"Aeg __slots__ juurdepääsuks: {read_slotted:.4f} sekundit")
speedup = (read_normal - read_slotted) / read_normal * 100
print(f"Kiirendus: {speedup:.2f}%")
print("\n--- Atribuutide kirjutamine ---")
# Testige atribuutide kirjutamist
write_normal = timeit.timeit("p_normal.x = 3", setup=SETUP_CODE, number=10_000_000)
write_slotted = timeit.timeit("p_slotted.x = 3", setup=SETUP_CODE, number=10_000_000)
print(f"Aeg __dict__ juurdepääsuks: {write_normal:.4f} sekundit")
print(f"Aeg __slots__ juurdepääsuks: {write_slotted:.4f} sekundit")
speedup = (write_normal - write_slotted) / write_normal * 100
print(f"Kiirendus: {speedup:.2f}%")
Tulemused näitavad, et __slots__ on tõepoolest kiirem, kuid paranemine on tavaliselt vahemikus 10-20%. Kuigi see pole tähtsusetu, on see mälu säästmisega võrreldes palju vähem dramaatiline.
Peamine järeldus: Kasutage __slots__ peamiselt mälu optimeerimiseks. Pidage kiiruse paranemist teretulnud, kuid teisejärguliseks boonuseks. Jõudluse suurenemine on kõige olulisem tihedates silmustes arvutuslikult intensiivsetes algoritmides, kus atribuutidele juurdepääs toimub miljoneid kordi.
Kompromissid ja "Gotchas": Mida kaotate funktsiooniga `__slots__`
__slots__ ei ole tasuta lõuna. Jõudluse suurenemine toimub paindlikkuse hinnaga ja toob kaasa mõningaid keerukusi, eriti pärimise osas. Nende kompromisside mõistmine on ülioluline __slots__ tõhusaks kasutamiseks.
1. DĂĽnaamiliste atribuutide kadu
See on kõige olulisem tagajärg. Atribuutide eelnevalt määratlemisel kaotate võimaluse lisada käitusajal uusi.
p_slotted = SlottedPoint2D(10, 20)
# See toimib hästi
p_slotted.x = 100
# See ebaõnnestub
try:
p_slotted.z = 30 # 'z' ei olnud __slots__
except AttributeError as e:
print(e) # Väljund: 'SlottedPoint2D' objektil puudub atribuut 'z'
See käitumine võib olla funktsioon, mitte viga. See jõustab rangema objektimudeli, hoides ära juhusliku atribuudi loomise ja muutes klassi "kuju" paremini ennustatavaks. Kui aga teie disain tugineb dünaamilisele atribuutide määramisele, on __slots__ mittetoimiv.
2. `__dict__` ja `__weakref__` puudumine
Nagu oleme näinud, hoiab __slots__ ära __dict__ loomise. See võib olla problemaatiline, kui peate töötama teekidega või tööriistadega, mis tuginevad introspektsioonile __dict__ kaudu.
Samamoodi hoiab __slots__ ära ka __weakref__ automaatse loomise, mis on atribuut, mis on vajalik, et objekt oleks nõrgalt viidatav. Nõrgad viited on täiustatud mäluhaldustööriist, mida kasutatakse objektide jälgimiseks, takistamata nende prügikogumist.
Lahendus: Saate selgesõnaliselt lisada '__dict__' ja '__weakref__' oma __slots__ määratlusesse, kui neid vajate.
class HybridSlottedPoint:
# Saame mälu säästu x ja y jaoks, kuid meil on ikka __dict__ ja __weakref__
__slots__ = ('x', 'y', '__dict__', '__weakref__')
def __init__(self, x, y):
self.x = x
self.y = y
p_hybrid = HybridSlottedPoint(5, 10)
p_hybrid.z = 20 # See toimib nĂĽĂĽd, sest __dict__ on olemas!
print(p_hybrid.__dict__) # Väljund: {'z': 20}
import weakref
w_ref = weakref.ref(p_hybrid) # See toimib ka nĂĽĂĽd
print(w_ref)
'__dict__' lisamine annab teile hübriidmudeli. Slotted atribuute (x, y) käsitletakse endiselt tõhusalt, samas kui kõik muud atribuudid paigutatakse __dict__. See eitab mõningaid mälu sääste, kuid võib olla kasulik kompromiss, et säilitada paindlikkus, optimeerides samal ajal kõige tavalisemaid atribuute.
3. Pärimise keerukus
Siin võib __slots__ muutuda keeruliseks. Selle käitumine muutub sõltuvalt sellest, kuidas vanem- ja lapseklassid on määratletud.
Üksikpärimine
-
Kui vanemklassil on
__slots__, kuid lapsel mitte: Lapsklass pärib vanema atribuutide jaoks slotted käitumise, kuid tal on ka oma__dict__. See tähendab, et lapsklassi eksemplarid on suuremad kui vanema eksemplarid.class SlottedBase: __slots__ = ('a',) class DictChild(SlottedBase): # Siin pole __slots__ määratletud def __init__(self): self.a = 1 self.b = 2 # 'b' salvestatakse __dict__ abil c = DictChild() print(f"Lapsel on __dict__: {hasattr(c, '__dict__')}") # Väljund: True print(c.__dict__) # Väljund: {'b': 2} -
Kui nii vanem- kui ka lapseklass määratlevad
__slots__: Lapsklassil ei ole__dict__. Selle efektiivne__slots__on kombinatsioon selle enda__slots__ja selle vanema__slots__.Oluline: Kui vanemaclass SlottedBase: __slots__ = ('a',) class SlottedChild(SlottedBase): __slots__ = ('b',) # Efektiivsed slots on ('a', 'b') def __init__(self): self.a = 1 self.b = 2 sc = SlottedChild() print(f"Lapsel on __dict__: {hasattr(sc, '__dict__')}") # Väljund: False try: sc.c = 3 # Põhjustab AttributeError except AttributeError as e: print(e)__slots__sisaldab atribuuti, mis on loetletud ka lapse__slots__, on see üleliigne, kuid üldiselt kahjutu.
Mitmekordne pärimine
Mitmekordne pärimine koos __slots__ on miiniväli. Reeglid on ranged ja võivad põhjustada ootamatuid vigu.
-
Põhireegel: Selleks, et lapsklass saaks
__slots__tõhusalt kasutada (st ilma__dict__), peavad kõik selle vanemklassid samuti omama__slots__. Kui isegi ühel vanemklassil puudub__slots__(ja seega on__dict__), on ka lapsklassil__dict__. -
`TypeError` lõks: Lapsklass ei saa pärida mitmelt vanemklassilt, millel mõlemal on mittetühi
__slots__.See piirang on olemas, kuna slotted objektide mälupaigutus on klassi loomisel fikseeritud. Python ei saa luua ühtset ja üheselt mõistetavat mälupaigutust, mis ühendaks kahe sõltumatu vanemklassi slotid.class SlotParentA: __slots__ = ('x',) class SlotParentB: __slots__ = ('y',) try: class ProblemChild(SlotParentA, SlotParentB): pass except TypeError as e: print(e) # Väljund: mitmel alusel on eksemplari paigutuse konflikt
Otsus: Millal ja millal mitte kasutada `__slots__`
Omades selget arusaama eelistest ja puudustest, saame luua praktilise otsustusraamistiku.
Rohelised lipud: Kasutage `__slots__`, kui...
- Loote tohutu hulga eksemplare. See on peamine kasutusjuhtum. Kui tegemist on miljonite objektidega, võib mälusääst olla erinevus rakenduse käivitamise ja krahhi vahel.
-
Objekti atribuudid on fikseeritud ja eelnevalt teada.
__slots__sobib suurepäraselt andmestruktuuride, kirjete või lihtsate andmeobjektide jaoks, mille "kuju" ei muutu. - Olete mälupiirangutega keskkonnas. See hõlmab IoT-seadmeid, mobiilirakendusi või suure tihedusega servereid, kus iga megabait on väärtuslik.
-
Optimeerite jõudluse kitsaskohta. Kui profileerimine näitab, et atribuutidele juurdepääs tihedas silmuses on märkimisväärne aeglustumine, võib
__slots__tagasihoidlik kiiruse suurendamine olla seda väärt.
Levinud näited:
- Sõlmed suures graafiku- või puustruktuuris.
- Osakesed fĂĽĂĽsika simulatsioonis.
- Objektid, mis esindavad ridu suurest andmebaasipäringust.
- Sündmus- või teateobjektid suure läbilaskevõimega süsteemis.
Punased lipud: Vältige `__slots__`, kui...
-
Paindlikkus on võtmetähtsusega. Kui teie klass on mõeldud üldiseks kasutamiseks või kui loodate atribuutide dünaamilisele lisamisele (monkey-patching), jääge vaike-
__dict__juurde. -
Teie klass on osa avalikust API-st, mis on mõeldud teiste poolt alamklassideks jaotamiseks.
__slots__kehtestamine baasklassile sunnib kõigile lapsklassidele peale piiranguid, mis võivad olla teie kasutajatele ebameeldiv üllatus. -
Te ei loo piisavalt eksemplare, et sellel oleks tähtsust. Kui teil on ainult mõni sada või tuhat eksemplari, on mälusääst tühine.
__slots__rakendamine siin on ennatlik optimeerimine, mis lisab keerukust ilma reaalse kasuta. -
Tegelete keerukate mitmekordse pärimise hierarhiatega.
TypeErrorpiirangud võivad muuta__slots__neis stsenaariumides rohkem vaeva kui see väärt on.
Kaasaegsed alternatiivid: Kas `__slots__` on endiselt parim valik?
Pythoni ökosüsteem on arenenud ja __slots__ pole enam ainus tööriist kergete objektide loomiseks. Kaasaegse Pythoni koodi puhul peaksite kaaluma neid suurepäraseid alternatiive.
`collections.namedtuple` ja `typing.NamedTuple`
Namedtuples on tehasefunktsioon enniku alamklasside loomiseks nimeliste väljadega. Need on uskumatult mälu säästvad (veelgi enam kui slotted objektid, kuna need on allosas ennikud) ja mis kõige tähtsam, muutumatud.
from typing import NamedTuple
# Loob muutumatu klassi koos tĂĽĂĽbiviidetega
class Point(NamedTuple):
x: int
y: int
p = Point(10, 20)
print(p.x) # 10
try:
p.x = 30 # Põhjustab AttributeError: atribuuti ei saa määrata
except AttributeError as e:
print(e)
Kui vajate muutumatut andmekonteinerit, on NamedTuple sageli parem ja lihtsam valik kui slotted klass.
Mõlema maailma parim: `@dataclass(slots=True)`
Tutvustatud Python 3.7-s ja täiustatud Python 3.10-s, andmeklassid on mängumuutja. Nad genereerivad automaatselt meetodeid nagu __init__, __repr__ ja __eq__, vähendades drastiliselt boilerplati koodi.
Kriitiliselt on @dataclass dekoraatoril argument slots (saadaval alates Python 3.10; Python 3.8-3.9 jaoks on sama mugavuse jaoks vaja kolmanda osapoole teeki). Kui määrate slots=True, genereerib andmeklass automaatselt __slots__ atribuudi, mis põhineb määratletud väljadel.
from dataclasses import dataclass
@dataclass(slots=True)
class DataPoint:
x: int
y: int
dp = DataPoint(10, 20)
print(dp) # Väljund: DataPoint(x=10, y=20) - kena repr tasuta!
print(hasattr(dp, '__dict__')) # Väljund: False - slots on lubatud!
See lähenemisviis annab teile kõigi maailmade parima:
- Loetavus ja lühidus: Palju vähem boilerplati kui käsitsi klassi määratlus.
- Mugavus: Automaatselt genereeritud erimeetodid säästavad teid tavalise boilerplati kirjutamisest.
- Jõudlus:
__slots__täielik mälu ja kiiruse kasu. - Tüüpide ohutus: Integreerub suurepäraselt Pythoni tüübistikuga.
Python 3.10+ kirjutatud uue koodi puhul peaks `@dataclass(slots=True)` olema teie vaikevalik lihtsate, muudetavate ja mälu säästvate andmeid hoidvate klasside loomiseks.
Järeldus: Võimas tööriist konkreetse töö jaoks
__slots__ on tunnistus Pythoni disainifilosoofiast, mis pakub võimsaid tööriistu arendajatele, kes peavad jõudluse piire nihutama. See ei ole funktsioon, mida tuleks kasutada valimatult, vaid pigem terav ja täpne instrument konkreetse ja levinud probleemi lahendamiseks: paljude väikeste objektide kõrge mälukulu.
Teeme kokkuvõtte olulistest tõdedest __slots__ kohta:
- Selle peamine eelis on märkimisväärne mälukasutuse vähenemine, mis vähendab sageli eksemplaride suurust 40-50%. See on selle põhifunktsioon.
- See pakub teisest, tagasihoidlikumat, kiiruse suurenemist atribuutidele juurdepääsuks, tavaliselt umbes 10-20%.
- Peamine kompromiss on dünaamilise atribuudi määramise kadu, mis jõustab jäiga objektistruktuuri.
- See toob kaasa keerukuse pärimisega, nõudes hoolikat kujundust, eriti mitmekordse pärimise stsenaariumides.
-
Kaasaegses Pythonis on `@dataclass(slots=True)` sageli parem ja mugavam alternatiiv, mis ĂĽhendab
__slots__eelised andmeklasside elegantsiga.
Siin kehtib optimeerimise kuldreegel: profileerige kõigepealt. Ärge puistake __slots__ kogu oma koodibaasi, lootes maagilisele kiiruse suurenemisele. Kasutage mälu profileerimise tööriistu, et tuvastada, millised objektid tarbivad kõige rohkem mälu. Kui leiate klassi, mida luuakse miljoneid kordi ja mis on peamine mälu röövel, siis – ja alles siis – on aeg haarata __slots__ järele. Mõistes selle jõudu ja ohte, saate seda tõhusalt kasutada tõhusamate ja skaleeritavamate Pythoni rakenduste loomiseks ülemaailmsele vaatajaskonnale.