IlmHamroh
Python kursi/Obyekt modeli va xotira5/10-dars25 daqiqa
Mundarija (20)

11.5-dars: Axlat yig'uvchi (GC) va sikllar

11-QISM — OBYEKT MODELI VA XOTIRA · 5-dars


1. Kirish va motivatsiya

11.4-darsda CPython obyektni havolalar soni nolga tushgan zahoti o'chirishini ko'rdik. Lekin bu mexanizm bitta holatda ojiz: obyektlar bir-biriga ishora qilsa.

python
class Tugun:
    def __init__(self, nom, ota=None):
        self.nom = nom
        self.ota = ota
        self.bolalar = []
        if ota:
            ota.bolalar.append(self)

ildiz = Tugun("html")
body = Tugun("body", ildiz)
del ildiz, body          # ildiz → body (bolalar), body → ildiz (ota) — sikl

del dan keyin ikkala tugunga ham tashqaridan havola yo'q, lekin har birining havolalar soni 1: ular bir-birini ushlab turadi. Havolalar hisobi ularni hech qachon o'chirmaydi.

Real hodisa: HTML parser har sahifa uchun shunday daraxt quradi. Servis gc.disable() bilan "tezlashtirilgan" edi — bir necha soatda xotira tugadi. Boshqa servisda esa aksincha: GC yoqilgan, lekin har bir necha soniyada 200 ms lik to'xtashlar paydo bo'ldi — sababi millionlab kichik obyektlarni qayta-qayta tekshiruvchi to'liq yig'ish edi.

CPython'da ikkita mexanizm birga ishlaydi: havolalar hisobi — ko'p obyektni darhol o'chiradi, sikl yig'uvchi (gc) — faqat bir-biriga ishora qiluvchi konteynerlar guruhini vaqti-vaqti bilan topadi. Ikkinchisining qanday ishlashi, qachon ishga tushishi va uni qachon sozlash kerakligini bilish — xotira va kechikish muammolarini hal qilishning kaliti.

Bu darsda:

  • Sikllar qayerdan paydo bo'ladi — ko'rinadigan va yashirin sikllar
  • Sikl yig'uvchi qanday ishlaydi: ichki havolalarni ayirish algoritmi
  • Avlodlar, chegaralar va avtomatik yig'ish
  • gc moduli: collect, disable, freeze, callbacks, get_stats
  • Sikl ichidagi __del__ (PEP 442) va gc.garbage
  • Sikllarni oldini olish: weakref, aniq tozalash, istisnolar

2. Nazariya — chuqur tushuntirish

2.1. Sikllar qayerdan paydo bo'ladi

Manba Sikl
Ota-bola havolalari bola.ota = ota, ota.bolalar.append(bola)
Ikki tomonlama bog'langan ro'yxat tugun.oldingi, tugun.keyingi
Graf tugunlari Qo'shnilar bir-biriga ishora qiladi
O'ziga havola obj.men = obj, royxat.append(royxat)
Istisno lokal o'zgaruvchida xato = e → e.__traceback__ → freym → xato
Bog'langan metod atributda self.ishlovchi = self.metod → __self__ → self
O'zini chaqiruvchi ichki funksiya def yur(): yur() → hujayra → funksiya (10.12-dars)
Klass va uning metodlari Funksiya → __globals__ → modul lug'ati → klass

Yashirin sikllar () ayniqsa xavfli: kodda hech qanday "ota/bola" ko'rinmaydi.

2.2. Sikl yig'uvchi qanday ishlaydi

GC faqat konteyner obyektlarni kuzatadi — boshqa obyektlarga havola saqlay oladiganlarni (list, dict, klass nusxalari, funksiyalar...). int, str, float kuzatilmaydi (gc.is_tracked). Faqat atomar qiymatlardan tuzilgan tuple esa yaratilishida yoki birinchi GC tekshiruvida kuzatuvdan chiqariladi — shuning uchun gc.is_tracked natijasi uning qanday yaratilganiga qarab farq qilishi mumkin.

Bir avlodni yig'ish algoritmi (soddalashtirilgan):

  1. Har kuzatilayotgan obyekt uchun gc_refs = ob_refcnt nusxasi olinadi
  2. Har obyektning ichidagi havolalari bo'ylab yuriladi — ishora qilingan obyektning gc_refs i 1 ga kamaytiriladi
  3. Endi gc_refs > 0 bo'lgan obyektlarga guruh tashqarisidan havola bor — ular va ulardan erishiladigan hamma narsa tirik
  4. Qolganlari — faqat bir-birini ushlab turuvchi erishib bo'lmaydigan guruh
  5. Ular uchun: zaif havolalar tozalanadi, __del__ chaqiriladi (bir marta), keyin ichki havolalar uziladi — havolalar hisobi ularni o'chiradi
tashqi nom ──► A ◄──► B        C ◄──► D
                                 (tashqaridan havola yo'q)
A: refcnt 2, ichki 1 → gc_refs 1 → tirik (B ham tirik)
C: refcnt 1, ichki 1 → gc_refs 0 → erishib bo'lmaydi

2.3. Avlodlar va avtomatik yig'ish

"Ko'p obyektlar yosh o'ladi" gipotezasi asosida uch avlod:

Avlod Kim Qachon yig'iladi (Python 3.14, sukut)
0 Yangi yaratilgan konteynerlar Ajratishlar − o'chirishlar > 2000
1 0-avloddan omon qolganlar 0-avlod 10 marta yig'ilgach
2 1-avloddan omon qolganlar 1-avlod 10 marta yig'ilgach (+ qo'shimcha shart)
  • Yig'ishdan omon qolgan obyekt keyingi avlodga o'tadi
  • 2-avlodni (to'liq) yig'ish uzoq yashaydigan obyektlar ko'paygandagina ishga tushadi — millionlab eski obyektlarni tez-tez tekshirmaslik uchun
  • gc.get_count() — joriy hisoblagichlar, gc.get_threshold() — chegaralar

Avtomatik yig'ish ixtiyoriy konteyner ajratilganda boshlanishi mumkin — ya'ni dasturning ixtiyoriy joyida, ixtiyoriy ipda. To'liq yig'ish katta xotirada sezilarli pauza beradi.

2.4. gc moduli

Funksiya Nima qiladi
gc.collect(avlod=2) Qo'lda yig'ish; topilgan erishib bo'lmaydigan obyektlar sonini qaytaradi
gc.disable() / gc.enable() / gc.isenabled() Avtomatik yig'ishni o'chirish/yoqish (havolalar hisobi ishlayveradi)
gc.get_threshold() / gc.set_threshold(a, b, c) Chegaralar
gc.get_count() Avlod hisoblagichlari
gc.get_stats() Har avlod: yig'ishlar, yig'ilgan, yig'ilmaydigan
gc.callbacks Yig'ish boshlanishi/tugashida chaqiriladigan funksiyalar ro'yxati
gc.freeze() / gc.unfreeze() Hozirgi barcha obyektlarni "doimiy avlodga" o'tkazish — ular yig'ishda tekshirilmaydi
gc.get_referrers, gc.get_referents Havolalar grafi (11.4-dars)
gc.is_tracked(obj), gc.is_finalized(obj) Kuzatiladimi, __del__ chaqirilganmi
gc.garbage, gc.set_debug(gc.DEBUG_SAVEALL) Tuzatish: yig'ilgan obyektlarni o'chirmasdan saqlash

2.5. Sikl ichidagi __del__

Python 3.4 dan (PEP 442) __del__ li obyektlar sikli ham yig'iladi: avval hammasining __del__ i chaqiriladi, keyin, agar hech biri tirilmagan bo'lsa, sikl uziladi. Tartib — aniqlanmagan.

gc.garbage endi odatda bo'sh. U faqat C kengaytmalardagi ba'zi obyektlar yoki DEBUG_SAVEALL bilan to'ladi.

Sikldagi __del__ ichida boshqa sikl a'zolariga murojaat qilish xavfli: ular allaqachon "tozalanib" bo'lishi mumkin.

2.6. GC ni qachon sozlash kerak

Vaziyat Chora
Sikllar yo'q, lekin ajratish ko'p (ma'lumot yuklash) Yuklash davomida gc.disable(), keyin gc.enable() va gc.collect()
Ko'p uzoq yashaydigan obyektlar (katta keshlar, modellar) Yuklangach gc.freeze() — to'liq yig'ish ularni qayta tekshirmaydi
fork qiluvchi serverlar (gunicorn preload) Ishga tushgach gc.freeze() — GC eski obyektlar sahifalariga yozmaydi, copy-on-write saqlanadi
Kechikishga sezgir yo'l Chegaralarni oshirish; yig'ishni boshqariladigan paytga surish (gc.disable + davriy collect)
Tushunarsiz xotira o'sishi gc.callbacks bilan yig'ishlarni kuzatish; sikl manbalarini weakref bilan uzish

gc.disable() ni sikllar mavjud dasturda doimiy qoldirish — xotira sizishi.

Eng yaxshi yechim ko'pincha GC ni sozlash emas, sikl yaratmaslik:

  • ota havolasini weakref.ref(ota) qilish
  • istisnoni lokal o'zgaruvchida saqlamaslik (yoki del xato / finally)
  • self.ishlovchi = self.metod o'rniga WeakMethod yoki funksiya
  • ish tugagach aniq clear() / close()

3. Tez ma'lumotnoma

Sikl manbalari

ota ↔ bola    oldingi ↔ keyingi    obj.men = obj
xato = e (freym ↔ traceback)    self.f = self.metod    rekursiv ichki funksiya

Algoritm

gc_refs = refcnt − ichki havolalar
gc_refs > 0 → tirik (va undan erishiladiganlar)
qolganlar → weakref tozalash → __del__ (bir marta) → havolalarni uzish

Avlodlar (3.14)

threshold (2000, 10, 10)    omon qolgan → keyingi avlod
gc.get_count()    gc.get_stats()    gc.callbacks

Sozlash

python
gc.disable(); yukla(); gc.enable(); gc.collect()
gc.freeze()                       # init / fork dan oldin
gc.set_threshold(50_000, 20, 20)  # ajratish ko'p, sikl kam

Oldini olish

weakref.ref(ota)    del xato / finally    WeakMethod    clear()/close()

4. Batafsil misollar

Misol 1 — Havolalar hisobi yetmaydigan sikllar

python
"""O'ziga havola, ota-bola, ikki tomonlama ro'yxat, istisno-freym va bog'langan metod sikllari; gc.collect topadi."""

import gc
import weakref

gc.collect()
gc.disable()                                   # avtomatik yig'ish natijani aralashtirmasin


class Tugun:
    def __init__(self, nom):
        self.nom = nom


def tekshir(tavsif, yaratuvchi):
    havolalar = yaratuvchi()
    tirik_oldin = sum(h() is not None for h in havolalar)
    topildi = gc.collect()
    tirik_keyin = sum(h() is not None for h in havolalar)
    print(f"  {tavsif:34} del dan keyin tirik: {tirik_oldin}/{len(havolalar)}, "
          f"gc.collect() → {topildi:>2} ta, keyin tirik: {tirik_keyin}")


def siklsiz():
    a, b = Tugun("a"), Tugun("b")
    a.b = b
    return [weakref.ref(a), weakref.ref(b)]


def ozi_havola():
    a = Tugun("a")
    a.men = a
    return [weakref.ref(a)]


def ota_bola():
    ota = Tugun("ota")
    ota.bolalar = [Tugun(f"bola{i}") for i in range(3)]
    for b in ota.bolalar:
        b.ota = ota
    return [weakref.ref(ota)] + [weakref.ref(b) for b in ota.bolalar]


def ikki_tomonlama():
    tugunlar = [Tugun(i) for i in range(5)]
    for chap, ong in zip(tugunlar, tugunlar[1:]):
        chap.keyingi, ong.oldingi = ong, chap
    return [weakref.ref(t) for t in tugunlar]


def istisno_freym():
    katta = Tugun("katta ma'lumot")
    try:
        raise ValueError("xato")
    except ValueError as e:
        xato = e                                # freym → xato → traceback → freym
    return [weakref.ref(katta)]


def istisno_freym_tozalangan():
    katta = Tugun("katta ma'lumot")
    try:
        raise ValueError("xato")
    except ValueError as e:
        xato = e
    del xato                                    # sikl uzildi
    return [weakref.ref(katta)]


class Tugma:
    def __init__(self):
        self.bosilganda = self.bos              # self → metod → __self__ → self

    def bos(self):
        return "bosildi"


def bogl_metod():
    return [weakref.ref(Tugma())]


print("=== Qaysi tuzilmalar sikl hosil qiladi ===")
for tavsif, f in [("sikl yo'q (a → b)", siklsiz), ("o'ziga havola", ozi_havola),
                  ("ota ↔ 3 bola", ota_bola), ("ikki tomonlama ro'yxat (5)", ikki_tomonlama),
                  ("⚠️ istisno lokal o'zgaruvchida", istisno_freym),
                  ("✅ del xato bilan", istisno_freym_tozalangan),
                  ("⚠️ self.f = self.metod", bogl_metod)]:
    tekshir(tavsif, f)

print("\n=== Sikl ichidagi __del__ (PEP 442) ===")


class Resurs:
    def __init__(self, nom):
        self.nom = nom

    def __del__(self):
        print(f"    __del__ {self.nom}")


x, y = Resurs("x"), Resurs("y")
x.y, y.x = y, x
del x, y
print("  del dan keyin — hech narsa chaqirilmadi")
print(f"  gc.collect() → {gc.collect()}, gc.garbage: {gc.garbage}")
gc.enable()

Natijaning muhim qismi:

text
=== Qaysi tuzilmalar sikl hosil qiladi ===
  sikl yo'q (a → b)                  del dan keyin tirik: 0/2, gc.collect() →  0 ta, keyin tirik: 0
  o'ziga havola                      del dan keyin tirik: 1/1, gc.collect() →  1 ta, keyin tirik: 0
  ota ↔ 3 bola                       del dan keyin tirik: 4/4, gc.collect() →  5 ta, keyin tirik: 0
  ikki tomonlama ro'yxat (5)         del dan keyin tirik: 5/5, gc.collect() →  5 ta, keyin tirik: 0
  ⚠️ istisno lokal o'zgaruvchida     del dan keyin tirik: 1/1, gc.collect() →  4 ta, keyin tirik: 0
  ✅ del xato bilan                   del dan keyin tirik: 0/1, gc.collect() →  0 ta, keyin tirik: 0
  ⚠️ self.f = self.metod             del dan keyin tirik: 1/1, gc.collect() →  2 ta, keyin tirik: 0

=== Sikl ichidagi __del__ (PEP 442) ===
  del dan keyin — hech narsa chaqirilmadi
    __del__ x
    __del__ y
  gc.collect() → 2, gc.garbage: []

Nima ko'rsatdi: 2.1, 2.5-bo'limlar. "Topildi" soni sikldagi barcha kuzatiladigan obyektlarni — nusxalar, ro'yxatlar, freym va traceback'ni (3.14 da nusxa atributlari alohida __dict__ obyektisiz saqlanadi) — o'z ichiga oladi.

Misol 2 — Algoritmni qo'lda bajarish

python
"""gc_refs = refcnt − ichki havolalar: kichik graf uchun erishib bo'lmaydigan obyektlarni topish va gc bilan solishtirish."""

import ctypes
import gc
import weakref

gc.disable()


class Obyekt:
    def __init__(self, nom):
        self.nom = nom
        self.havolalar = []

    def __repr__(self):
        return self.nom


def refcnt(o):
    return ctypes.c_ssize_t.from_address(id(o)).value


A, B, C, D, E = (Obyekt(n) for n in "ABCDE")
A.havolalar.append(B)
B.havolalar.append(A)
C.havolalar.append(D)
D.havolalar.append(C)
D.havolalar.append(E)
tashqi = [A]
guruh = [A, B, C, D, E]
nomlar = {id(o): o.nom for o in guruh}
zaif = {o.nom: weakref.ref(o) for o in guruh}


def ichki_obyektlar(o):
    """Obyekt → __dict__ → havolalar ro'yxati → obyektlar (bir qadamda ochib beramiz)."""
    return list(o.havolalar)


print("=== 1. Graf ===")
for o in guruh:
    print(f"  {o.nom} → {ichki_obyektlar(o)}")
print("  tashqaridan: tashqi ro'yxat → A")

del A, B, C, D, E, o, guruh


def gc_refs_hisobla():
    """Hisoblashning o'zi yaratgan vaqtinchalik havolalarni nazorat obyekti bilan o'lchab, ayiramiz."""
    # nazorat obyektining tashqi havolasi yo'q — unda faqat o'lchovning o'z havolalari bo'ladi
    obyektlar = [z() for z in zaif.values()] + [Obyekt("nazorat")]
    xom = {x.nom: refcnt(x) for x in obyektlar}
    ortiqcha = xom.pop("nazorat")
    natija = {nom: qiymat - ortiqcha for nom, qiymat in xom.items()}
    for x in obyektlar[:-1]:
        for ichki in ichki_obyektlar(x):
            natija[ichki.nom] -= 1
    return natija, ortiqcha


print("\n=== 2. gc_refs hisoblash ===")
gc_refs, ortiqcha = gc_refs_hisobla()
print(f"  o'lchovning o'z havolalari (nazorat obyektida): {ortiqcha}")
print(f"  refcnt − o'lchov − ichki havolalar: {gc_refs}")

tirik = set()
navbat = [nom for nom, qiymat in gc_refs.items() if qiymat > 0]
while navbat:
    nom = navbat.pop()
    if nom in tirik:
        continue
    tirik.add(nom)
    navbat.extend(i.nom for i in ichki_obyektlar(zaif[nom]()))
erishib_bolmaydi = sorted(set(gc_refs) - tirik)
print(f"  tashqaridan havolasi bor: {sorted(n for n, q in gc_refs.items() if q > 0)}")
print(f"  tirik (erishiladigan): {sorted(tirik)}")
print(f"  bizning bashorat — o'chiriladi: {erishib_bolmaydi}")

print("\n=== 3. Haqiqiy gc bilan solishtirish ===")
gc.collect()
haqiqiy = sorted(n for n, z in zaif.items() if z() is None)
print(f"  gc o'chirdi: {haqiqiy}, bashorat bilan bir xil: {haqiqiy == erishib_bolmaydi}")
del tashqi
gc.collect()
print(f"  tashqi havola olib tashlangach: o'lganlar {sorted(n for n, z in zaif.items() if z() is None)}")
gc.enable()

Natijaning muhim qismi:

text
=== 1. Graf ===
  A → [B]
  B → [A]
  C → [D]
  D → [C, E]
  E → []
  tashqaridan: tashqi ro'yxat → A

=== 2. gc_refs hisoblash ===
  o'lchovning o'z havolalari (nazorat obyektida): 2
  refcnt − o'lchov − ichki havolalar: {'A': 1, 'B': 0, 'C': 0, 'D': 0, 'E': 0}
  tashqaridan havolasi bor: ['A']
  tirik (erishiladigan): ['A', 'B']
  bizning bashorat — o'chiriladi: ['C', 'D', 'E']

=== 3. Haqiqiy gc bilan solishtirish ===
  gc o'chirdi: ['C', 'D', 'E'], bashorat bilan bir xil: True
  tashqi havola olib tashlangach: o'lganlar ['A', 'B', 'C', 'D', 'E']

Nima ko'rsatdi: 2.2-bo'lim. E sikl a'zosi emas, lekin faqat D orqali ushlangani uchun u ham C–D sikli bilan birga o'ldi; A–B sikli esa tashqi havola tufayli tirik qoldi.

Misol 3 — Avlodlar, chegaralar va freeze

python
"""gc.get_count o'zgarishi, avtomatik yig'ishlarni callbacks bilan sanash, chegarani oshirish, disable va freeze ta'siri."""

import gc
import time
import weakref


class Tugun:
    pass


print("=== 1. Hisoblagichlar ===")
gc.collect()
print(f"  chegaralar: {gc.get_threshold()}, collect dan keyin: {gc.get_count()}")
vaqtincha = [[] for _ in range(1500)]
print(f"  1500 ro'yxat yaratildi: {gc.get_count()}")
vaqtincha2 = [[] for _ in range(1000)]
print(f"  yana 1000 ta (chegara 2000 dan oshdi): {gc.get_count()}   — 0-avlod yig'ildi, 1-avlodga +1")
del vaqtincha, vaqtincha2

print("\n=== 2. Avtomatik yig'ishlarni sanash ===")


def sana(chegara, soni=100_000):
    gc.collect()
    eski = gc.get_threshold()
    gc.set_threshold(*chegara)
    avlodlar = [0, 0, 0]

    def kuzat(bosqich, malumot):
        if bosqich == "start":
            avlodlar[malumot["generation"]] += 1

    gc.callbacks.append(kuzat)
    saqlanadi = [[i] for i in range(soni)]
    gc.callbacks.remove(kuzat)
    gc.set_threshold(*eski)
    return avlodlar


for chegara in [(2000, 10, 10), (50_000, 20, 20)]:
    print(f"  chegara {chegara}: 100 000 ro'yxat davomida yig'ishlar (0, 1, 2-avlod) = {sana(chegara)}")

print("\n=== 3. ⚠️ gc.disable() va sikllar ===")
gc.collect()
gc.disable()
havolalar = []
for i in range(1000):
    t = Tugun()
    t.men = t
    havolalar.append(weakref.ref(t))
del t
print(f"  1000 ta o'ziga havolali obyekt, GC o'chiq: tirik {sum(h() is not None for h in havolalar)}")
gc.enable()
gc.collect()
print(f"  gc.enable() + collect: tirik {sum(h() is not None for h in havolalar)}")

print("\n=== 4. freeze: uzoq yashaydigan obyektlarni tekshiruvdan chiqarish ===")
katta_kesh = [{"id": i, "teglar": [i]} for i in range(300_000)]


def olchov():
    bosh = time.perf_counter()
    gc.collect()
    return time.perf_counter() - bosh


oddiy = min(olchov() for _ in range(3))
gc.freeze()
muzlatilgan = min(olchov() for _ in range(3))
print(f"  muzlatilgan obyektlar: {gc.get_freeze_count() > 300_000}")
print(f"  freeze dan keyin to'liq yig'ish kamida 5 marta tezroq: {oddiy > 5 * muzlatilgan}")
gc.unfreeze()
del katta_kesh
gc.collect()

Natijaning muhim qismi:

text
=== 1. Hisoblagichlar ===
  chegaralar: (2000, 10, 10), collect dan keyin: (4, 0, 0)
  1500 ro'yxat yaratildi: (1506, 0, 0)
  yana 1000 ta (chegara 2000 dan oshdi): (506, 1, 0)   — 0-avlod yig'ildi, 1-avlodga +1

=== 2. Avtomatik yig'ishlarni sanash ===
  chegara (2000, 10, 10): 100 000 ro'yxat davomida yig'ishlar (0, 1, 2-avlod) = [45, 4, 0]
  chegara (50000, 20, 20): 100 000 ro'yxat davomida yig'ishlar (0, 1, 2-avlod) = [2, 0, 0]

=== 3. ⚠️ gc.disable() va sikllar ===
  1000 ta o'ziga havolali obyekt, GC o'chiq: tirik 1000
  gc.enable() + collect: tirik 0

=== 4. freeze: uzoq yashaydigan obyektlarni tekshiruvdan chiqarish ===
  muzlatilgan obyektlar: True
  freeze dan keyin to'liq yig'ish kamida 5 marta tezroq: True

Nima ko'rsatdi: 2.3, 2.4, 2.6-bo'limlar. Chegara oshirilganda 0-avlod yig'ishlari keskin kamaydi; freeze qilingan katta kesh to'liq yig'ishda qayta tekshirilmadi.

Misol 4 — Amaliy: sikllarsiz daraxt

HTML-ga o'xshash hujjat daraxti: har tugun bolalarini va otasini biladi. Ikki versiya — oddiy ota havolasi va weakref — GC o'chiq holatda sahifalarni qayta ishlaganda xotirada qancha tugun qolishini solishtiramiz.

python
"""Ota havolasi: kuchli (sikl) va zaif (weakref.ref); GC o'chiq holatda sizish; property orqali qulay API; tozalash."""

import gc
import weakref


class KuchliTugun:
    tiriklar = weakref.WeakSet()

    def __init__(self, teg, ota=None):
        self.teg = teg
        self.ota = ota
        self.bolalar = []
        if ota is not None:
            ota.bolalar.append(self)
        KuchliTugun.tiriklar.add(self)

    def yol(self):
        qismlar, t = [], self
        while t is not None:
            qismlar.append(t.teg)
            t = t.ota
        return "/".join(reversed(qismlar))


class ZaifTugun:
    tiriklar = weakref.WeakSet()

    def __init__(self, teg, ota=None):
        self.teg = teg
        self._ota = weakref.ref(ota) if ota is not None else None
        self.bolalar = []
        if ota is not None:
            ota.bolalar.append(self)
        ZaifTugun.tiriklar.add(self)

    @property
    def ota(self):
        return self._ota() if self._ota is not None else None

    def yol(self):
        qismlar, t = [], self
        while t is not None:
            qismlar.append(t.teg)
            t = t.ota
        return "/".join(reversed(qismlar))


def sahifa_qur(klass, raqam):
    html = klass("html")
    body = klass("body", html)
    for i in range(20):
        div = klass(f"div{i}", body)
        klass("p", div)
        klass("a", div)
    return html.bolalar[0].bolalar[raqam % 20].bolalar[1].yol()


print("=== 1. Bir xil API ===")
for klass in (KuchliTugun, ZaifTugun):
    print(f"  {klass.__name__}: {sahifa_qur(klass, 7)}")

print("\n=== 2. GC o'chiq holatda 100 ta sahifa ===")
gc.collect()
gc.disable()
for klass in (KuchliTugun, ZaifTugun):
    for i in range(100):
        sahifa_qur(klass, i)
    print(f"  {klass.__name__:12} xotirada qolgan tugunlar: {len(klass.tiriklar):>5}   (bir sahifa — 62 tugun)")
gc.enable()
topildi = gc.collect()
print(f"  gc.collect(): {topildi} ta obyekt yig'ildi → KuchliTugun qoldi: {len(KuchliTugun.tiriklar)}")

print("\n=== 3. ⚠️ Zaif ota — daraxt ildizini ushlab turish kerak ===")
bola = ZaifTugun("span", ZaifTugun("div"))
print(f"  ota vaqtinchalik edi: bola.ota = {bola.ota}, yol = {bola.yol()!r}")
div = ZaifTugun("div")
bola2 = ZaifTugun("span", div)
print(f"  ota nomda saqlandi: yol = {bola2.yol()!r}")

print("\n=== 4. Kuzatuv: yig'ishlarni callbacks bilan jurnallash ===")
jurnal = []


def kuzat(bosqich, malumot):
    if bosqich == "stop" and malumot["collected"]:
        jurnal.append((malumot["generation"], malumot["collected"]))


gc.collect()
gc.callbacks.append(kuzat)
for i in range(200):
    sahifa_qur(KuchliTugun, i)
gc.collect()
gc.callbacks.remove(kuzat)
yigilgan = sum(s for _, s in jurnal)
print(f"  200 kuchli sahifa: {len(jurnal)} ta yig'ish obyekt topdi, jami {yigilgan} ta obyekt")
print(f"  avlodlar bo'yicha: { {a: sum(s for g, s in jurnal if g == a) for a in (0, 1, 2)} }")

Natijaning muhim qismi:

text
=== 1. Bir xil API ===
  KuchliTugun: html/body/div7/a
  ZaifTugun: html/body/div7/a

=== 2. GC o'chiq holatda 100 ta sahifa ===
  KuchliTugun  xotirada qolgan tugunlar:  6200   (bir sahifa — 62 tugun)
  ZaifTugun    xotirada qolgan tugunlar:     0   (bir sahifa — 62 tugun)
  gc.collect(): 18600 ta obyekt yig'ildi → KuchliTugun qoldi: 0

=== 3. ⚠️ Zaif ota — daraxt ildizini ushlab turish kerak ===
  ota vaqtinchalik edi: bola.ota = None, yol = 'span'
  ota nomda saqlandi: yol = 'div/span'

=== 4. Kuzatuv: yig'ishlarni callbacks bilan jurnallash ===
  200 kuchli sahifa: 18 ta yig'ish obyekt topdi, jami 37200 ta obyekt
  avlodlar bo'yicha: {0: 30318, 1: 3906, 2: 2976}

Nima ko'rsatdi: 2.1, 2.6-bo'limlar — ota havolasini zaif qilish sikllarni butunlay yo'q qildi: tugunlar GC o'chiq bo'lsa ham darhol o'ldi. Zaif ota bilan esa ildizni biror nom ushlab turishi kerak.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"Python xotirasini faqat GC boshqaradi" Ko'p obyekt havolalar hisobi bilan darhol o'ladi; GC — faqat sikllar uchun
"gc.disable() barcha tozalashni to'xtatadi" Faqat sikl yig'uvchini; havolalar hisobi ishlayveradi
"GC hamma obyektlarni tekshiradi" Faqat kuzatiladigan konteynerlarni, avlodlar bo'yicha
"__del__ li sikllar yig'ilmaydi va gc.garbage ga tushadi" 3.4 dan yig'iladi (PEP 442)
"Sikl bo'lsa, del bilan obyektlar o'ladi" Faqat keyingi GC yig'ishida
"Istisnoni lokal o'zgaruvchida saqlash zararsiz" Freym ↔ traceback sikli
"GC pauzalari kichik, ahamiyatsiz" Millionlab uzoq yashaydigan obyektda to'liq yig'ish sezilarli
"gc.collect() ni tez-tez chaqirish xotirani tejaydi" Sikllar yo'q bo'lsa — faqat CPU sarfi

6. Keng tarqalgan xatolar va yechimlari

1. Ota-bola daraxtida kuchli ota havolasi

python
bola.ota = ota                          # ❌ har tugun sikl — faqat GC tozalaydi

bola._ota = weakref.ref(ota)            # ✅ property orqali ota() ni qaytaring

2. Istisnoni lokal o'zgaruvchida saqlash

python
except ValueError as e:
    oxirgi_xato = e                     # ❌ freym ↔ traceback sikli
    ...

except ValueError as e:
    oxirgi_xato = str(e)                # ✅ yoki funksiya oxirida del oxirgi_xato

3. self.callback = self.metod

python
self.ishlovchi = self.qayta_ishla       # ❌ sikl

def ishlovchi(self, *a):                # ✅ oddiy metod chaqiring yoki WeakMethod
    return self.qayta_ishla(*a)

4. Sikllar bor dasturda doimiy gc.disable()

python
gc.disable()                            # ❌ "tezlashtirish" — sekin xotira sizishi

O'chirishni faqat yuklash oynasida qiling, keyin gc.enable() va gc.collect().

5. Har so'rovdan keyin gc.collect()

python
def javob_ber(sorov):
    ...
    gc.collect()                        # ❌ to'liq yig'ish — har so'rovga o'nlab ms

GC ni o'z holiga qo'ying yoki chegaralarni sozlang; sikllarni manbada yo'q qiling.

6. Katta keshni yuklagach muzlatmaslik

python
model = yukla_katta_model()             # millionlab obyekt
# ❌ har to'liq yig'ish ularni qayta tekshiradi

gc.freeze()                             # ✅ yuklangandan keyin

7. Sikldagi __del__ da boshqa a'zoga murojaat

python
def __del__(self):
    self.sherik.yop()                   # ❌ sherik allaqachon tozalangan bo'lishi mumkin

Resurslarni with yoki weakref.finalize bilan yoping.

8. Sizishni gc.garbage bilan qidirish

python
print(gc.garbage)                       # ❌ zamonaviy Python'da deyarli har doim bo'sh

gc.set_debug(gc.DEBUG_SAVEALL); gc.collect()   # ✅ yig'ilganlarni ko'rish (faqat tuzatishda)

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 9.5-dars (o'tilgan): except ... as e blok oxirida o'chirilishi — sikllarni oldini olish uchun
  • 10.12-dars (o'tilgan): o'zini chaqiruvchi ichki funksiya sikli
  • 11.4-dars (o'tilgan): havolalar hisobi — GC uning to'ldiruvchisi
  • 11.10-dars: weakref — sikllarni uzishning asosiy vositasi
  • 12-qism: GC ob'ektlari C darajasida, tp_traverse va tp_clear
  • 14-qism: GC va iplar; erkin ipli yig'mada GC
  • 28-qism: gunicorn preload, fork va gc.freeze()
  • 29-qism: xotira optimizatsiyasi va kechikish pauzalarini o'lchash

8. Eng yaxshi amaliyotlar

  1. Sikllar yaratmang, ularni GC ga qoldirmang — ota havolalari zaif, istisnolar matn sifatida.

  2. GC ni o'chirmang — faqat qisqa, aniq oynalarda, keyin albatta enable va collect.

  3. Uzoq yashaydigan katta tuzilmalarni yuklagach — gc.freeze().

  4. fork qiluvchi serverlarda ishga tushishdan keyin gc.freeze().

  5. Chegaralarni o'lchab o'zgartiring — gc.callbacks bilan yig'ishlar soni va davomiyligini yozib.

  6. Sikldagi __del__ ga tayanmang.

  7. Xotira o'sishini weakref + gc.collect() testlari bilan tekshiring — "del dan keyin tirikmi?".

  8. gc.collect() ni qo'lda faqat tuzatish, testlar va boshqariladigan oynalarda chaqiring.


9. Amaliy topshiriq

Vazifa 1: Natijani bashorat qiling

python
import gc, weakref

class T:
    pass

gc.collect()
1.  a = T()
    a.me = a
    w = weakref.ref(a)
    del a
    print(w() is None)
2.  gc.collect()
    print(w() is None)
3.  b = T()
    c = T()
    b.c = c
    w2 = weakref.ref(b)
    del b, c
    print(w2() is None)
4.  print(gc.is_tracked(1), gc.is_tracked([]), gc.is_tracked("s"))
5.  print(gc.is_tracked(0.5), gc.is_tracked(T()))
6.  print(gc.get_threshold()[0])
7.  gc.disable()
    x = T()
    x.x = x
    wx = weakref.ref(x)
    del x
    print(gc.isenabled(), wx() is None)
8.  print(gc.collect() >= 1, wx() is None)
    gc.enable()
9.  class Q:
        def __init__(self, ota):
            self.ota = weakref.ref(ota)
    class P:
        def __init__(self):
            self.bola = Q(self)
    p = P()
    wp = weakref.ref(p)
    del p
    print(wp() is None)
10. print(gc.garbage)
11. gc.freeze()
    print(gc.get_freeze_count() > 0)
    gc.unfreeze()
12. def f():
        t = T()
        try:
            1 / 0
        except ZeroDivisionError as e:
            xato = e
        return weakref.ref(t)
    wt = f()
    print(wt() is None, end=" ")
    gc.collect()
    print(wt() is None)
Javoblar
  1. False — o'ziga havola sikli
  2. True — GC siklni topdi
  3. True — sikl yo'q, havolalar hisobi darhol o'chirdi
  4. False True False
  5. False True — float konteyner emas, klass nusxasi esa kuzatiladi
  6. 2000
  7. False False
  8. True True — qo'lda yig'ish GC o'chiq bo'lsa ham ishlaydi
  9. True — bola otani zaif havola bilan ushlaydi, sikl yo'q
  10. []
  11. True
  12. False True — xato lokal o'zgaruvchisi freym ↔ traceback siklini yaratdi

Vazifa 2: Xatolarni tuzating

python
1.  class Menyu:
        def __init__(self, nom, ota=None):
            self.nom, self.ota, self.bolalar = nom, ota, []
            if ota:
                ota.bolalar.append(self)

2.  def yuklash():
        gc.disable()
        return [yukla(f) for f in fayllar]

3.  def qayta_urin(f):
        oxirgi = None
        for _ in range(3):
            try:
                return f()
            except IOError as e:
                oxirgi = e
        raise oxirgi

4.  class Kuzatuvchi:
        def __init__(self, manba):
            self.manba = manba
            manba.obunachilar.append(self.yangila)

5.  @app.after_request
    def tozala(javob):
        gc.collect()
        return javob
Javoblar
python
1.  class Menyu:
        def __init__(self, nom, ota=None):
            self.nom, self.bolalar = nom, []
            self._ota = weakref.ref(ota) if ota else None
            if ota:
                ota.bolalar.append(self)
        @property
        def ota(self):
            return self._ota() if self._ota else None

2.  def yuklash():
        gc.disable()
        try:
            return [yukla(f) for f in fayllar]
        finally:
            gc.enable()                     # xato bo'lsa ham qayta yoqiladi

3.  def qayta_urin(f):
        oxirgi = None
        for _ in range(3):
            try:
                return f()
            except IOError as e:
                oxirgi = e
        try:
            raise oxirgi
        finally:
            oxirgi = None                   # freym ↔ traceback siklini uzish

4.  class Kuzatuvchi:
        def __init__(self, manba):
            self.manba = manba
            manba.obunachilar.append(weakref.WeakMethod(self.yangila))
    # manba ↔ kuzatuvchi sikli va kuzatuvchini abadiy ushlash yo'qoladi

5.  # Har so'rovdan keyin to'liq yig'ish — kechikish manbai. Olib tashlang;
    # kerak bo'lsa server ishga tushgach gc.freeze() va sikllarni manbada yo'q qilish.

Vazifa 3: Sikl detektori

sikllarni_top(ildiz) funksiyasini yozing:

  1. gc.get_referents bo'ylab chuqurlik bo'yicha qidirib, ildizdan erishiladigan barcha sikllarni topsin (Tarjan algoritmi yoki DFS + stek)
  2. Har siklni tur nomlari zanjiri sifatida chiqarsin: Tugun → dict → list → Tugun
  3. __dict__ va hujayralar kabi oraliq obyektlarni "qisqartirish" rejimi
  4. Misol 1 dagi tuzilmalarga qo'llang

Vazifa 4: GC profilchisi

Kontekst menejer yozing: with gc_profil() as p:

  1. gc.callbacks orqali har yig'ishning avlodi, davomiyligi (time.perf_counter), yig'ilgan va yig'ilmaydigan obyektlar sonini yozsin
  2. Blok tugagach hisobot: avlod bo'yicha soni, jami va eng uzun pauza
  3. Blok ichida xato bo'lsa ham callback olib tashlansin
  4. Misol 3 dagi turli chegaralar bilan solishtiring

Vazifa 5: Zaif ota daraxt kutubxonasi

Daraxt va Tugun klasslarini yozing:

  1. Tugunlar ota havolasini zaif saqlasin, daraxt esa ildizni kuchli ushlasin
  2. tugun.ajrat() — daraxtdan ajratish, daraxt.tugunlar() — generator
  3. copy.deepcopy(daraxt) va pickle zaif havolalar bilan to'g'ri ishlasin (__getstate__/__setstate__)
  4. Test: GC o'chiq holatda 10 000 daraxt yaratib o'chirganda tirik tugunlar soni 0 bo'lsin

Vazifa 6: fork va copy-on-write tajribasi (Linux/macOS)

  1. Ota jarayonda 500 MB lik ro'yxatlar tuzilmasini yarating
  2. os.fork() bilan 4 ta bola jarayon oching, har biri gc.collect() chaqirsin
  3. Har bola jarayonning xususiy xotirasini (/proc/self/smaps_rollup dagi Private_Dirty) o'lchang
  4. Xuddi shuni ota jarayonda gc.freeze() qilib takrorlang va farqni tushuntiring

Vazifa 7: O'ylash

Instagram muhandislari 2017 yilda o'z Django serverlarida Python GC ni o'chirib, xotira sarfini kamaytirganliklari haqida yozgan; keyinchalik Python 3.7 da gc.freeze() qo'shildi. Nega GC ni o'chirish xotirani kamaytirishi mumkin, qanday xavflari bor va gc.freeze() bu muammoni qanday nozikroq hal qiladi?

Javob

Qisqa javob: fork qiluvchi serverlarda bola jarayonlar ota jarayon xotirasini copy-on-write asosida bo'lishadi: sahifa faqat unga yozilganda nusxalanadi. Sikl yig'uvchi esa obyektlarni tekshirganda ularning sarlavhasiga (GC ma'lumotlari va havolalar soni) yozadi — natijada bo'lishilgan sahifalar birin-ketin nusxalanib, har bola jarayon xotirasi o'sadi. GC ni o'chirish bu yozuvlarni to'xtatadi, lekin sikllar tozalanmay qoladi. gc.freeze() esa ishga tushishdagi obyektlarni GC dan "yashiradi" — ular tekshirilmaydi va sahifalari yozilmaydi — yangi sikllar esa odatdagidek yig'iladi.

1. Copy-on-write va Python

  • Gunicorn kabi serverlar ilovani ota jarayonda yuklab, keyin fork bilan worker'lar yaratadi
  • Operatsion tizim xotira sahifalarini bo'lishadi: worker faqat sahifaga yozganda uning nusxasi yaratiladi
  • Python'da o'qish ham yozadi: havolalar sonini oshirish/kamaytirish — sarlavhaga yozuv. Shuning uchun bo'lishish tabiatan "sizib" boradi
  • Sikl yig'uvchi esa bundan ham ko'proq yozadi: har kuzatiladigan obyektning GC maydonlari yig'ish paytida o'zgartiriladi — ishlatilmayotgan obyektlar ham

2. Instagram tajribasi

Instagram muhandislari o'z blogida ("Dismissing Python Garbage Collection at Instagram") tasvirlaganidek, GC ni o'chirgach worker'lar orasidagi bo'lishilgan xotira ulushi oshgan va umumiy xotira sarfi kamaygan. Ular GC ni butunlay o'chirishning xavfini ham ta'kidlagan: sikllar tozalanmaydi, shuning uchun xotira nazorati va worker'larni davriy qayta ishga tushirish kerak bo'lgan.

3. GC ni o'chirish xavflari

Xavf Oqibat
Sikllar tozalanmaydi Sekin xotira sizishi — worker'larni qayta ishga tushirishga majbur
Uchinchi tomon kutubxonalar sikl yaratadi Nazorat qilib bo'lmaydigan sizish
gc.enable() qayta chaqirilishi Kutubxona yoki test kodi GC ni qayta yoqib yuborishi mumkin
Tuzatish qiyinligi "Nega xotira o'syapti?" savoliga javob topish murakkablashadi

4. gc.freeze() qanday yordam beradi

  • gc.freeze() — hozir kuzatilayotgan barcha obyektlarni "doimiy avlodga" o'tkazadi
  • Keyingi yig'ishlar ularni tekshirmaydi → ularning sahifalariga GC yozuvi yo'q
  • Keyin yaratilgan obyektlar va sikllar odatdagidek yig'iladi
  • Python hujjatlari tavsiyasi: ota jarayonda fork dan oldin gc.disable(), ilovani yuklab, gc.freeze(), bola jarayonda esa gc.enable() — ota jarayonda fork dan oldingi tasodifiy yig'ish sahifalarni "iflos" qilmasligi uchun

freeze havolalar sonini yangilashni to'xtatmaydi — o'qish ham baribir ba'zi sahifalarni nusxalaydi. Python 3.12 dagi o'lmas obyektlar (PEP 683) bu muammoning bir qismini — ko'p ishlatiladigan umumiy obyektlar uchun — hal qildi.

5. Umumiy saboq

  1. Xotira muammosini o'lchang: Private_Dirty, Shared — "Python xotirani ko'p yeydi" degan taxmin emas
  2. GC ni o'chirishdan oldin sikllarni manbada yo'q qiling
  3. fork modelida: yuklash → gc.freeze() → fork
  4. Uzoq yashaydigan katta tuzilmalar uchun ham freeze pauzalarni kamaytiradi
  5. O'lmas obyektlar va erkin ipli yig'ma (14-qism) CPython xotira modelini shu yo'nalishda rivojlantirmoqda

6. Xulosa

  1. fork qiluvchi serverlarda sahifalar copy-on-write bilan bo'lishiladi, Python esa sarlavhalarga yozib bu bo'lishishni buzadi
  2. Sikl yig'uvchi ishlatilmayotgan obyektlarga ham yozadi — GC ni o'chirish bo'lishilgan xotirani saqlaydi
  3. Narxi — sikllar tozalanmaydi va xotira sekin sizadi
  4. gc.freeze() ishga tushishdagi obyektlarni GC dan chiqarib, yangi sikllarni esa yig'ishda davom etadi
  5. Eng yaxshi natija: sikllarni kamaytirish + yuklashdan keyin freeze + o'lchov

Nimani mustahkamlaydi: 2.1–2.6-bo'limlar.


Xulosa

Bu darsda havolalar hisobini to'ldiruvchi sikl yig'uvchini o'rgandik.

Eng muhim uch fikr:

  1. Sikllar havolalar hisobini aldaydi. Ota ↔ bola, ikki tomonlama ro'yxat va o'ziga havola kabi ko'rinadigan sikllar hamda lokal o'zgaruvchidagi istisno (freym ↔ traceback), atributdagi bog'langan metod va rekursiv ichki funksiya kabi yashirin sikllar del dan keyin ham tirik qoladi. Ularni faqat sikl yig'uvchi topadi — __del__ li sikllar ham (PEP 442), gc.garbage esa odatda bo'sh.

  2. GC ichki havolalarni ayirib, tashqaridan erishib bo'lmaydigan guruhlarni topadi. Faqat kuzatiladigan konteynerlar tekshiriladi, uch avlod bo'yicha: Python 3.14 da 0-avlod 2000 ta ajratishdan keyin, keyingilari esa 10 martadan keyin yig'iladi. Avtomatik yig'ish ixtiyoriy joyda boshlanadi va katta xotirada to'liq yig'ish sezilarli pauza beradi — gc.callbacks va gc.get_stats bilan o'lchang.

  3. Eng yaxshi sozlash — sikl yaratmaslik. Ota havolasini weakref.ref qiling, istisnolarni matn sifatida saqlang yoki del qiling, bog'langan metodni atributga qo'ymang. GC ni faqat qisqa yuklash oynalarida o'chiring, uzoq yashaydigan katta tuzilmalar va fork qiluvchi serverlar uchun esa gc.freeze() dan foydalaning.

Keyingi darsda eng mashhur Python tuzoqlaridan biriga qaytamiz: o'zgaruvchan standart argument — endi obyekt modeli nuqtai nazaridan: sukut qiymat qachon yaratiladi, qayerda saqlanadi, va bu "xato" qachon ataylab foydali.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
11.5-dars: Axlat yig'uvchi (GC) va sikllar — IlmHamroh