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

11.4-dars: Reference counting

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


1. Kirish va motivatsiya

11.1-darsda har obyekt sarlavhasida ob_refcnt — havolalar soni borligini ko'rdik. Endi shu son qanday o'zgarishini va nol bo'lganda nima bo'lishini o'rganamiz.

Veb-xizmatdan real hodisa. Har so'rov katta hisobot obyektini yaratadi va ish tugagach "tashlab yuboriladi". Lekin xotira sekin-asta o'sib, bir kunda 4 GB ga yetadi. Profiler hech qanday "sizish" topmaydi — barcha obyektlar qonuniy havolalarga ega. Sabab xatolar jurnalida edi:

python
oxirgi_xatolar = []

def sorovni_ishla(sorov):
    hisobot = Hisobot.yarat(sorov)          # 50 MB
    try:
        hisobot.hisobla()
    except HisobXatosi as e:
        oxirgi_xatolar.append(e)            # "keyin ko'rib chiqamiz"
        raise

Istisno obyekti __traceback__ orqali freymni, freym esa barcha lokal o'zgaruvchilarni — jumladan 50 MB lik hisobot ni — ushlab turadi. Ro'yxatdagi har bir xato bitta to'liq hisobotni tirik saqlaydi.

CPython obyektni havolalar soni nolga tushgan zahoti o'chiradi — deterministik va darhol. Bu kuchli kafolat: fayl del dan keyin yopiladi, xotira kutilmagan pauzalarsiz bo'shaydi. Lekin buning teskari tomoni ham bor: bitta yashirin havola obyektni cheksiz tirik ushlaydi. Qaysi amallar havola yaratishini bilish — xotira muammolarini topishning asosi.

Bu darsda:

  • Havolalar soni qachon oshadi va kamayadi
  • Nol bo'lganda: darhol o'chirish, zanjir bo'ylab tozalash, __del__
  • sys.getrefcount va uning Python 3.14 dagi nozikliklari
  • Obyektni kutilganidan uzoq ushlab turuvchi yashirin havolalar
  • __del__ ning qoidalari: bir marta, xatolar yutiladi, tirilish
  • Kim ushlab turibdi? — gc.get_referrers bilan tekshirish

Sikllar (bir-biriga ishora qiluvchi obyektlar) va axlat yig'uvchi — 11.5-darsda.


2. Nazariya — chuqur tushuntirish

2.1. Havolalar soni qachon o'zgaradi

C darajasida har yangi havola Py_INCREF, har yo'qolgan havola Py_DECREF ni chaqiradi.

Amal Havola
Nomga bog'lash: x = obj +1 (eski qiymatga −1)
Konteynerga qo'yish: royxat.append(obj), d[k] = obj, (obj,) +1
Atribut: o.a = obj +1
Funksiya argumenti — chaqiruv davomida +1 (freym ichida)
Closure hujayrasi, bog'langan metod (__self__), generator freymi +1
Istisnoning __traceback__ → freym → lokal o'zgaruvchilar Zanjir orqali
del x, nomni qayta bog'lash, konteynerdan olib tashlash −1
Funksiyadan chiqish — lokal o'zgaruvchilar Har biriga −1
O'lmas obyektlar (None, kichik sonlar...) O'zgarmaydi (11.1-dars)

2.2. Nol bo'lganda nima bo'ladi

Havolalar soni nolga tushgan paytda (keyingi qatorni kutmasdan):

  1. Obyektda __del__ bo'lsa — chaqiriladi
  2. Obyekt o'z havolalarini bo'shatadi — ichidagi har bir obyektga Py_DECREF
  3. Ular ham nolga tushsa — zanjir bo'ylab xuddi shu jarayon
  4. Xotira ajratuvchiga qaytariladi (11.1-dars, 2.6)

Oqibatlari:

  • Deterministik: del katta_obyekt dan keyin xotira darhol bo'shaydi (operatsion tizimga qaytishi shart emas)
  • Tartib: tashqi obyekt avval, keyin ichkilari
  • Narxi zanjirda: 10 million elementli tuzilmani del qilish — shu joyda sezilarli pauza
  • Sikllarda ishlamaydi: a.b = b; b.a = a — hech biri nolga tushmaydi (11.5-dars)

Bu CPython xususiyati. PyPy va boshqa implementatsiyalar izlovchi (tracing) axlat yig'uvchi ishlatadi — ular obyektni keyinroq, noma'lum paytda o'chiradi. Resursni yopish uchun havolalar hisobiga tayanmang — with ishlating (10.6-dars).

2.3. sys.getrefcount va uning nozikliklari

sys.getrefcount(x) havolalar sonini qaytaradi, lekin:

Holat Natija
Global nom orqali Haqiqiydan +1 (argument sifatida vaqtinchalik havola)
Python 3.14 da lokal nom orqali Ko'pincha aniq — interpretator lokal o'zgaruvchini "qarz" sifatida uzatadi (+1 yo'q)
O'lmas obyekt Maxsus katta son
Iplar ishlayotganda Boshqa iplar o'zgartirishi mumkin

Qoida: getrefcount ni mutlaq son sifatida emas, farq sifatida ishlating — "bu amaldan keyin nechta havola qo'shildi". Aniq qiymat uchun o'rganish maqsadida ctypes bilan ob_refcnt ni o'qish mumkin (11.1-dars).

2.4. Yashirin havolalar

Obyektni kutilganidan uzoq ushlab turadigan joylar:

Manba Qanday Yechim
Saqlangan istisno e.__traceback__ → freymlar → barcha lokal o'zgaruvchilar Faqat kerakli ma'lumotni saqlang: str(e), traceback.format_exception(e); yoki e.__traceback__ = None
To'xtatilgan generator Generator freymi lokal o'zgaruvchilarni ushlaydi gen.close(), oxirigacha iteratsiya
Closure Hujayra butun obyektni ushlaydi (10.12-dars) Faqat kerakli qiymatni oling
Bog'langan metod callback'da obj.metod → __self__ weakref.WeakMethod
Kesh lru_cache argument va natijalarni ushlaydi (10.13-dars) maxsize, cache_clear()
Global ro'yxatlar/registrlar Plaginlar, kuzatuvchilar, "tarix" WeakSet, WeakValueDictionary (11.10-dars)
Klass atributlari Klass modul yashagan paytgacha tirik Nusxa atributi
REPL _ oxirgi natijani saqlaydi —
Freym obyektlari sys._getframe(), inspect.currentframe() ni saqlash del frame yoki try/finally

2.5. __del__ qoidalari

python
class Fayl:
    def __del__(self):
        print("yopildi")
Qoida Tafsilot
Qachon Havolalar soni nolga tushganda (yoki GC sikl topganda — 11.5)
Necha marta Bir marta — obyekt "tirilsa" ham qayta chaqirilmaydi (PEP 442)
Xato bo'lsa Tashqariga chiqmaydi — Exception ignored in: ... deb stderr ga yoziladi (sys.unraisablehook)
Tirilish __del__ ichida self ni biror joyga saqlash obyektni tirik qoldiradi
Interpretator yopilganda Chaqirilishi kafolatlanmagan; modul global o'zgaruvchilari allaqachon None bo'lishi mumkin
Tartib Tashqi obyekt avval; sikldagi obyektlar orasida tartib aniqlanmagan

Resurslar uchun __del__ o'rniga:

  • with va kontekst menejerlar — aniq, istisnolar bilan ishlaydi (10.6-dars)
  • weakref.finalize(obj, funksiya, *args) — self ga havola saqlamaydi, interpretator yopilganda ham ishonchliroq chaqiriladi (11.10-dars)

2.6. Kim ushlab turibdi?

python
import gc
gc.get_referrers(obj)       # obj ga ishora qiluvchi konteynerlar (GC kuzatadigan)
gc.get_referents(obj)       # obj ishora qiladigan obyektlar
  • Natijada joriy freym va tekshiruv uchun yaratilgan ro'yxatlar ham chiqadi — ularni filtrlang
  • Faqat GC kuzatadigan obyektlar (konteynerlar) ko'rinadi
  • Tuzatish vositasi — ishlab chiqarish kodida ishlatmang (sekin)

Chuqurroq tahlil uchun uchinchi tomon objgraph kutubxonasi havolalar grafini rasm qilib chizadi; tracemalloc esa xotira qayerda ajratilganini ko'rsatadi (29-qism).


3. Tez ma'lumotnoma

O'zgarish

+1: nomga bog'lash, konteynerga qo'yish, atribut, argument, closure, bog'langan metod
−1: del, qayta bog'lash, konteynerdan olish, funksiyadan chiqish
0  → darhol: __del__ → ichkilarga DECREF → zanjir → xotira

sys.getrefcount

global nom:       haqiqiy + 1
3.14 lokal nom:   ko'pincha haqiqiy (qarz)
o'lmas:           maxsus son
→ farq sifatida ishlating

Yashirin havolalar

saqlangan istisno → traceback → freym → lokallar
to'xtatilgan generator, closure, bog'langan metod, lru_cache, global ro'yxat, klass atributi

__del__

bir marta (PEP 442)     xato → "Exception ignored"     yopilishda kafolat yo'q
resurs uchun: with / weakref.finalize

Tekshirish

python
gc.get_referrers(obj)    gc.get_referents(obj)    weakref.ref(obj)() is None

4. Batafsil misollar

Misol 1 — Havolalar sonini kuzatish

python
"""ob_refcnt ni aniq o'qish; har amal qancha havola qo'shadi; getrefcount ning global va lokal farqi."""

import ctypes
import sys


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


class Tugun:
    def metod(self):
        return self


print("=== 1. Qaysi amal havola qo'shadi ===")
obyekt = Tugun()
qadamlar = [("yaratildi", None)]
boshlangich = refcnt(obyekt)
saqlagichlar = []


def qadam(tavsif):
    print(f"  {tavsif:34} ob_refcnt = {refcnt(obyekt)}  (+{refcnt(obyekt) - boshlangich})")


qadam("obyekt = Tugun()")
b = obyekt
qadam("b = obyekt")
royxat = [obyekt, obyekt]
qadam("royxat = [obyekt, obyekt]")
lugat = {"kalit": obyekt}
qadam("lugat['kalit'] = obyekt")
kortej = (obyekt,)
qadam("kortej = (obyekt,)")
boshqa = Tugun()
boshqa.havola = obyekt
qadam("boshqa.havola = obyekt")
bogl = obyekt.metod
qadam("bogl = obyekt.metod  (__self__)")
closure = (lambda: obyekt)
qadam("lambda: obyekt  (global — hujayra yo'q)")


def yasa(x):
    return lambda: x


closure2 = yasa(obyekt)
qadam("yasa(obyekt) — closure hujayrasi")
del b, royxat, lugat, kortej, boshqa, bogl, closure2
qadam("hammasi del qilindi")

print("\n=== 2. sys.getrefcount: global va lokal ===")
print(f"  global: ob_refcnt={refcnt(obyekt)}, sys.getrefcount={sys.getrefcount(obyekt)}")


def lokal_sinov():
    lokal = Tugun()
    return refcnt(lokal), sys.getrefcount(lokal)


print(f"  lokal 3.14-bob: ob_refcnt={lokal_sinov()[0]}, sys.getrefcount={lokal_sinov()[1]}")
print("  ⚠️ getrefcount ni farq sifatida ishlating")

print("\n=== 3. Funksiya chaqiruvi davomida ===")


def ichkarida(x):
    return refcnt(x)


oldin = refcnt(obyekt)
print(f"  chaqiruvdan oldin {oldin}, chaqiruv ichida {ichkarida(obyekt)}, keyin {refcnt(obyekt)}")
print("  (tashqarida: global nom + refcnt() argumenti; ichkarida: global nom + x parametri — ikkalasida bittadan qo'shimcha)")

print("\n=== 4. Generator freymi ushlab turadi ===")


def generator(x):
    yield x
    yield x


oldin = refcnt(obyekt)
g = generator(obyekt)
next(g)
print(f"  to'xtatilgan generator: +{refcnt(obyekt) - oldin}")
g.close()
print(f"  g.close() dan keyin:    +{refcnt(obyekt) - oldin}")

Natijaning muhim qismi:

text
=== 1. Qaysi amal havola qo'shadi ===
  obyekt = Tugun()                   ob_refcnt = 2  (+0)
  b = obyekt                         ob_refcnt = 3  (+1)
  royxat = [obyekt, obyekt]          ob_refcnt = 5  (+3)
  lugat['kalit'] = obyekt            ob_refcnt = 6  (+4)
  kortej = (obyekt,)                 ob_refcnt = 7  (+5)
  boshqa.havola = obyekt             ob_refcnt = 8  (+6)
  bogl = obyekt.metod  (__self__)    ob_refcnt = 9  (+7)
  lambda: obyekt  (global — hujayra yo'q) ob_refcnt = 9  (+7)
  yasa(obyekt) — closure hujayrasi   ob_refcnt = 10  (+8)
  hammasi del qilindi                ob_refcnt = 2  (+0)

=== 2. sys.getrefcount: global va lokal ===
  global: ob_refcnt=2, sys.getrefcount=2
  lokal 3.14-bob: ob_refcnt=1, sys.getrefcount=1
  ⚠️ getrefcount ni farq sifatida ishlating

=== 3. Funksiya chaqiruvi davomida ===
  chaqiruvdan oldin 2, chaqiruv ichida 2, keyin 2
  (tashqarida: global nom + refcnt() argumenti; ichkarida: global nom + x parametri — ikkalasida bittadan qo'shimcha)

=== 4. Generator freymi ushlab turadi ===
  to'xtatilgan generator: +1
  g.close() dan keyin:    +0

Nima ko'rsatdi: 2.1, 2.3-bo'limlar. lambda: obyekt havola qo'shmadi — obyekt global nom, lambda uni chaqiruv paytida lug'atdan o'qiydi; yasa(obyekt) esa hujayra orqali havola saqladi.

Misol 2 — __del__ qachon va qanday chaqiriladi

python
"""del, qayta bog'lash, funksiyadan chiqish, konteynerdan olish, zanjir; __del__ dagi xato, tirilish."""

import sys


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

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


print("=== 1. Darhol o'chirish ===")
r = R("del bilan")
print("  del dan oldin")
del r
print("  del dan keyin")
r = R("birinchi")
r = R("ikkinchi")
print("  qayta bog'lashdan keyin")
del r


def funksiya():
    lokal = R("lokal o'zgaruvchi")
    print("  funksiya tugayapti")


funksiya()
print("  funksiyadan keyin")
royxat = [R("ro'yxat elementi")]
royxat.pop()
print("  pop dan keyin")

print("\n=== 2. Zanjir: tashqaridan ichkariga ===")
ota = R("ota")
ota.bola = R("bola")
ota.bola.nevara = R("nevara")
del ota

print("\n=== 3. Ikki havola — ikkinchisi yo'qolguncha tirik ===")
a = R("ikki havolali")
b = a
del a
print("  a o'chirildi, b hali ushlab turibdi")
del b

print("\n=== 4. __del__ dagi xato tashqariga chiqmaydi ===")


def hook(xabar):
    print(f"    unraisablehook: {type(xabar.exc_value).__name__}: {xabar.exc_value}")


eski_hook = sys.unraisablehook
sys.unraisablehook = hook


class XatoliDel:
    def __del__(self):
        raise RuntimeError("fayl yopilmadi")


x = XatoliDel()
del x
print("  dastur davom etdi")
sys.unraisablehook = eski_hook

print("\n=== 5. Tirilish: __del__ faqat bir marta ===")
qabriston = []


class Tiriluvchi:
    def __del__(self):
        print("    __del__ chaqirildi — o'zini saqlayapti")
        qabriston.append(self)


t = Tiriluvchi()
del t
print(f"  tirildi: {len(qabriston)} ta obyekt ro'yxatda")
qabriston.clear()
print("  ro'yxat tozalandi — __del__ ikkinchi marta chaqirilmadi")

Natijaning muhim qismi:

text
=== 1. Darhol o'chirish ===
  del dan oldin
    🗑 del bilan
  del dan keyin
    🗑 birinchi
  qayta bog'lashdan keyin
    🗑 ikkinchi
  funksiya tugayapti
    🗑 lokal o'zgaruvchi
  funksiyadan keyin
    🗑 ro'yxat elementi
  pop dan keyin

=== 2. Zanjir: tashqaridan ichkariga ===
    🗑 ota
    🗑 bola
    🗑 nevara

=== 3. Ikki havola — ikkinchisi yo'qolguncha tirik ===
  a o'chirildi, b hali ushlab turibdi
    🗑 ikki havolali

=== 4. __del__ dagi xato tashqariga chiqmaydi ===
    unraisablehook: RuntimeError: fayl yopilmadi
  dastur davom etdi

=== 5. Tirilish: __del__ faqat bir marta ===
    __del__ chaqirildi — o'zini saqlayapti
  tirildi: 1 ta obyekt ro'yxatda
  ro'yxat tozalandi — __del__ ikkinchi marta chaqirilmadi

Nima ko'rsatdi: 2.2, 2.5-bo'limlar.

Misol 3 — Yashirin havolalar va "kim ushlab turibdi"

python
"""Saqlangan istisno freymni ushlaydi; generator, closure, callback, kesh; gc.get_referrers bilan topish va tuzatish."""

import gc
import traceback
import weakref
from functools import lru_cache


class Hisobot:
    def __init__(self, nom):
        self.nom = nom
        self.malumot = bytearray(1_000_000)

    def __repr__(self):
        return f"Hisobot({self.nom!r})"


def tirikmi(havola):
    return "tirik" if havola() is not None else "o'lgan"


print("=== 1. ⚠️ Saqlangan istisno lokal o'zgaruvchilarni ushlaydi ===")
xatolar = []
xato_matnlari = []


def ishla(nom, saqlash_usuli):
    hisobot = Hisobot(nom)
    havola = weakref.ref(hisobot)
    try:
        raise ValueError(f"{nom}: hisoblashda xato")
    except ValueError as e:
        if saqlash_usuli == "obyekt":
            xatolar.append(e)
        else:
            xato_matnlari.append("".join(traceback.format_exception(e)).strip().splitlines()[-1])
    return havola


h1 = ishla("A", "obyekt")
h2 = ishla("B", "matn")
print(f"  istisno obyekti saqlandi: Hisobot A — {tirikmi(h1)}")
print(f"  faqat matn saqlandi:      Hisobot B — {tirikmi(h2)}, matn: {xato_matnlari[0]!r}")


def kim_ushlab_turibdi(obj, chuqurlik=4):
    zanjir = []
    joriy = obj
    for _ in range(chuqurlik):
        egalar = [r for r in gc.get_referrers(joriy)
                  if r is not zanjir and type(r).__name__ not in ("list_iterator",)
                  and r is not globals() and not (isinstance(r, list) and r is gc.garbage)]
        egalar = [r for r in egalar if type(r).__name__ in ("frame", "dict", "traceback", "ValueError", "list")]
        if not egalar:
            break
        joriy = egalar[0]
        zanjir.append(type(joriy).__name__)
        if joriy is xatolar:
            break
    return " ← ".join(zanjir)


print(f"  Hisobot A ni kim ushlaydi: Hisobot ← {kim_ushlab_turibdi(h1())}")
xatolar[0].__traceback__ = None
gc.collect()
print(f"  e.__traceback__ = None dan keyin: Hisobot A — {tirikmi(h1)}")

print("\n=== 2. To'xtatilgan generator ===")


def hisobot_oqimi():
    hisobot = Hisobot("generator")
    yield hisobot.nom
    yield "oxiri"


g = hisobot_oqimi()
print(f"  next(g) = {next(g)!r}")
gh = weakref.ref(g.gi_frame.f_locals["hisobot"])
print(f"  to'xtatilgan: {tirikmi(gh)}")
g.close()
print(f"  g.close(): {tirikmi(gh)}")

print("\n=== 3. Callback, kesh va closure ===")


class Tugma:
    def __init__(self):
        self.ishlovchilar = []


class Oyna:
    def __init__(self, tugma):
        self.hisobot = Hisobot("oyna")
        tugma.ishlovchilar.append(self.yangila)

    def yangila(self):
        return self.hisobot.nom


tugma = Tugma()
oyna_havola = weakref.ref(Oyna(tugma))
print(f"  bog'langan metod callback'da: Oyna — {tirikmi(oyna_havola)}")
tugma.ishlovchilar.clear()
print(f"  callback olib tashlangach: Oyna — {tirikmi(oyna_havola)}")


@lru_cache(maxsize=None)
def tahlil(hisobot):
    return len(hisobot.malumot)


kesh_hisobot = Hisobot("kesh")
kh = weakref.ref(kesh_hisobot)
tahlil(kesh_hisobot)
del kesh_hisobot
print(f"  lru_cache argumenti: {tirikmi(kh)}")
tahlil.cache_clear()
print(f"  cache_clear(): {tirikmi(kh)}")


def yasa_hisoblagich(hisobot):
    return lambda: len(hisobot.malumot)


c_hisobot = Hisobot("closure")
ch = weakref.ref(c_hisobot)
hisoblagich = yasa_hisoblagich(c_hisobot)
del c_hisobot
print(f"  closure hujayrasi: {tirikmi(ch)}")
del hisoblagich
print(f"  closure o'chirilgach: {tirikmi(ch)}")

Natijaning muhim qismi:

text
=== 1. ⚠️ Saqlangan istisno lokal o'zgaruvchilarni ushlaydi ===
  istisno obyekti saqlandi: Hisobot A — tirik
  faqat matn saqlandi:      Hisobot B — o'lgan, matn: 'ValueError: B: hisoblashda xato'
  Hisobot A ni kim ushlaydi: Hisobot ← frame ← traceback ← ValueError ← list
  e.__traceback__ = None dan keyin: Hisobot A — o'lgan

=== 2. To'xtatilgan generator ===
  next(g) = 'generator'
  to'xtatilgan: tirik
  g.close(): o'lgan

=== 3. Callback, kesh va closure ===
  bog'langan metod callback'da: Oyna — tirik
  callback olib tashlangach: Oyna — o'lgan
  lru_cache argumenti: tirik
  cache_clear(): o'lgan
  closure hujayrasi: tirik
  closure o'chirilgach: o'lgan

Nima ko'rsatdi: 2.4, 2.6-bo'limlar. Istisno obyekti traceback orqali butun 1 MB lik hisobotni ushlab turdi, faqat xato matnini saqlash esa hech narsani ushlamadi.

Misol 4 — Amaliy: obyektlar hayot davri kuzatuvchisi

Veb-xizmatdagi so'rov obyektlari hayot davrini kuzatuvchi vosita: har Sorov yaratilganda weakref.finalize ro'yxatga oladi, o'lganda qayd qiladi. Simulyatsiyada ba'zi so'rovlar turli yo'llar bilan "sizadi" — vosita ularni topib, sababini ko'rsatadi.

python
"""weakref.finalize bilan hayot davri jurnali; __del__ vs finalize; sizish sabablarini gc.get_referrers bilan tahlil."""

import gc
import weakref
from functools import lru_cache


class Kuzatuvchi:
    def __init__(self):
        self.tiriklar = weakref.WeakValueDictionary()
        self.yaratildi = 0
        self.oldi = 0

    def royxatga_ol(self, obj, nom):
        self.yaratildi += 1
        self.tiriklar[nom] = obj
        weakref.finalize(obj, self._oldi)

    def _oldi(self):
        self.oldi += 1

    def hisobot(self):
        return {"yaratildi": self.yaratildi, "oldi": self.oldi, "tirik": sorted(self.tiriklar.keys())}


kuzat = Kuzatuvchi()


class Sorov:
    def __init__(self, raqam):
        self.raqam = raqam
        self.tana = bytearray(100_000)
        kuzat.royxatga_ol(self, f"sorov-{raqam}")


xatolar = []
tinglovchilar = []


@lru_cache(maxsize=None)
def narx_hisobla(sorov):
    return sorov.raqam * 1000


def tinglovchi_qosh(sorov):
    tinglovchilar.append(lambda: sorov.raqam)


def ishla(raqam):
    sorov = Sorov(raqam)
    if raqam % 10 == 3:
        try:
            raise TimeoutError(f"so'rov {raqam}")
        except TimeoutError as e:
            xatolar.append(e)
    elif raqam % 10 == 7:
        tinglovchi_qosh(sorov)
    elif raqam % 10 == 9:
        narx_hisobla(sorov)
    return sorov.raqam


for i in range(30):
    ishla(i)

print("=== 1. 30 ta so'rovdan keyin ===")
h = kuzat.hisobot()
print(f"  yaratildi: {h['yaratildi']}, o'ldi: {h['oldi']}, tirik qoldi: {len(h['tirik'])}")
print(f"  tiriklar: {h['tirik']}")


def sabab(obj):
    for ega in gc.get_referrers(obj):
        tur = type(ega).__name__
        if tur == "frame":
            for ega2 in gc.get_referrers(ega):
                if type(ega2).__name__ == "traceback":
                    return "saqlangan istisno traceback'i"
        if tur == "cell":
            return "closure hujayrasi (tinglovchi lambda)"
        if tur == "tuple":
            for ega2 in gc.get_referrers(ega):
                if type(ega2).__name__ == "dict" and ega2 is not globals():
                    return "lru_cache kaliti"
    return "noma'lum"


print("\n=== 2. Sizish sabablari ===")
for nom in sorted(kuzat.tiriklar.keys(), key=lambda n: int(n.split("-")[1])):
    obj = kuzat.tiriklar.get(nom)
    if obj is not None:
        print(f"  {nom:10} ← {sabab(obj)}")
        del obj

print("\n=== 3. Tuzatishlar ===")
for e in xatolar:
    e.__traceback__ = None
print(f"  traceback'lar tozalandi → tirik: {len(kuzat.tiriklar)}")
tinglovchilar.clear()
print(f"  tinglovchilar tozalandi → tirik: {len(kuzat.tiriklar)}")
narx_hisobla.cache_clear()
gc.collect()
print(f"  kesh tozalandi → tirik: {len(kuzat.tiriklar)}, o'ldi: {kuzat.oldi}/{kuzat.yaratildi}")

print("\n=== 4. __del__ va weakref.finalize ===")


class DelBilan:
    def __del__(self):
        print("    __del__: self hali mavjud — lekin tirilish mumkin, interpretator yopilishida kafolat yo'q")


def tozalovchi(nom):
    print(f"    finalize: {nom} — self ga havola yo'q, faqat kerakli argumentlar")


d = DelBilan()
del d
f_obj = Sorov(999)
fin = weakref.finalize(f_obj, tozalovchi, "sorov-999")
print(f"  finalize tirikmi: {fin.alive}, interpretator yopilishida ham chaqiriladi (atexit): {fin.atexit}")
del f_obj
print(f"  obyekt o'lgach finalize tirikmi: {fin.alive}   (bir marta ishladi)")

Natijaning muhim qismi:

text
=== 1. 30 ta so'rovdan keyin ===
  yaratildi: 30, o'ldi: 21, tirik qoldi: 9
  tiriklar: ['sorov-13', 'sorov-17', 'sorov-19', 'sorov-23', 'sorov-27', 'sorov-29', 'sorov-3', 'sorov-7', 'sorov-9']

=== 2. Sizish sabablari ===
  sorov-3    ← saqlangan istisno traceback'i
  sorov-7    ← closure hujayrasi (tinglovchi lambda)
  sorov-9    ← lru_cache kaliti
  sorov-13   ← saqlangan istisno traceback'i
  sorov-17   ← closure hujayrasi (tinglovchi lambda)
  sorov-19   ← lru_cache kaliti
  sorov-23   ← saqlangan istisno traceback'i
  sorov-27   ← closure hujayrasi (tinglovchi lambda)
  sorov-29   ← lru_cache kaliti

=== 3. Tuzatishlar ===
  traceback'lar tozalandi → tirik: 6
  tinglovchilar tozalandi → tirik: 3
  kesh tozalandi → tirik: 0, o'ldi: 30/30

=== 4. __del__ va weakref.finalize ===
    __del__: self hali mavjud — lekin tirilish mumkin, interpretator yopilishida kafolat yo'q
  finalize tirikmi: True, interpretator yopilishida ham chaqiriladi (atexit): True
    finalize: sorov-999 — self ga havola yo'q, faqat kerakli argumentlar
  obyekt o'lgach finalize tirikmi: False   (bir marta ishladi)

Nima ko'rsatdi: 2.4, 2.5, 2.6-bo'limlar — weakref.finalize obyektni tirik ushlamasdan uning o'limini qayd etadi, gc.get_referrers esa havola zanjiri bo'ylab sabab turini ko'rsatadi.


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

Noto'g'ri fikr To'g'risi
"Python obyektlarni vaqti-vaqti bilan axlat yig'uvchi o'chiradi" CPython'da ko'p obyekt havolalar soni nolga tushganda darhol o'chadi; GC faqat sikllar uchun
"del x obyektni o'chiradi" Bitta havolani olib tashlaydi
"sys.getrefcount — aniq havolalar soni" Globalda +1, 3.14 lokalda ko'pincha aniq, o'lmaslarda maxsus son
"Xato ro'yxatiga istisnoni saqlash arzon" Traceback orqali butun freymlar va lokal o'zgaruvchilar tirik
"__del__ — destruktor, har doim chaqiriladi" Interpretator yopilishida kafolat yo'q; bir marta; xatolari yutiladi
"__del__ ichida xato ko'tarsam, chaqiruvchi ko'radi" Exception ignored in — faqat stderr
"Havolalar hisobi — barcha Python implementatsiyalarida" CPython xususiyati; PyPy boshqacha
"Obyekt tirikmi — sys.getrefcount bilan tekshiraman" weakref.ref(obj)() is None — ishonchli usul

6. Keng tarqalgan xatolar va yechimlari

1. Istisno obyektlarini ro'yxatda saqlash

python
xatolar.append(e)                               # ❌ traceback → freym → lokallar

xatolar.append({"tur": type(e).__name__, "xabar": str(e),
                "iz": traceback.format_exception(e)})   # ✅ faqat matn

2. Resursni __del__ da yopish

python
class Ulanish:
    def __del__(self):
        self.soket.close()                      # ❌ qachon chaqirilishi noma'lum (PyPy, sikl, yopilish)

    def __enter__(self): return self
    def __exit__(self, *e): self.soket.close()  # ✅ with bilan aniq

3. __del__ ichida global nomlarga tayanish

python
def __del__(self):
    jurnal.info("yopildi")                      # ❌ yopilishda jurnal allaqachon None bo'lishi mumkin

weakref.finalize(self, funksiya, kerakli_argumentlar) — argumentlar oldindan olinadi.

4. Generator oxirigacha yetmasdan tashlab yuborish

python
satr = next(fayl_oqimi(yol))                    # ❌ generator freymi faylni ochiq ushlaydi

with contextlib.closing(fayl_oqimi(yol)) as oqim: yoki gen.close().

5. Obyekt o'lganmi — getrefcount bilan tekshirish

python
if sys.getrefcount(obj) == 2: ...               # ❌ versiyaga va chaqiruv joyiga bog'liq

havola = weakref.ref(obj)
if havola() is None: ...                        # ✅

6. Katta tuzilmani issiq yo'lda del qilish

python
def sorovni_ishla():
    kesh = qur_katta_indeks()                   # 10 mln obyekt
    ...
    return javob                                # ⚠️ chiqishda butun indeks o'chiriladi — javob kechikadi

Katta tuzilmani qayta ishlating yoki tozalashni javobdan keyinga suring.

7. Freym obyektini saqlash

python
self.chaqiruvchi = inspect.currentframe().f_back    # ❌ butun chaqiruv zanjiri tirik
self.chaqiruvchi_nomi = inspect.currentframe().f_back.f_code.co_name   # ✅

8. Callback ro'yxatiga bog'langan metod

python
hodisalar.append(self.yangila)                  # ❌ self abadiy tirik
hodisalar.append(weakref.WeakMethod(self.yangila))   # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 9.5-dars (o'tilgan): __traceback__, except ... as e blok oxirida o'chirilishi
  • 10.3, 10.6-darslar (o'tilgan): generator close(), kontekst menejerlar
  • 10.12–10.13-darslar (o'tilgan): closure va lru_cache havolalari
  • 11.5-dars: sikllar va axlat yig'uvchi — havolalar hisobi yetmaydigan joy
  • 11.10-dars: weakref — havola qo'shmasdan kuzatish
  • 12-qism: Py_INCREF/Py_DECREF bytecode va C darajasida
  • 14-qism: GIL va havolalar hisobi; erkin ipli yig'mada "biased reference counting"
  • 29-qism: tracemalloc va xotira sizishini ishlab chiqarishda topish
  • 30-qism: C kengaytmalarda havolalarni qo'lda boshqarish

8. Eng yaxshi amaliyotlar

  1. Resurslarni with bilan yoping — havolalar hisobiga tayanmang.

  2. Istisnolardan faqat kerakli ma'lumotni saqlang — obyektni emas.

  3. Generatorlarni oxirigacha iteratsiya qiling yoki close() qiling.

  4. Uzoq yashaydigan ro'yxatlarga — zaif havolalar (WeakSet, WeakMethod, WeakValueDictionary).

  5. __del__ o'rniga weakref.finalize.

  6. Obyekt tirikligini weakref.ref bilan tekshiring, getrefcount bilan emas.

  7. Sizishni taxmin qilmang — gc.get_referrers, tracemalloc bilan isbotlang.

  8. Katta tuzilmalar o'chirilishi vaqtini hisobga oling — kechikishga sezgir kodda.


9. Amaliy topshiriq

Vazifa 1: Natijani bashorat qiling

python
import sys

class R:
    def __init__(self, n):
        self.n = n
    def __del__(self):
        print("del", self.n, end=" ")
1.  r = R(1)
    del r
    print("keyin")
2.  r = R(1)
    r = R(2)
    print("|", end=" ")
    del r
    print()
3.  def f():
        t = R("f")
        print("ichkarida", end=" ")
    f()
    print("tashqarida")
4.  a = R("a")
    b = a
    del a
    print("hali", end=" ")
    del b
    print()
5.  l = [R("x")]
    l.clear()
    print("tozalandi")
6.  o = object()
    d = {1: o, 2: o}
    print(sys.getrefcount(o) - 1)
7.  def g():
        x = []
        return sys.getrefcount(x)
    print(g())
8.  saqla = []
    class T:
        def __del__(self):
            saqla.append(self)
            print("del", end=" ")
    t = T()
    del t
    saqla.clear()
    print(len(saqla))
9.  def f():
        obj = R("freym")
        raise ValueError
    try:
        f()
    except ValueError as e:
        saqlangan = e
    print("except dan keyin", end=" ")
    del saqlangan
    print()
10. import weakref
    r = R("w")
    w = weakref.ref(r)
    del r
    print(w())
11. a = R("ota")
    a.bola = R("bola")
    del a
    print()
12. def gen():
        obj = R("gen")
        yield 1
        yield 2
    g = gen()
    next(g)
    print("to'xtatildi", end=" ")
    del g
    print()
Javoblar
  1. del 1 keyin — del bilan darhol
  2. del 1 | del 2 — qayta bog'lash eski obyektni o'chirdi
  3. ichkarida del f tashqarida — funksiyadan chiqishda
  4. hali del a — ikkinchi havola o'chguncha tirik
  5. del x tozalandi
  6. 3 — o, d[1], d[2] (global nom orqali +1 ayrildi)
  7. 1 — Python 3.14 da lokal o'zgaruvchi qarz sifatida uzatildi
  8. del 0 — tirilgan obyektning __del__ i ikkinchi marta chaqirilmadi
  9. except dan keyin del freym — saqlangan istisno freymni ushlab turdi
  10. del w None
  11. del ota del bola — tashqaridan ichkariga
  12. to'xtatildi del gen — generator o'chganda freymi ham

Vazifa 2: Xatolarni tuzating

python
1.  class Jurnal:
        def __init__(self, yol):
            self.fayl = open(yol, "a")
        def __del__(self):
            self.fayl.close()

2.  so_nggi_xatolar = []
    def ishla(malumot):
        katta = yukla(malumot)
        try:
            return tahlil(katta)
        except Exception as e:
            so_nggi_xatolar.append(e)
            raise

3.  def birinchi_qator(yol):
        def oqi():
            with open(yol) as f:
                yield from f
        return next(oqi())

4.  class Hodisa:
        obunachilar = []
        def obuna(self, f):
            self.obunachilar.append(f)
    class Panel:
        def __init__(self, hodisa):
            hodisa.obuna(self.yangila)

5.  def tirikmi(obj):
        return sys.getrefcount(obj) > 2
Javoblar
python
1.  class Jurnal:
        def __init__(self, yol):
            self.fayl = open(yol, "a")
        def __enter__(self):
            return self
        def __exit__(self, *e):
            self.fayl.close()               # with bilan aniq yopish
    # yoki: self._fin = weakref.finalize(self, self.fayl.close) — self ni ushlamaydigan zaxira

2.  so_nggi_xatolar = []
    def ishla(malumot):
        katta = yukla(malumot)
        try:
            return tahlil(katta)
        except Exception as e:
            so_nggi_xatolar.append("".join(traceback.format_exception(e)))   # faqat matn
            raise

3.  def birinchi_qator(yol):
        with open(yol) as f:
            return next(f)                  # generator va ochiq fayl freymda qolmaydi

4.  class Hodisa:
        def __init__(self):
            self.obunachilar = []           # klass emas, nusxa atributi
        def obuna(self, f):
            self.obunachilar.append(weakref.WeakMethod(f))
    # chaqirishda: for w in list(self.obunachilar): f = w(); f() if f else self.obunachilar.remove(w)

5.  # Tiriklikni tekshirish uchun obyektning o'zini emas, zaif havolani saqlash kerak:
    havola = weakref.ref(obj)
    def tirikmi():
        return havola() is not None

Vazifa 3: Havola hisoblagich jadvali

Misol 1 ni kengaytiring:

  1. 20 xil amal uchun (set.add, dict kaliti, functools.partial, property qaytarishi, __slots__ atributi, enumerate, zip, map iteratori, threading.local...) havolalar sonini o'lchang
  2. Har amal uchun "necha havola va nega" izohini yozing
  3. Python 3.14 va imkoni bo'lsa boshqa versiyada natijalarni solishtiring — qaysi qatorlar farq qiladi va nima uchun

Vazifa 4: Sizish detektori dekoratori

@sizishni_kuzat(chegara=100) klass dekoratori yozing:

  1. Klassning barcha nusxalarini WeakSet da kuzatsin
  2. Tirik nusxalar soni chegara dan oshsa, ogohlantirish chiqarsin va eng ko'p uchraydigan "egalar" turlarini (gc.get_referrers bo'yicha) ko'rsatsin
  3. klass.hisobot() — yaratilgan, o'lgan, tirik soni va o'rtacha yashash davomiyligi
  4. Misol 4 dagi simulyatsiyaga qo'llang

Vazifa 5: Xavfsiz istisno saqlovchi

XatoArxivi klassini yozing:

  1. saqla(e) — istisnoning turi, xabari, __notes__, sabab zanjiri va formatlangan traceback'ini saqlasin, lekin freymlarni emas
  2. Har freymdan faqat fayl, qator, funksiya nomi va (ixtiyoriy) lokal o'zgaruvchilarning repr idan birinchi 100 belgini olsin
  3. oxirgi(n) va tur_boyicha() statistikasi
  4. Test: saqlangandan keyin asl lokal obyektlar weakref bo'yicha o'lganini tekshiring

Vazifa 6: O'chirish narxini o'lchash

  1. 10⁴, 10⁵, 10⁶, 10⁷ elementli (a) list[int], (b) list[list], (c) dict[str, obyekt], (d) bog'langan ro'yxat (har tugun next atributi bilan) tuzilmalarni yarating
  2. Har biri uchun del vaqtini time.perf_counter bilan o'lchang
  3. Nega bog'langan ro'yxatda (d) chuqur zanjir bo'lsa ham interpretator stek toshishidan yiqilmasligini tushuntiring (CPython'ning "trashcan" mexanizmi haqida o'qing)
  4. Natijalarni grafik yoki jadval qilib chiqaring

Vazifa 7: O'ylash

CPython xotirani havolalar hisobi bilan boshqaradi. Java, Go va .NET izlovchi (tracing) axlat yig'uvchilarga, Swift esa kompilyator qo'shadigan havolalar hisobiga (ARC), Rust esa egalik tizimiga va kerak bo'lganda Rc/Arc ga tayanadi. Havolalar hisobining afzallik va kamchiliklari nimada, va nega CPython uni saqlab kelmoqda?

Javob

Qisqa javob: havolalar hisobi deterministik va bosqichma-bosqich: obyekt oxirgi havola yo'qolgan zahoti o'ladi, katta "dunyoni to'xtatuvchi" pauzalar yo'q, resurslar darhol bo'shaydi. Narxi — har tayinlashda schyotchikni yangilash (ayniqsa ko'p ipli muhitda), sikllarni topa olmaslik va o'chirish narxining chaqiruv joyiga "tushishi". CPython uni saqlaydi, chunki butun C API, kengaytmalar ekotizimi va Python kodining deterministik tozalashga bo'lgan kutishlari shu modelga qurilgan — o'rniga esa bosqichma-bosqich yaxshilanishlar (o'lmas obyektlar, biased reference counting) qo'shilmoqda.

1. Havolalar hisobining afzalliklari

Afzallik Oqibat
Darhol tozalash del yoki funksiyadan chiqishda xotira qaytadi
Bashorat qilinadigan pauzalar Katta GC pauzalari yo'q (sikl GC bundan mustasno, u kichik)
Kesh lokalligi Bo'shagan xotira darhol qayta ishlatiladi — hali "issiq"
Sodda C API Kengaytma yozuvchi Py_INCREF/Py_DECREF bilan aniq egalikni ifodalaydi
Kichik xotira sarfi Izlovchi GC odatda "bo'sh joy zaxirasi" talab qiladi

2. Kamchiliklari

Kamchilik Oqibat
Har tayinlashda yozuv Hatto o'qish uchun ham schyotchik yangilanadi — CPU keshi va tezlik
Ko'p iplilik Schyotchikni atomar yangilash qimmat → GIL tarixan shu sabab (14-qism)
Sikllar Alohida sikl yig'uvchi kerak (11.5-dars)
O'chirish zanjiri Katta tuzilma o'chirilganda narx aynan o'sha joyda
Qo'lda xatolar C kengaytmalardagi INCREF/DECREF xatolari — sizish yoki yiqilish

3. Boshqa tillar

Java, .NET, Go — izlovchi GC: vaqti-vaqti bilan "tirik" obyektlarni ildizlardan boshlab belgilaydi, qolganini tozalaydi. Tayinlash arzon (schyotchik yo'q), sikllar muammo emas, zamonaviy yig'uvchilar (G1, ZGC, Go'ning konkurent yig'uvchisi) pauzalarni juda kichraytirgan. Lekin tozalash vaqti noma'lum — resurslar uchun try-with-resources / using / defer kerak.

Swift — ARC: kompilyator retain/release chaqiruvlarini avtomatik qo'yadi va keraksizlarini optimallashtiradi. Sikllar uchun dasturchi weak va unowned havolalarni aniq belgilaydi — sikl yig'uvchi yo'q.

Rust — egalik: ko'p hollarda schyotchik umuman kerak emas, obyekt egasi doiradan chiqqanda o'chadi. Umumiy egalik uchun Rc (bir ip) va Arc (atomar, ko'p ip); sikllar uchun Weak. Dasturchi narxni aniq tanlaydi.

4. CPython nega havolalar hisobini saqlaydi

  • C API va ekotizim: NumPy, pandas va minglab kengaytmalar Py_INCREF/Py_DECREF ga tayanadi — boshqa modelga o'tish ekotizimni buzadi
  • Deterministik tozalash kutilmasi: ko'p Python kodi (bilib yoki bilmay) faylning del dan keyin yopilishiga tayanadi
  • Bosqichma-bosqich yaxshilanishlar:
    • O'lmas obyektlar (PEP 683, 3.12) — None, kichik sonlar kabi ko'p ishlatiladigan obyektlarda schyotchik yozuvlari yo'q
    • Qarz olingan havolalar — 3.14 da lokal o'zgaruvchilarni ba'zi amallarda schyotchikni oshirmasdan uzatish
    • Biased reference counting (PEP 703, erkin ipli yig'ma) — obyektni yaratgan ip o'z schyotchigini arzon, atomarsiz yangilaydi, boshqa iplar esa alohida atomar schyotchikdan foydalanadi
    • Kechiktirilgan havolalar hisobi ba'zi tur obyektlari va funksiyalar uchun (erkin ipli yig'ma)

PyPy esa izlovchi GC ishlatadi va C API ni emulyatsiya qatlami orqali qo'llab-quvvatlaydi — bu mumkin, lekin kengaytmalar sekinroq ishlaydi.

5. Amaliy xulosa Python dasturchisi uchun

  1. Deterministik tozalashga tayanmang — with ishlating; kod PyPy'da ham to'g'ri ishlasin
  2. Havolalar hisobi qanday ishlashini bilish esa xotira sizishini topishda kerak: yashirin havola = tirik obyekt
  3. Sikllardan qoching yoki weakref bilan uzing — shunda obyektlar darhol o'ladi, GC ga kutilmaydi
  4. Ko'p ipli kod uchun erkin ipli yig'maning narxlarini (14-qism) biling

6. Xulosa

  1. Havolalar hisobi obyektni darhol va bashorat qilinadigan tarzda o'chiradi, lekin har tayinlashda narx to'laydi va sikllarni topa olmaydi
  2. Izlovchi GC tayinlashni arzon qiladi va sikllarni hal qiladi, lekin tozalash vaqti noma'lum
  3. Swift va Rust narxni kompilyator va dasturchi nazoratiga o'tkazadi
  4. CPython C API va ekotizim tufayli havolalar hisobini saqlaydi va uni o'lmas obyektlar, qarz havolalar va biased reference counting bilan yaxshilamoqda
  5. Dasturchi uchun: resurslar — with, yashirin havolalar — sizish, sikllar — weakref

Nimani mustahkamlaydi: 2.1–2.6-bo'limlar.


Xulosa

Bu darsda CPython xotirasining asosiy mexanizmi — havolalar hisobini o'rgandik.

Eng muhim uch fikr:

  1. Havolalar soni nolga tushgan zahoti obyekt o'ladi. Nomga bog'lash, konteynerga qo'yish, atribut, argument, closure va bog'langan metod havola qo'shadi; del, qayta bog'lash va funksiyadan chiqish ayiradi. Nol bo'lganda __del__ chaqiriladi va obyekt ichidagi havolalar zanjir bo'ylab bo'shatiladi — deterministik, lekin bu CPython xususiyati: resurslar uchun with ishlating.

  2. Yashirin havola — tirik obyekt. Saqlangan istisno traceback orqali freymlar va barcha lokal o'zgaruvchilarni, to'xtatilgan generator o'z freymini, closure hujayrasi, callback'dagi bog'langan metod va lru_cache argumentlarni ushlab turadi. Istisnolardan faqat matnni saqlang, generatorlarni yoping, uzoq yashaydigan ro'yxatlarda zaif havolalardan foydalaning va sababni gc.get_referrers bilan isbotlang.

  3. getrefcount va __del__ — ehtiyot bilan. sys.getrefcount global nom orqali +1 qo'shadi, Python 3.14 da lokal o'zgaruvchi uchun ko'pincha aniq son beradi, o'lmas obyektlar uchun maxsus qiymat qaytaradi — uni farq sifatida ishlating, tiriklikni esa weakref.ref bilan tekshiring. __del__ bir marta chaqiriladi, xatolari yutiladi va interpretator yopilishida kafolatlanmagan — weakref.finalize ishonchliroq.

Keyingi darsda havolalar hisobi hal qila olmaydigan muammoni — sikllarni — va CPython'ning avlodli axlat yig'uvchisini o'rganamiz: gc moduli, avlodlar va chegaralar, gc.disable() qachon mantiqiy va sikllarni weakref bilan qanday uzish mumkin.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
11.4-dars: Reference counting — IlmHamroh