IlmHamroh
Python kursi/Turlar tizimi5/9-dars22 daqiqa
Mundarija (21)

13.5-dars: Protocol — strukturaviy turlash

13-QISM — TURLAR TIZIMI · 5-dars


1. Kirish va motivatsiya

13.1-darsda duck typing ni ko'rdik: "write metodi bo'lsa yetarli". Lekin buni turlar tilida qanday yozish kerak?

Real vaziyat. Loyihada hisobotlarni turli joyga yozadigan kod bor: faylga, S3 ga, xotiradagi buferga, testda esa soxta (mock) obyektga. Birinchi yechim — umumiy ota klass:

python
class Yozuvchi(ABC):
    @abstractmethod
    def write(self, matn: str) -> int: ...

Muammo: io.StringIO va open() qaytargan fayl obyekti bu klassdan meros olmaydi — ular allaqachon yozilgan. Tashqi kutubxonaning klassini ham o'zgartirib bo'lmaydi. Natijada isinstance tekshiruvlari va Any lar paydo bo'ldi, tur xavfsizligi yo'qoldi.

Protocol shu muammoni hal qiladi: "menga write(str) -> int metodi bo'lgan har qanday obyekt mos" — merossiz. io.StringIO, o'z klassingiz, mock — hammasi avtomatik mos keladi va tekshiruvchi buni tekshiradi.

Bu darsda:

  • Protocol: strukturaviy turlash — "shakl mos bo'lsa yetarli"
  • Nominal (ABC) va strukturaviy yondashuv farqi
  • @runtime_checkable va uning chegaralari
  • Ma'lumot atributlari bo'lgan protokollar
  • Chaqiriladigan protokollar (__call__) va generik protokollar
  • Standart protokollar: Sized, Iterable, SupportsIndex va boshqalar
  • Amaliy: ulanadigan (pluggable) saqlash qatlami

2. Nazariya — chuqur tushuntirish

2.1. Protocol nima

python
from typing import Protocol


class Yozuvchi(Protocol):
    def write(self, matn: str) -> int: ...


def hisobot_yoz(chiqish: Yozuvchi, matn: str) -> int:
    return chiqish.write(matn)
  • Yozuvchi dan meros olish shart emas: write(str) -> int metodi bo'lgan har qanday obyekt mos
  • io.StringIO, fayl obyekti, o'z klassingiz, test uchun soxta obyekt — hammasi ishlaydi
  • Tekshiruvchi mos kelishini imzo bo'yicha tekshiradi

Bu — "duck typing ni tekshiruvchiga tushuntirish" usuli.

2.2. Nominal va strukturaviy

ABC (nominal) Protocol (strukturaviy)
Mos kelish sharti Meros olish kerak Shakl mos bo'lsa yetarli
Tashqi klasslar Ro'yxatdan o'tkazish kerak (register) Avtomatik
Ish vaqtida tekshiruv isinstance — har doim Faqat @runtime_checkable bilan, cheklangan
Sukut implementatsiya Metodlarda kod bo'lishi mumkin Odatda yo'q (lekin mumkin)
Qachon Sizning ierarxiyangiz, umumiy xulq Interfeys, tashqi obyektlar, testlar

Ikkalasi birga ham ishlatiladi: Protocol — chegaralarda (funksiya parametrlari), ABC — o'z ierarxiyangizda.

2.3. @runtime_checkable va chegaralari

python
@runtime_checkable
class Yopiladigan(Protocol):
    def close(self) -> None: ...


isinstance(fayl, Yopiladigan)     # ✅ ishlaydi

Uchta muhim cheklov:

  1. Imzo tekshirilmaydi — faqat nom bor-yo'qligi: def close(self, majburiy: int) ham "mos" deb hisoblanadi
  2. @runtime_checkable bo'lmasa — isinstance TypeError beradi
  3. issubclass faqat metodli protokollar uchun ishlaydi (ma'lumot atributlari bo'lsa — TypeError)

Ya'ni ish vaqtidagi tekshiruv — "taxminiy"; haqiqiy kafolat statik tekshiruvchidan keladi.

2.4. Ma'lumot atributlari

python
@runtime_checkable
class Nomli(Protocol):
    nom: str
  • Nusxada nom atributi bo'lsa — mos (isinstance ham ishlaydi)
  • Klass darajasida nom: str e'loni yetarli emas — qiymat kerak
  • Protocol ichida __init__ yozish — odatda keraksiz (protokol — interfeys, konstruktor emas)

2.5. Chaqiriladigan va generik protokollar

Chaqiriladigan protokol — murakkab imzoli funksiyalar uchun (13.2-darsdagi Callable cheklovini yengadi):

python
class Jonatuvchi(Protocol):
    def __call__(self, xabar: str, *, qayta_urinish: int = 0) -> bool: ...

Generik protokol:

python
class Ombor[T](Protocol):
    def ol(self, kalit: str) -> T | None: ...
    def qoy(self, kalit: str, qiymat: T) -> None: ...

Endi Ombor[Foydalanuvchi] va Ombor[Mahsulot] alohida tekshiriladi.

2.6. Standart protokollar

Protokol Talab Qayerdan
Sized __len__ collections.abc
Iterable[T], Iterator[T] __iter__, __next__ collections.abc
Container[T] __contains__ collections.abc
Hashable __hash__ collections.abc
SupportsIndex, SupportsFloat, SupportsInt __index__, __float__, __int__ typing
SupportsRead, SupportsWrite read, write _typeshed (faqat tekshiruv paytida)

Ko'p hollarda o'z protokolingizni yozishdan oldin standartini qidiring.

2.7. Qachon Protocol, qachon ABC

Vaziyat Tanlov
Tashqi kutubxona obyektlarini qabul qilish Protocol
Test uchun soxta obyektlar Protocol
"Faqat shu metod kerak" Protocol
Umumiy xulq va sukut implementatsiya ABC
Ierarxiya va super() chaqiruvlari ABC
Plagin tizimi, ro'yxatdan o'tkazish Ikkalasi (Protocol + reestr)

3. Tez ma'lumotnoma

python
from typing import Protocol, runtime_checkable

class Yozuvchi(Protocol):
    def write(self, matn: str) -> int: ...

@runtime_checkable
class Yopiladigan(Protocol):
    def close(self) -> None: ...

class Ombor[T](Protocol):              # generik
    def ol(self, kalit: str) -> T | None: ...

class Jonatuvchi(Protocol):            # chaqiriladigan
    def __call__(self, xabar: str, *, qayta: int = 0) -> bool: ...

Qoidalar

meros shart emas — shakl yetarli
isinstance faqat @runtime_checkable bilan va faqat nomlarni tekshiradi
issubclass — faqat metodli protokollar uchun
standart protokollar: Sized, Iterable, SupportsIndex, Hashable

4. Batafsil misollar

Misol 1 — Protokol va meros

python
"""Bitta funksiya — turli manbalar; ABC bilan solishtirish; tashqi klasslar avtomatik mos keladi; testda soxta obyekt."""

import io
import typing
from abc import ABC, abstractmethod


class Yozuvchi(typing.Protocol):
    def write(self, matn: str) -> int: ...


def hisobot_yoz(chiqish: Yozuvchi, qatorlar: list[str]) -> int:
    """Chiqish qayerdaligi muhim emas — write(str) -> int bo'lsa yetarli."""
    jami = 0
    for qator in qatorlar:
        jami += chiqish.write(qator + "\n")
    return jami


class BuferYozuvchi:
    """Protokoldan meros OLMAYDI — lekin mos keladi."""

    def __init__(self) -> None:
        self.qatorlar: list[str] = []

    def write(self, matn: str) -> int:
        self.qatorlar.append(matn)
        return len(matn)


class SoxtaYozuvchi:
    """Test uchun: nima yozilganini eslaydi va xato simulyatsiya qiladi."""

    def __init__(self, xato_qatorda: int | None = None) -> None:
        self.yozilgan: list[str] = []
        self.xato_qatorda = xato_qatorda

    def write(self, matn: str) -> int:
        if self.xato_qatorda == len(self.yozilgan):
            raise OSError("disk to'ldi")
        self.yozilgan.append(matn)
        return len(matn)


QATORLAR = ["sana,summa", "2026-09-16,250000", "2026-09-16,120000"]

print("=== 1. Turli manbalar, bitta funksiya ===")
bufer = io.StringIO()
print(f"  io.StringIO:    {hisobot_yoz(bufer, QATORLAR)} bayt, natija: {bufer.getvalue().splitlines()}")
oz_yozuvchi = BuferYozuvchi()
print(f"  BuferYozuvchi:  {hisobot_yoz(oz_yozuvchi, QATORLAR)} bayt, qatorlar: {len(oz_yozuvchi.qatorlar)}")
soxta = SoxtaYozuvchi()
print(f"  SoxtaYozuvchi:  {hisobot_yoz(soxta, QATORLAR)} bayt")

print("\n=== 2. Testda xatoni simulyatsiya qilish ===")
buzuq = SoxtaYozuvchi(xato_qatorda=2)
try:
    hisobot_yoz(buzuq, QATORLAR)
except OSError as xato:
    print(f"  OSError: {xato}; xatogacha yozilgan qatorlar: {len(buzuq.yozilgan)}")

print("\n=== 3. ABC bilan bo'lsa nima bo'lardi ===")


class AbstraktYozuvchi(ABC):
    @abstractmethod
    def write(self, matn: str) -> int: ...


class ABCdanMeros(AbstraktYozuvchi):
    def __init__(self) -> None:
        self.qatorlar: list[str] = []

    def write(self, matn: str) -> int:
        self.qatorlar.append(matn)
        return len(matn)


print(f"  ABC vorisi mos keladi: {isinstance(ABCdanMeros(), AbstraktYozuvchi)}")
print(f"  io.StringIO ABC ga mos kelmaydi: {isinstance(io.StringIO(), AbstraktYozuvchi)}")
print(f"  BuferYozuvchi ham: {isinstance(BuferYozuvchi(), AbstraktYozuvchi)}")
print("  ⭐ ABC — meros talab qiladi; tashqi klasslarni o'zgartirib bo'lmaydi")
try:
    AbstraktYozuvchi()
except TypeError as xato:
    print(f"  abstrakt klassni yaratib bo'lmaydi: TypeError: {str(xato)[:60]}...")

print("\n=== 4. Protokol — faqat annotatsiya ===")
print(f"  Yozuvchi klassmi: {isinstance(Yozuvchi, type)}, Protocol vorisimi: {typing.Protocol in Yozuvchi.__mro__}")
print(f"  hisobot_yoz izohi: {typing.get_type_hints(hisobot_yoz)['chiqish'].__name__}")
print("  ⭐ ish vaqtida hech qanday tekshiruv yo'q — kafolatni tekshiruvchi beradi")

Natijaning muhim qismi:

text
=== 1. Turli manbalar, bitta funksiya ===
  io.StringIO:    47 bayt, natija: ['sana,summa', '2026-09-16,250000', '2026-09-16,120000']
  BuferYozuvchi:  47 bayt, qatorlar: 3
  SoxtaYozuvchi:  47 bayt

=== 2. Testda xatoni simulyatsiya qilish ===
  OSError: disk to'ldi; xatogacha yozilgan qatorlar: 2

=== 3. ABC bilan bo'lsa nima bo'lardi ===
  ABC vorisi mos keladi: True
  io.StringIO ABC ga mos kelmaydi: False
  BuferYozuvchi ham: False
  ⭐ ABC — meros talab qiladi; tashqi klasslarni o'zgartirib bo'lmaydi
  abstrakt klassni yaratib bo'lmaydi: TypeError: Can't instantiate abstract class AbstraktYozuvchi without an...

=== 4. Protokol — faqat annotatsiya ===
  Yozuvchi klassmi: True, Protocol vorisimi: True
  hisobot_yoz izohi: Yozuvchi
  ⭐ ish vaqtida hech qanday tekshiruv yo'q — kafolatni tekshiruvchi beradi

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

Misol 2 — @runtime_checkable va uning chegaralari

python
"""isinstance bilan protokol; imzo tekshirilmasligi; ma'lumot atributlari; issubclass cheklovi; xavfsizroq tekshiruv."""

import inspect
import io
import typing


@typing.runtime_checkable
class Yopiladigan(typing.Protocol):
    def close(self) -> None: ...


@typing.runtime_checkable
class Nomli(typing.Protocol):
    nom: str


class Tekshirilmaydigan(typing.Protocol):
    def ishla(self) -> None: ...


class Ulanish:
    def close(self) -> None:
        pass


class NotogriImzo:
    def close(self, majburiy_argument: int) -> None:
        pass


class Mahsulot:
    def __init__(self, nom: str) -> None:
        self.nom = nom


class NomsizKlass:
    nom: str          # faqat annotatsiya, qiymat yo'q


print("=== 1. isinstance protokol bilan ===")
for tavsif, obj in [("Ulanish()", Ulanish()), ("io.StringIO()", io.StringIO()), ("'matn'", "matn")]:
    print(f"  {tavsif:16} Yopiladigan: {isinstance(obj, Yopiladigan)}")

print("\n=== 2. ⚠️ Imzo tekshirilmaydi ===")
print(f"  NotogriImzo().close(majburiy_argument) — isinstance: {isinstance(NotogriImzo(), Yopiladigan)}")
try:
    NotogriImzo().close()          # type: ignore[call-arg]
except TypeError as xato:
    print(f"  lekin chaqirilganda: TypeError: {xato}")
print("  ⭐ ish vaqtidagi tekshiruv faqat nom bor-yo'qligini ko'radi")

print("\n=== 3. Xavfsizroq tekshiruv: imzoni solishtirish ===")


def imzo_mos(obj: object, protokol: type, metod: str) -> bool:
    kutilgan = inspect.signature(getattr(protokol, metod))
    haqiqiy = inspect.signature(getattr(obj, metod))
    kutilgan_parametrlar = [p for n, p in kutilgan.parameters.items() if n != "self"]
    return [p.name for p in haqiqiy.parameters.values()] == [p.name for p in kutilgan_parametrlar]


for tavsif, obj in [("Ulanish()", Ulanish()), ("NotogriImzo()", NotogriImzo())]:
    print(f"  {tavsif:16} isinstance: {isinstance(obj, Yopiladigan)}, imzo ham mos: {imzo_mos(obj, Yopiladigan, 'close')}")

print("\n=== 4. Ma'lumot atributlari ===")
print(f"  Mahsulot('Klaviatura') Nomli: {isinstance(Mahsulot('Klaviatura'), Nomli)}")
print(f"  NomsizKlass() (faqat annotatsiya): {isinstance(NomsizKlass(), Nomli)}")
print(f"  Ulanish() Nomli: {isinstance(Ulanish(), Nomli)}")

print("\n=== 5. Cheklovlar ===")
try:
    isinstance(Ulanish(), Tekshirilmaydigan)
except TypeError as xato:
    print(f"  runtime_checkable siz: TypeError: {str(xato)[:64]}...")
print(f"  issubclass metodli protokol bilan: {issubclass(Ulanish, Yopiladigan)}")
try:
    issubclass(Mahsulot, Nomli)
except TypeError as xato:
    print(f"  issubclass ma'lumot atributli protokol bilan: TypeError: {str(xato)[:52]}...")

Natijaning muhim qismi:

text
=== 1. isinstance protokol bilan ===
  Ulanish()        Yopiladigan: True
  io.StringIO()    Yopiladigan: True
  'matn'           Yopiladigan: False

=== 2. ⚠️ Imzo tekshirilmaydi ===
  NotogriImzo().close(majburiy_argument) — isinstance: True
  lekin chaqirilganda: TypeError: NotogriImzo.close() missing 1 required positional argument: 'majburiy_argument'
  ⭐ ish vaqtidagi tekshiruv faqat nom bor-yo'qligini ko'radi

=== 3. Xavfsizroq tekshiruv: imzoni solishtirish ===
  Ulanish()        isinstance: True, imzo ham mos: True
  NotogriImzo()    isinstance: True, imzo ham mos: False

=== 4. Ma'lumot atributlari ===
  Mahsulot('Klaviatura') Nomli: True
  NomsizKlass() (faqat annotatsiya): False
  Ulanish() Nomli: False

=== 5. Cheklovlar ===
  runtime_checkable siz: TypeError: Instance and class checks can only be used with @runtime_checkab...
  issubclass metodli protokol bilan: True
  issubclass ma'lumot atributli protokol bilan: TypeError: Protocols with non-method members don't support issu...

Nima ko'rsatdi: 2.3, 2.4-bo'limlar.

Misol 3 — Generik va chaqiriladigan protokollar

python
"""Generik Ombor[T] protokoli; ikki implementatsiya; chaqiriladigan protokol bilan murakkab imzo; standart protokollar."""

import typing
from collections.abc import Hashable, Iterable, Sized
from dataclasses import dataclass


@dataclass(frozen=True)
class Foydalanuvchi:
    id: int
    ism: str


class Ombor[T](typing.Protocol):
    def ol(self, kalit: str) -> T | None: ...
    def qoy(self, kalit: str, qiymat: T) -> None: ...
    def __len__(self) -> int: ...


class XotiraOmbori[T]:
    def __init__(self) -> None:
        self._yozuvlar: dict[str, T] = {}

    def ol(self, kalit: str) -> T | None:
        return self._yozuvlar.get(kalit)

    def qoy(self, kalit: str, qiymat: T) -> None:
        self._yozuvlar[kalit] = qiymat

    def __len__(self) -> int:
        return len(self._yozuvlar)


class JurnalliOmbor[T]:
    """Har amalni yozib boradi — protokolga mos, lekin merossiz."""

    def __init__(self, asos: Ombor[T]) -> None:
        self._asos = asos
        self.jurnal: list[str] = []

    def ol(self, kalit: str) -> T | None:
        self.jurnal.append(f"ol({kalit})")
        return self._asos.ol(kalit)

    def qoy(self, kalit: str, qiymat: T) -> None:
        self.jurnal.append(f"qoy({kalit})")
        self._asos.qoy(kalit, qiymat)

    def __len__(self) -> int:
        return len(self._asos)


def yukla_yoki_yarat(ombor: Ombor[Foydalanuvchi], kalit: str, yarat: typing.Callable[[], Foydalanuvchi]) -> Foydalanuvchi:
    mavjud = ombor.ol(kalit)
    if mavjud is not None:
        return mavjud
    yangi = yarat()
    ombor.qoy(kalit, yangi)
    return yangi


print("=== 1. Generik protokol ===")
xotira: XotiraOmbori[Foydalanuvchi] = XotiraOmbori()
jurnalli = JurnalliOmbor(xotira)
aziz = yukla_yoki_yarat(jurnalli, "u1", lambda: Foydalanuvchi(1, "Aziz"))
yana = yukla_yoki_yarat(jurnalli, "u1", lambda: Foydalanuvchi(1, "Aziz"))
print(f"  birinchi chaqiruv: {aziz}")
print(f"  ikkinchi chaqiruv o'shanimi: {aziz is yana}, ombor hajmi: {len(jurnalli)}")
print(f"  jurnal: {jurnalli.jurnal}")
print(f"  Ombor tur parametrlari: {Ombor.__type_params__}")

print("\n=== 2. Chaqiriladigan protokol ===")


class Jonatuvchi(typing.Protocol):
    def __call__(self, xabar: str, *, qayta_urinish: int = 0) -> bool: ...


def xabarlarni_yubor(jonat: Jonatuvchi, xabarlar: list[str]) -> int:
    return sum(jonat(x, qayta_urinish=1) for x in xabarlar)


yuborilganlar: list[tuple[str, int]] = []


def sms_yubor(xabar: str, *, qayta_urinish: int = 0) -> bool:
    yuborilganlar.append((xabar, qayta_urinish))
    return len(xabar) < 20


print(f"  yuborilgan: {xabarlarni_yubor(sms_yubor, ['salom', 'juda uzun xabar matni bu yerda'])} ta muvaffaqiyatli")
print(f"  qayd: {yuborilganlar}")
print(f"  __call__ izohi: {typing.get_type_hints(Jonatuvchi.__call__)}")

print("\n=== 3. Standart protokollar ===")
namunalar: list[object] = [[1, 2, 3], "matn", 42, {"a": 1}, (x for x in range(3))]
print(f"  {'obyekt':22} {'Sized':>6} {'Iterable':>9} {'Hashable':>9} {'SupportsIndex':>14}")
for obj in namunalar:
    tavsif = type(obj).__name__ + ("" if isinstance(obj, (int, str)) else f" ({len(obj) if isinstance(obj, Sized) else '?'})")
    print(f"  {tavsif:22} {isinstance(obj, Sized)!s:>6} {isinstance(obj, Iterable)!s:>9} "
          f"{isinstance(obj, Hashable)!s:>9} {isinstance(obj, typing.SupportsIndex)!s:>14}")


def indeksla(ketma_ketlik: typing.Sequence[str], indeks: typing.SupportsIndex) -> str:
    return ketma_ketlik[indeks.__index__()]


print(f"  SupportsIndex bilan: {indeksla(['a', 'b', 'c'], 1)}, bool ham mos: {indeksla(['a', 'b', 'c'], True)}")

Natijaning muhim qismi:

text
=== 1. Generik protokol ===
  birinchi chaqiruv: Foydalanuvchi(id=1, ism='Aziz')
  ikkinchi chaqiruv o'shanimi: True, ombor hajmi: 1
  jurnal: ['ol(u1)', 'qoy(u1)', 'ol(u1)']
  Ombor tur parametrlari: (T,)

=== 2. Chaqiriladigan protokol ===
  yuborilgan: 1 ta muvaffaqiyatli
  qayd: [('salom', 1), ('juda uzun xabar matni bu yerda', 1)]
  __call__ izohi: {'xabar': <class 'str'>, 'qayta_urinish': <class 'int'>, 'return': <class 'bool'>}

=== 3. Standart protokollar ===
  obyekt                  Sized  Iterable  Hashable  SupportsIndex
  list (3)                 True      True     False          False
  str                      True      True      True          False
  int                     False     False      True           True
  dict (1)                 True      True     False          False
  generator (?)           False      True      True          False
  SupportsIndex bilan: b, bool ham mos: b

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

Misol 4 — Amaliy: ulanadigan saqlash qatlami

Hisobotlarni turli joyga saqlash kerak: xotira (test), fayl tizimi, "bulut" (simulyatsiya). Protokol interfeysni belgilaydi, implementatsiyalar merossiz yoziladi, tanlov esa sozlamaga qarab qilinadi. Qo'shimcha: implementatsiyalarni ro'yxatdan o'tkazuvchi reestr va imzolarni tekshiruvchi.

python
"""Protokol interfeys; uch implementatsiya; reestr; imzolarni tekshirish; bir xil kod har qanday backend bilan."""

import inspect
import pathlib
import tempfile
import typing


class Saqlagich(typing.Protocol):
    """Hisobotlarni saqlash interfeysi."""

    nom: str

    def saqla(self, kalit: str, mazmun: str) -> int: ...
    def oqi(self, kalit: str) -> str | None: ...
    def royxat(self) -> list[str]: ...


class XotiraSaqlagich:
    nom = "xotira"

    def __init__(self) -> None:
        self._yozuvlar: dict[str, str] = {}

    def saqla(self, kalit: str, mazmun: str) -> int:
        self._yozuvlar[kalit] = mazmun
        return len(mazmun)

    def oqi(self, kalit: str) -> str | None:
        return self._yozuvlar.get(kalit)

    def royxat(self) -> list[str]:
        return sorted(self._yozuvlar)


class FaylSaqlagich:
    nom = "fayl"

    def __init__(self, papka: pathlib.Path) -> None:
        self.papka = papka
        papka.mkdir(parents=True, exist_ok=True)

    def saqla(self, kalit: str, mazmun: str) -> int:
        yol = self.papka / f"{kalit}.txt"
        yol.write_text(mazmun, encoding="utf-8")
        return len(mazmun)

    def oqi(self, kalit: str) -> str | None:
        yol = self.papka / f"{kalit}.txt"
        return yol.read_text(encoding="utf-8") if yol.exists() else None

    def royxat(self) -> list[str]:
        return sorted(y.stem for y in self.papka.glob("*.txt"))


class BulutSaqlagich:
    """Tarmoq simulyatsiyasi: kechikish va urinishlar hisobi."""

    nom = "bulut"

    def __init__(self) -> None:
        self._obyektlar: dict[str, str] = {}
        self.sorovlar = 0

    def saqla(self, kalit: str, mazmun: str) -> int:
        self.sorovlar += 1
        self._obyektlar[f"hisobotlar/{kalit}"] = mazmun
        return len(mazmun)

    def oqi(self, kalit: str) -> str | None:
        self.sorovlar += 1
        return self._obyektlar.get(f"hisobotlar/{kalit}")

    def royxat(self) -> list[str]:
        self.sorovlar += 1
        return sorted(k.split("/", 1)[1] for k in self._obyektlar)


def hisobot_saqla(saqlagich: Saqlagich, sana: str, qatorlar: list[str]) -> str:
    """Saqlagich turidan qat'i nazar bir xil kod."""
    mazmun = "\n".join(qatorlar)
    hajm = saqlagich.saqla(sana, mazmun)
    return f"{saqlagich.nom}: {sana} ({hajm} bayt)"


def protokolga_mosmi(klass: type, protokol: type) -> tuple[bool, list[str]]:
    """Statik tekshiruvchi qiladigan ishning soddalashtirilgan varianti."""
    kamchiliklar = []
    for nom, kutilgan in inspect.getmembers(protokol, inspect.isfunction):
        if nom.startswith("_"):
            continue
        haqiqiy = getattr(klass, nom, None)
        if haqiqiy is None:
            kamchiliklar.append(f"{nom}: metod yo'q")
            continue
        kutilgan_imzo = [p for p in inspect.signature(kutilgan).parameters if p != "self"]
        haqiqiy_imzo = [p for p in inspect.signature(haqiqiy).parameters if p != "self"]
        if kutilgan_imzo != haqiqiy_imzo:
            kamchiliklar.append(f"{nom}: {haqiqiy_imzo} ≠ {kutilgan_imzo}")
    for nom in getattr(protokol, "__annotations__", {}):
        if not hasattr(klass, nom):
            kamchiliklar.append(f"{nom}: atribut yo'q")
    return not kamchiliklar, kamchiliklar


QATORLAR = ["sana,summa", "2026-09-16,250000", "2026-09-16,120000"]
papka = pathlib.Path(tempfile.mkdtemp()) / "hisobotlar"

print("=== 1. Bir xil kod, uch saqlagich ===")
saqlagichlar: list[Saqlagich] = [XotiraSaqlagich(), FaylSaqlagich(papka), BulutSaqlagich()]
for s in saqlagichlar:
    print(f"  {hisobot_saqla(s, '2026-09-16', QATORLAR)}")

print("\n=== 2. Saqlangan ma'lumotni o'qish ===")
for s in saqlagichlar:
    mazmun = s.oqi("2026-09-16")
    print(f"  {s.nom:8} ro'yxat: {s.royxat()}, birinchi qator: {mazmun.splitlines()[0] if mazmun else None!r}")
bulut = saqlagichlar[2]
print(f"  bulut so'rovlari: {bulut.sorovlar}")     # type: ignore[attr-defined]

print("\n=== 3. Moslikni tekshirish ===")


class YarimSaqlagich:
    nom = "yarim"

    def saqla(self, kalit: str) -> int:            # ⚠️ mazmun argumenti yo'q
        return 0


class NomsizSaqlagich:
    def saqla(self, kalit: str, mazmun: str) -> int:
        return 0

    def oqi(self, kalit: str) -> str | None:
        return None

    def royxat(self) -> list[str]:
        return []


for klass in (XotiraSaqlagich, FaylSaqlagich, BulutSaqlagich, YarimSaqlagich, NomsizSaqlagich):
    mos, kamchiliklar = protokolga_mosmi(klass, Saqlagich)
    belgi = "✅" if mos else "❌"
    print(f"  {belgi} {klass.__name__:18} {'' if mos else '; '.join(kamchiliklar)}")
print("  ⭐ statik tekshiruvchi bu xatolarni kodni ishga tushirmasdan topadi")

Natijaning muhim qismi:

text
=== 1. Bir xil kod, uch saqlagich ===
  xotira: 2026-09-16 (46 bayt)
  fayl: 2026-09-16 (46 bayt)
  bulut: 2026-09-16 (46 bayt)

=== 2. Saqlangan ma'lumotni o'qish ===
  xotira   ro'yxat: ['2026-09-16'], birinchi qator: 'sana,summa'
  fayl     ro'yxat: ['2026-09-16'], birinchi qator: 'sana,summa'
  bulut    ro'yxat: ['2026-09-16'], birinchi qator: 'sana,summa'
  bulut so'rovlari: 3

=== 3. Moslikni tekshirish ===
  ✅ XotiraSaqlagich
  ✅ FaylSaqlagich
  ✅ BulutSaqlagich
  ❌ YarimSaqlagich     oqi: metod yo'q; royxat: metod yo'q; saqla: ['kalit'] ≠ ['kalit', 'mazmun']
  ❌ NomsizSaqlagich    nom: atribut yo'q
  ⭐ statik tekshiruvchi bu xatolarni kodni ishga tushirmasdan topadi

Nima ko'rsatdi: 2.1, 2.3, 2.7-bo'limlar.


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

Noto'g'ri fikr To'g'risi
"Protocol dan meros olish kerak" Shart emas — shakl mos bo'lsa yetarli
"isinstance protokol bilan har doim ishlaydi" Faqat @runtime_checkable bilan
"isinstance imzoni tekshiradi" Faqat nom bor-yo'qligini
"Protocol — ABC ning yangi nomi" Turli mexanizm: strukturaviy va nominal
"Protokolda kod yozib bo'lmaydi" Mumkin (sukut implementatsiya), lekin kamdan-kam
"Har interfeys uchun protokol yozish kerak" Avval standart protokollarni qarang
"Protokol ish vaqtida himoya beradi" Kafolat — tekshiruvchidan
"Protokol va ABC ni birga ishlatib bo'lmaydi" Ko'p loyihalarda ikkalasi ham bor

6. Keng tarqalgan xatolar va yechimlari

1. Tashqi obyektlar uchun ABC talab qilish

python
def yoz(chiqish: AbstraktYozuvchi) -> None: ...    # ❌ io.StringIO mos kelmaydi
def yoz(chiqish: Yozuvchi) -> None: ...            # ✅ Protocol

2. isinstance ga ishonib, imzoni tekshirmaslik

python
if isinstance(obj, Yopiladigan):
    obj.close()                                     # ⚠️ close(majburiy) bo'lsa — TypeError

Statik tekshiruv yoki inspect.signature bilan qo'shimcha tekshiruv.

3. @runtime_checkable ni unutish

python
isinstance(x, MeningProtokolim)                     # ❌ TypeError

4. Protokolda ortiqcha metodlar

python
class Ombor(Protocol):
    def ol(self, k: str) -> str: ...
    def qoy(self, k: str, v: str) -> None: ...
    def tozala(self) -> None: ...
    def zaxira(self) -> bytes: ...                  # ⚠️ hamma implementatsiyaga kerakmi?

Kichik protokollar (interfeysni ajratish tamoyili).

5. Protokolni konstruktor sifatida ishlatish

python
def yarat(turi: type[Saqlagich]) -> Saqlagich:
    return turi()                                   # ⚠️ protokolda __init__ shartnomasi yo'q

Fabrika funksiyasi yoki reestr.

6. Ma'lumot atributini klass annotatsiyasi bilan chalkashtirish

python
class Nomli(Protocol):
    nom: str
class K:
    nom: str                                        # ⚠️ qiymat yo'q — isinstance False
class K:
    def __init__(self): self.nom = "a"              # ✅

7. Callable bilan murakkab imzoni ifodalash

python
jonat: Callable[..., bool]                          # ⚠️ kalitli argumentlar yo'qoldi
class Jonatuvchi(Protocol):                         # ✅
    def __call__(self, xabar: str, *, qayta: int = 0) -> bool: ...

8. Generik protokolni parametrsiz ishlatish

python
def f(ombor: Ombor) -> None: ...                    # ⚠️ T noma'lum
def f(ombor: Ombor[Foydalanuvchi]) -> None: ...     # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 8.18-dars (o'tilgan): abstrakt klasslar (ABC)
  • 10.15-dars (o'tilgan): ma'lumot modeli — dunder protokollari
  • 13.2-dars (o'tilgan): Callable va kolleksiya turlari
  • 13.4-dars (o'tilgan): generiklar — generik protokollar uchun
  • 13.9-dars: turlar bilan loyiha dizayni — portlar va adapterlar
  • 17-qism: testlar — soxta obyektlar protokolga mos keladi
  • 20–23-qismlar: servis qatlamlari, repozitoriylar, tashqi API mijozlari
  • 27-qism: arxitektura — bog'liqliklarni teskari aylantirish (DIP)

8. Eng yaxshi amaliyotlar

  1. Chegaralarda Protocol, ierarxiyada ABC.

  2. Protokollarni kichik ushlang — "faqat kerakli metodlar".

  3. Standart protokollarni qidiring (Sized, Iterable, SupportsIndex).

  4. @runtime_checkable ni faqat kerak bo'lganda va uning cheklovlarini bilib qo'shing.

  5. Murakkab imzolar uchun chaqiriladigan protokol.

  6. Generik protokollarni parametr bilan ishlating.

  7. Test obyektlari protokolga mos bo'lsin — Mock o'rniga oddiy klass.

  8. Statik tekshiruvchiga tayaning — ish vaqtidagi tekshiruv taxminiy.


9. Amaliy topshiriq

Vazifa 1: Natijani bashorat qiling

python
import typing
@typing.runtime_checkable
class Yopiladigan(typing.Protocol):
    def close(self) -> None: ...
class Tekshirilmas(typing.Protocol):
    def ishla(self) -> None: ...
class A:
    def close(self) -> None: ...
class B:
    def close(self, x: int) -> None: ...
1.  print(isinstance(A(), Yopiladigan))
2.  print(isinstance(B(), Yopiladigan))
3.  print(isinstance("matn", Yopiladigan))
4.  try:
        isinstance(A(), Tekshirilmas)
    except TypeError:
        print("TypeError")
5.  print(issubclass(A, Yopiladigan))
6.  @typing.runtime_checkable
    class Nomli(typing.Protocol):
        nom: str
    class K:
        def __init__(self):
            self.nom = "x"
    print(isinstance(K(), Nomli))
7.  class L:
        nom: str
    print(isinstance(L(), Nomli))
8.  try:
        issubclass(K, Nomli)
    except TypeError:
        print("TypeError")
9.  import io
    print(isinstance(io.StringIO(), Yopiladigan))
10. from collections.abc import Sized, Iterable
    print(isinstance([1], Sized), isinstance(5, Sized))
11. print(isinstance(5, typing.SupportsIndex), isinstance(5.0, typing.SupportsIndex))
12. class Ombor[T](typing.Protocol):
        def ol(self) -> T: ...
    print(Ombor.__type_params__)
Javoblar
  1. True
  2. True — imzo tekshirilmaydi
  3. False
  4. TypeError
  5. True
  6. True
  7. False — faqat annotatsiya, qiymat yo'q
  8. TypeError — ma'lumot atributli protokol bilan issubclass ishlamaydi
  9. True
  10. True False
  11. True False — float da __index__ yo'q
  12. (T,)

Vazifa 2: Xatolarni tuzating

python
1.  class Yozuvchi(ABC):
        @abstractmethod
        def write(self, matn: str) -> int: ...
    def hisobot(chiqish: Yozuvchi) -> None:    # io.StringIO uzatiladi
        ...

2.  class Ombor(Protocol):
        def ol(self, kalit: str) -> str: ...
    def ishla(ombor: Ombor) -> None:
        if isinstance(ombor, Ombor):
            ...

3.  def yubor(jonatuvchi: Callable[..., bool], xabar: str) -> bool:
        return jonatuvchi(xabar, qayta_urinish=2)

4.  class Saqlagich(Protocol):
        def saqla(self, k: str, v: str) -> None: ...
        def oqi(self, k: str) -> str: ...
        def zaxirala(self) -> bytes: ...
        def tikla(self, z: bytes) -> None: ...
        def migratsiya(self) -> None: ...

5.  @runtime_checkable
    class Ishlovchi(Protocol):
        def ishla(self) -> None: ...
    def bajar(obj: object) -> None:
        if isinstance(obj, Ishlovchi):
            obj.ishla()          # imzo mos kelmasligi mumkin
Javoblar
python
1.  class Yozuvchi(Protocol):                        # meros talab qilinmaydi
        def write(self, matn: str) -> int: ...

2.  def ishla(ombor: Ombor) -> None:
        ...                                          # isinstance shart emas — tekshiruvchi bajaradi
    # kerak bo'lsa: @runtime_checkable va cheklovlarini bilib

3.  class Jonatuvchi(Protocol):
        def __call__(self, xabar: str, *, qayta_urinish: int = 0) -> bool: ...
    def yubor(jonatuvchi: Jonatuvchi, xabar: str) -> bool:
        return jonatuvchi(xabar, qayta_urinish=2)

4.  class Saqlagich(Protocol):                       # kichik interfeys
        def saqla(self, k: str, v: str) -> None: ...
        def oqi(self, k: str) -> str: ...
    class Zaxiralanadigan(Protocol):                 # alohida protokol
        def zaxirala(self) -> bytes: ...
        def tikla(self, z: bytes) -> None: ...

5.  def bajar(obj: Ishlovchi) -> None:               # turni imzoda talab qiling
        obj.ishla()
    # ish vaqtida kerak bo'lsa: try/except TypeError yoki imzoni tekshirish

Vazifa 3: Protokollar kutubxonasi

Loyihangiz uchun protokollar to'plamini yozing:

  1. Saqlagich, Jurnalchi, Soat (vaqt manbai), Xabarnoma (yuborish)
  2. Har biri uchun kamida ikki implementatsiya: real va test uchun
  3. Soat protokoli bilan testlarda vaqtni boshqaring (time.time o'rniga)
  4. Barcha funksiyalar protokollarni qabul qilsin, konkret klasslarni emas
  5. Implementatsiyalarni almashtirib, testlar o'zgarmasligini ko'rsating

Vazifa 4: Moslik tekshiruvchisi

Misol 4 dagi protokolga_mosmi ni rivojlantiring:

  1. Argument turlarini ham solishtirsin (inspect.signature annotatsiyalari)
  2. Qaytish turini tekshirsin
  3. Kalitli va sukut qiymatli argumentlarni hisobga olsin
  4. *args/**kwargs bilan ishlasin
  5. Natijani mypy xulosasi bilan solishtiring (13.7-darsdan keyin)

Vazifa 5: Standart protokollar bilan tanishuv

  1. collections.abc va typing dagi barcha protokollarni ro'yxatga oling
  2. Har biri qaysi dunder metodlarni talab qilishini aniqlang (__abstractmethods__)
  3. Standart turlar (list, dict, str, int, generator, fayl) qaysi protokollarga mos kelishini jadval qiling
  4. O'z klassingizni Sequence protokoliga to'liq mos qilib yozing va tekshiring

Vazifa 6: Plagin tizimi

Protocol asosida plagin tizimi yozing:

  1. Plagin protokoli: nom, versiya, ishga_tushir(kontekst)
  2. Plaginlarni papkadan yuklovchi reestr (importlib)
  3. Yuklashda protokolga moslikni tekshirish va mos kelmaganini rad etish
  4. Uchta namuna plagin yozing va ularni ishga tushiring
  5. Xatolarni izolyatsiya qiling: bitta plagin yiqilsa, qolgani ishlasin

Vazifa 7: O'ylash

Go'da interfeyslar strukturaviy: tip interfeysdagi metodlarga ega bo'lsa, u avtomatik mos keladi (Python Protocol kabi); Java va C# da interfeyslar nominal: implements yozish shart; TypeScript'da esa strukturaviy turlash butun tilning asosi. Bu farqlar arxitekturaga qanday ta'sir qiladi va Python ikkala modelni ham qo'llab-quvvatlagani yaxshimi?

Javob

Qisqa javob: nominal interfeyslar niyatni aniq bildiradi ("men bu shartnomani bajaraman"), strukturaviy interfeyslar esa bog'liqlikni kamaytiradi (implementatsiya interfeys haqida bilmasligi mumkin). Python ikkalasini ham berib, chegaralarda strukturaviy, ierarxiyalarda nominal yondashuvni tanlash imkonini yaratadi.

1. Modellar

Til Model Xususiyat
Go Strukturaviy Interfeys iste'molchi tomonda e'lon qilinadi ("accept interfaces, return structs")
TypeScript Strukturaviy Butun tur tizimi shunday
Java, C# Nominal implements/: yozilishi shart
Rust Nominal (trait) + impl Tashqi tur uchun ham impl yozish mumkin
Python Ikkalasi Protocol (strukturaviy) va ABC (nominal)

2. Strukturaviy yondashuvning kuchi

python
def hisobot(chiqish: Yozuvchi) -> None: ...
  • io.StringIO, fayl, tarmoq soketi, test obyekti — hech biri sizning kodingiz haqida bilmaydi
  • Interfeys iste'molchi tomonda e'lon qilinadi — Go falsafasi: "kichik interfeyslar, iste'molchi tomonda"
  • Bog'liqlik yo'nalishi teskari aylantiriladi (DIP, 27-qism)

3. Nominal yondashuvning kuchi

  • Niyat aniq: klass nima ekanini nomidan bilish mumkin
  • Sukut implementatsiya va umumiy xulqni ota klass beradi
  • isinstance ishonchli ishlaydi — ish vaqtidagi dispatch uchun
  • Tasodifiy moslik yo'q: bir xil nomli metod tasodifan mos kelib qolmaydi

4. Tasodifiy moslik muammosi

python
class Otish(Protocol):
    def otish(self) -> None: ...      # "o'q uzish"

class Eshik:
    def otish(self) -> None: ...      # "o'tish"? — tasodifan mos keldi

Nominal tizimda bunday chalkashlik bo'lmaydi. Amalda protokol nomlari va metod nomlarini aniq tanlash bilan hal qilinadi.

5. Python uchun xulosa

  1. Chegarada (funksiya parametrlari, portlar) — Protocol
  2. Ichkarida (o'z ierarxiyangiz, umumiy kod) — ABC yoki oddiy meros
  3. Interfeysni iste'molchi yonida e'lon qiling — u faqat kerakli metodlarni talab qilsin
  4. Ikkala modelni birga ishlatish — cheklov emas, imkoniyat: Go va Java tajribasining eng yaxshi qismlari

Nimani mustahkamlaydi: 2.1–2.7-bo'limlar.


Xulosa

Bu darsda strukturaviy turlashni o'rgandik.

Eng muhim uch fikr:

  1. Protocol — duck typing ning turlangan ko'rinishi. Funksiya "menga write(str) -> int kerak" deb aytadi va io.StringIO, o'z klassingiz yoki test obyekti — hammasi merossiz mos keladi. ABC esa meros talab qiladi, shuning uchun tashqi kutubxona obyektlari bilan ishlamaydi. Amaliy taqsimot: chegaralarda Protocol, o'z ierarxiyangizda ABC.

  2. Ish vaqtidagi tekshiruv cheklangan. @runtime_checkable bo'lmasa isinstance TypeError beradi; bo'lsa ham u faqat nomlar borligini tekshiradi — close(self, majburiy) ham "mos" hisoblanadi. issubclass esa ma'lumot atributli protokollar bilan umuman ishlamaydi. Haqiqiy kafolat statik tekshiruvchidan keladi.

  3. Protokollar generik va chaqiriladigan bo'lishi mumkin. class Ombor[T](Protocol) — har model uchun alohida tekshiriladigan interfeys; def __call__(self, xabar: str, *, qayta: int = 0) -> bool — Callable[..., bool] yo'qotadigan imzoni saqlaydi. Yangi protokol yozishdan oldin standartlarini qarang: Sized, Iterable, Hashable, SupportsIndex.

Keyingi darsda lug'at va kortejlarni turlangan qilishni o'rganamiz: TypedDict bilan JSON tuzilmalari, NamedTuple, dataclass bilan solishtirish va qaysi birini qachon tanlash.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
13.5-dars: Protocol — strukturaviy turlash — IlmHamroh