3-qism · Professional Python·13-bob
OOP chuqurroq: dunder metodlar, dataclass, ABC, Protocol va Enum
Python ma'lumot modeli: __repr__, __eq__, __hash__, __len__, __getitem__, __iter__, __call__ va operatorlar; dataclass bilan toza ma'lumot klasslari; abstrakt klasslar, Protocol (duck typing), Enum va __slots__.
Nega len(lst) ishlaydi, lekin lst.length() emas? Nega NumPy massivlarini
a + b deb qo'shish mumkin? Nega PyTorch modelini model(x) deb chaqirasiz?
Javob — Python'ning ma'lumot modeli (data model): maxsus nomli
metodlar (__len__, __add__, __call__...) orqali o'z klasslaringizni
tilning o'rnatilgan amallariga "ulash" mumkin. Bu bob kutubxonalar
ichida nima bo'layotganini tushunishga va o'zingiz shunday qulay API'lar
yaratishga o'rgatadi.
dataclass nima uchun qulay: avtomatik __init__, __repr__, __eq__, frozen va default_factory — bobning 7-bo'limi amalda.
1. Dunder metodlar — Python'ning "ilgaklari"#
Dunder — "double underscore" (__nom__). Bu metodlarni o'zingiz
to'g'ridan-to'g'ri chaqirmaysiz — Python ularni ma'lum amallarda
chaqiradi:
| Siz yozasiz | Python chaqiradi |
|---|---|
len(x) | x.__len__() |
x[i] | x.__getitem__(i) |
for a in x | x.__iter__() |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
x(arg) | x.__call__(arg) |
print(x), str(x) | x.__str__() |
REPL'da x, repr(x) | x.__repr__() |
with x: | x.__enter__() / x.__exit__() |
if x: | x.__bool__() (yo'q bo'lsa __len__) |
hash(x), dict kaliti | x.__hash__() |
2. __repr__ va __str__ — obyektni ko'rsatish#
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
print(p) # <__main__.Point object at 0x104a8f2d0> — foydasizclass Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self) -> str: # dasturchi uchun: aniq, iloji bo'lsa — qayta yaratish kodi
return f"Point(x={self.x!r}, y={self.y!r})"
def __str__(self) -> str: # foydalanuvchi uchun: chiroyli
return f"({self.x}, {self.y})"
p = Point(1, 2)
repr(p) # 'Point(x=1, y=2)'
str(p) # '(1, 2)'
f"{p}" # '(1, 2)'
f"{p!r}" # 'Point(x=1, y=2)'Har bir klassga __repr__ yozing — debug, log va Jupyter'da vaqtni
juda tejaydi. __str__ ixtiyoriy: yo'q bo'lsa, __repr__ ishlatiladi.
3. Tenglik va hash#
Standart holatda == — is bilan bir xil (bitta obyektmi). Qiymat
bo'yicha solishtirish uchun __eq__:
class Point:
...
def __eq__(self, other: object) -> bool:
if not isinstance(other, Point):
return NotImplemented # "bilmayman" — Python boshqa tomonni sinab ko'radi
return (self.x, self.y) == (other.x, other.y)
def __hash__(self) -> int:
return hash((self.x, self.y))Ikki muhim qoida:
__eq__yozsangiz, Python__hash__niNoneqiladi — obyekt lug'at kaliti yoki to'plam elementi bo'la olmay qoladi. Agar obyekt o'zgarmas bo'lsa —__hash__ni ham yozing; o'zgaruvchan bo'lsa — hash qilinmasin.- Teng obyektlarning hashi teng bo'lishi shart:
a == bbo'lsa,hash(a) == hash(b). Aks holda lug'atlar buziladi.
NotImplemented (NotImplementedError emas!) — bu maxsus qiymat:
"men bu tip bilan ishlashni bilmayman". Python keyin boshqa operandning
metodini sinab ko'radi va hech kim bilmasa — False (yoki operatorlar
uchun TypeError) qaytaradi.
4. Operatorlarni qayta yuklash: Vector klassi#
12-bobdagi mashqni davom ettiramiz — ML'dagi embedding vektorlarining soddalashtirilgan versiyasi:
import math
class Vector:
def __init__(self, *values: float) -> None:
self.values = tuple(float(v) for v in values)
def __repr__(self) -> str:
return f"Vector{self.values}"
def __len__(self) -> int:
return len(self.values)
def __getitem__(self, i):
return self.values[i]
def __iter__(self):
return iter(self.values)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self.values == other.values
def __hash__(self) -> int:
return hash(self.values)
def __add__(self, other: "Vector") -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
return Vector(*(a + b for a, b in zip(self, other, strict=True)))
def __mul__(self, k: float) -> "Vector": # v * 2
if not isinstance(k, (int, float)):
return NotImplemented
return Vector(*(a * k for a in self))
__rmul__ = __mul__ # 2 * v
def __matmul__(self, other: "Vector") -> float: # v @ w — skalyar ko'paytma
return sum(a * b for a, b in zip(self, other, strict=True))
def __abs__(self) -> float: # abs(v) — uzunlik
return math.sqrt(self @ self)
def __bool__(self) -> bool: # nol vektor — False
return any(self.values)
v = Vector(1, 2)
w = Vector(3, 4)
v + w # Vector(4.0, 6.0)
2 * v # Vector(2.0, 4.0) — __rmul__
v @ w # 11.0
abs(w) # 5.0
v[0], len(v) # (1.0, 2)
list(v) # [1.0, 2.0]
v == Vector(1, 2) # True
bool(Vector(0, 0)) # False
{v: "embedding"} # hash bor — kalit bo'la oladiTeskari operatorlar#
2 * v da Python avval int.__mul__(2, v) ni sinaydi — int Vectorni
bilmaydi va NotImplemented qaytaradi. Keyin Python v.__rmul__(2) ni
chaqiradi. Shuning uchun __rmul__, __radd__ kabi "r" (reflected)
metodlar kerak.
Operatorlar jadvali#
| Operator | Metod | Teskari | Joyida |
|---|---|---|---|
+ | __add__ | __radd__ | __iadd__ (+=) |
- | __sub__ | __rsub__ | __isub__ |
* | __mul__ | __rmul__ | __imul__ |
/ | __truediv__ | __rtruediv__ | __itruediv__ |
@ | __matmul__ | __rmatmul__ | __imatmul__ |
<, <= | __lt__, __le__ | ||
-x, abs(x) | __neg__, __abs__ |
__iadd__ bo'lmasa, a += b → a = a + b (yangi obyekt). listda
__iadd__ bor (joyida o'zgartiradi) — 2-bobdagi += farqi shundan.
5. Konteyner protokollari#
__len__, __getitem__ va __iter__ yozsangiz, klassingiz "ro'yxatga
o'xshab" ishlaydi. ML'dagi eng muhim misol — dataset klassi. PyTorch
Dataset aynan shu ikki metodni talab qiladi:
import json
from pathlib import Path
class JsonlDataset:
"""PyTorch Dataset uslubidagi klass: len() va [i] qo'llab-quvvatlanadi."""
def __init__(self, path: Path, transform=None) -> None:
with open(path, encoding="utf-8") as f:
self.records = [json.loads(line) for line in f if line.strip()]
self.transform = transform
def __len__(self) -> int:
return len(self.records)
def __getitem__(self, idx: int) -> dict:
record = self.records[idx]
return self.transform(record) if self.transform else record
ds = JsonlDataset(Path("train.jsonl"), transform=lambda r: {**r, "text": r["text"].lower()})
len(ds) # yozuvlar soni
ds[0] # birinchi yozuv (transform qo'llangan)
for rec in ds: # __iter__ yo'q, lekin __getitem__ + IndexError orqali aylanish ishlaydi
...__contains__ (x in obj) ham qo'shish mumkin; yo'q bo'lsa Python
aylanib qidiradi.
6. __call__ — chaqiriladigan obyektlar#
Obyektni funksiya kabi chaqirish mumkin bo'lsin:
class Tokenizer:
def __init__(self, vocab: dict[str, int]) -> None:
self.vocab = vocab
self.unk = vocab.get("<unk>", 0)
def __call__(self, text: str) -> list[int]:
return [self.vocab.get(w, self.unk) for w in text.lower().split()]
tok = Tokenizer({"<unk>": 0, "salom": 1, "dunyo": 2})
tok("Salom dunyo!") # [1, 0] — "dunyo!" lug'atda yo'qHolatli funksiya kerak bo'lsa (sozlamalari, keshi, hisoblagichi bor) —
__call__li klass closure'dan (8-bob) ko'pincha tushunarliroq. PyTorch'da
model(x) aynan nn.Module.__call__ni chaqiradi — u esa hook'larni
ishlatib, sizning forward(x) metodingizni chaqiradi. Shuning uchun
model.forward(x) emas, model(x) deb yozish kerak.
7. dataclass — toza ma'lumot klasslari#
Ko'p klasslar asosan ma'lumot saqlaydi. Ular uchun __init__, __repr__,
__eq__ yozish — zerikarli takrorlash. @dataclass ularni avtomatik
yaratadi:
from dataclasses import dataclass, field, asdict, replace
@dataclass
class TrainConfig:
model: str = "small"
lr: float = 3e-4
epochs: int = 10
batch_size: int = 32
tags: list[str] = field(default_factory=list) # mutable default — faqat shunday!
cfg = TrainConfig(lr=1e-3)
cfg # TrainConfig(model='small', lr=0.001, epochs=10, batch_size=32, tags=[])
cfg == TrainConfig(lr=1e-3) # True — __eq__ maydonlar bo'yicha
asdict(cfg) # {'model': 'small', 'lr': 0.001, ...} — JSON'ga saqlash uchun
replace(cfg, epochs=5) # yangi obyekt, bitta maydon o'zgarganType annotatsiyalar bu yerda majburiy — dataclass maydonlarni
aynan shular orqali topadi. Mutable default'ni to'g'ridan-to'g'ri yozsangiz
(tags: list = []), dataclass o'zi xato beradi:
ValueError: mutable default <class 'list'> for field tags is not allowed: use default_factory.
Muhim parametrlar#
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
p = Point(1, 2)
p.x = 5 # FrozenInstanceError — o'zgarmas
{p: "markaz"} # frozen → avtomatik __hash__ → kalit bo'la oladi| Parametr | Nima beradi |
|---|---|
frozen=True | O'zgarmas obyekt + __hash__. Konfiguratsiyalar, kalitlar uchun |
slots=True | __slots__ (pastda) — kamroq xotira, tezroq atributlar |
order=True | <, > maydonlar tartibida — saralash mumkin |
kw_only=True | Hamma maydonlar faqat kalit so'z bilan |
__post_init__ — tekshiruv va hisoblanadigan maydonlar#
@dataclass
class Rect:
w: float
h: float
area: float = field(init=False) # __init__ da berilmaydi
def __post_init__(self) -> None:
if self.w <= 0 or self.h <= 0:
raise ValueError(f"O'lchamlar musbat bo'lishi kerak: {self.w}x{self.h}")
self.area = self.w * self.h
Rect(2, 3) # Rect(w=2, h=3, area=6)Dataclass, NamedTuple, dict yoki Pydantic?#
dict | NamedTuple | @dataclass | Pydantic BaseModel | |
|---|---|---|---|---|
| Atribut orqali kirish | ❌ | ✅ | ✅ | ✅ |
| Xato nomdagi maydon | Jim yangi kalit | Xato | Xato | Xato |
| O'zgaruvchan | ✅ | ❌ | ✅ (yoki frozen) | ✅ |
| Runtime tip tekshiruvi | ❌ | ❌ | ❌ | ✅ |
| JSON'dan/ga | Tabiiy | Qo'lda | asdict | model_validate_json / model_dump_json |
| Qachon | Tez skript, dinamik kalitlar | Kichik o'zgarmas yozuv | Ichki ma'lumot klasslari | Tashqi ma'lumot: API, konfiguratsiya, LLM chiqishi |
Qoida: ichki ma'lumot — dataclass; tashqaridan keladigan (ishonib bo'lmaydigan) ma'lumot — Pydantic (15-bob).
8. Abstrakt klasslar (ABC)#
Ota klass "shartnoma" belgilashi kerak bo'lsa — voris albatta qaysi metodlarni amalga oshirishi kerak:
from abc import ABC, abstractmethod
class Embedder(ABC):
@abstractmethod
def embed(self, texts: list[str]) -> list[list[float]]:
"""Matnlarni vektorlarga aylantiradi."""
def embed_one(self, text: str) -> list[float]: # oddiy metod — meros qilinadi
return self.embed([text])[0]
class HashEmbedder(Embedder):
"""Test uchun oddiy embedder: so'zlar hashidan vektor."""
def __init__(self, dim: int = 8) -> None:
self.dim = dim
def embed(self, texts: list[str]) -> list[list[float]]:
out = []
for t in texts:
v = [0.0] * self.dim
for w in t.lower().split():
v[hash(w) % self.dim] += 1.0
out.append(v)
return out
Embedder() # TypeError: Can't instantiate abstract class Embedder ...
HashEmbedder().embed_one("salom dunyo")12-bobda raise NotImplementedError ishlatgan edik — ABC'ning afzalligi:
xato metod chaqirilganda emas, obyekt yaratilganda chiqadi.
9. Protocol — strukturaviy tiplash (duck typing)#
"Agar u o'rdakdek yursa va o'rdakdek g'aqillasa — u o'rdak". Python'da
ko'pincha meros emas, kerakli metodlar borligi muhim. Protocol
buni tiplar darajasida ifodalaydi:
from typing import Protocol
class SupportsEmbed(Protocol):
def embed(self, texts: list[str]) -> list[list[float]]: ...
class OpenSourceEmbedder: # hech narsadan meros olmagan!
def embed(self, texts: list[str]) -> list[list[float]]:
return [[0.0] * 4 for _ in texts]
def build_index(embedder: SupportsEmbed, docs: list[str]) -> list[list[float]]:
return embedder.embed(docs)
build_index(OpenSourceEmbedder(), ["a", "b"]) # ✅ mypy qabul qiladi — kerakli metod bor| ABC | Protocol |
|---|---|
| Voris aniq meros olishi kerak (nominal) | Mos metodlar bo'lishi kifoya (strukturaviy) |
| Umumiy kod (oddiy metodlar) berishi mumkin | Faqat "shakl"ni tasvirlaydi |
| O'zingizning klasslar ierarxiyangiz uchun | Tashqi kutubxonalar klasslarini ham qamrab olish uchun |
Zamonaviy Python'da Protocol tobora ko'proq ishlatilmoqda — ayniqsa
"istalgan LLM klienti", "istalgan vektor bazasi" kabi almashtiriladigan
komponentlar uchun. 15-bobda batafsil.
10. Enum — cheklangan qiymatlar to'plami#
Sehrli satrlar ("user", "assistant") o'rniga:
from enum import Enum, StrEnum, auto
class Split(Enum):
TRAIN = "train"
VAL = "val"
TEST = "test"
class Role(StrEnum): # Python 3.11+ — qiymatlar satr sifatida ham ishlaydi
USER = auto() # "user"
ASSISTANT = auto() # "assistant"
Split.TRAIN.value # 'train'
Split("val") # Split.VAL — qiymatdan
Split["TEST"] # Split.TEST — nomdan
list(Split) # hamma a'zolar
Split.TRAIN == "train" # False — oddiy Enum satrga teng emas
Role.USER == "user" # True — StrEnum
{"role": Role.USER} # JSON'ga "user" bo'lib yoziladiAfzalligi: xato yozish imkonsiz (Split.TRIAN → darhol AttributeError,
"trian" esa jim o'tib ketardi), muharrir avtomatik to'ldiradi, matchda
ishlatish mumkin (case Split.TRAIN: — 4-bobdagi konstanta muammosi
yo'q).
11. __slots__ — xotirani tejash#
Har bir oddiy obyekt atributlarini __dict__ lug'atida saqlaydi — bu
qulay, lekin qimmat. __slots__ atributlar ro'yxatini qotiradi:
class PointSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p = PointSlots(1, 2)
p.z = 3 # AttributeError — yangi atribut qo'shib bo'lmaydiMillion kichik obyekt yaratilganda xotira 2–3 baravar kamayadi. Odatda
@dataclass(slots=True) eng qulay yo'l. Lekin agar shuncha ko'p
sonli obyekt bo'lsa — ehtimol sizga obyektlar emas, NumPy massivi
yoki DataFrame kerak.
12. Amaliyot: model registri#
Hamma narsani birlashtiramiz — turli LLM provayderlarini bitta interfeys orqali boshqarish:
from dataclasses import dataclass
from enum import StrEnum
from typing import Protocol
class Provider(StrEnum):
ANTHROPIC = "anthropic"
LOCAL = "local"
@dataclass(frozen=True, slots=True)
class ModelSpec:
name: str
provider: Provider
context_window: int
input_price: float # $ / 1M token
output_price: float
def cost(self, input_tokens: int, output_tokens: int) -> float:
return (input_tokens * self.input_price + output_tokens * self.output_price) / 1_000_000
class ChatModel(Protocol):
spec: ModelSpec
def complete(self, prompt: str) -> str: ...
class EchoModel:
"""Test uchun: javob sifatida promptni qaytaradi."""
def __init__(self) -> None:
self.spec = ModelSpec("echo", Provider.LOCAL, 1_000, 0.0, 0.0)
def complete(self, prompt: str) -> str:
return prompt[::-1]
class Registry:
def __init__(self) -> None:
self._models: dict[str, ChatModel] = {}
def register(self, model: ChatModel) -> None:
if model.spec.name in self._models:
raise ValueError(f"{model.spec.name!r} allaqachon ro'yxatda")
self._models[model.spec.name] = model
def __getitem__(self, name: str) -> ChatModel:
try:
return self._models[name]
except KeyError:
raise KeyError(f"{name!r} topilmadi. Mavjudlar: {sorted(self._models)}") from None
def __contains__(self, name: str) -> bool:
return name in self._models
def __len__(self) -> int:
return len(self._models)
def __repr__(self) -> str:
return f"Registry({sorted(self._models)})"
reg = Registry()
reg.register(EchoModel())
print(reg, len(reg), "echo" in reg) # Registry(['echo']) 1 True
print(reg["echo"].complete("salom")) # molas
print(ModelSpec("opus", Provider.ANTHROPIC, 1_000_000, 4.0, 20.0).cost(10_000, 1_000)) # 0.06raise ... from None — asl KeyError zanjirini yashiradi, foydalanuvchi
faqat tushunarli xabarni ko'radi.
Nima noto'g'ri ketishi mumkin#
| Xato | Oqibat | Tuzatish |
|---|---|---|
__repr__ yo'q | Log va debug'da <object at 0x...> | Har klassga __repr__ (yoki dataclass) |
__eq__ bor, __hash__ yo'q | Obyekt set/dict'ga tushmaydi | O'zgarmas bo'lsa __hash__ yoki frozen=True |
| Teng obyektlar — har xil hash | Lug'atda "yo'qolgan" kalitlar | Hash'ni __eq__dagi maydonlardan hisoblang |
raise NotImplementedError operatorda | Teskari operator sinab ko'rilmaydi | return NotImplemented |
__rmul__ yo'q | 2 * v → TypeError | Teskari metodni qo'shing |
model.forward(x) | Hook'lar ishlamaydi | model(x) |
Dataclass'da tags: list = [] | ValueError | field(default_factory=list) |
| Tashqi JSON'ni dataclass'ga | Tiplar tekshirilmaydi | Pydantic |
Sehrli satrlar ("trian") | Jim xato | Enum |
Split.TRAIN == "train" | False | .value yoki StrEnum |
1. Vector'ni to'ldiring
Vector klassiga - (ayirish va unar minus), / (songa bo'lish),
normalized() (uzunligi 1 bo'lgan vektor), cosine(other) metodlarini
qo'shing. Vector(1, 0).cosine(Vector(0, 1)) → 0.0,
Vector(1, 1).cosine(Vector(2, 2)) → 1.0 (taxminan).
Maslahatni ko'rsatish
__sub__ ni __add__ dagidek yozing; __neg__ uchun -1 ga ko'paytiring; __truediv__ da k == 0 holatini tekshiring; normalized = self / abs(self).
2. Dataset klassi
ListDataset(items, transform=None) klassini yozing: len(), ds[i],
ds[2:5] (yangi ListDataset qaytarsin), for x in ds, x in ds
ishlasin. Keyin split(ratio, seed) metodini qo'shing — ikkita
ListDataset qaytarsin.
Maslahatni ko'rsatish
__init__ da ro'yxatni saqlang; __getitem__ slice'ni ham qo'llab-quvvatlasin: isinstance(idx, slice) bo'lsa yangi Dataset qaytaring.
3. Konfiguratsiyani dataclass bilan
GenerationConfigni (12-bob mashqi) @dataclass(frozen=True) bilan qayta
yozing: tekshiruvlar __post_init__da, to_api_params() None
maydonlarsiz lug'at qaytarsin, from_dict(d) noma'lum kalit kelsa xato
bersin. Property versiyasi bilan solishtiring — qaysi biri qisqaroq?
Maslahatni ko'rsatish
@dataclass(frozen=True, kw_only=True); __post_init__ da tekshiruv; from_dict classmethod'ida noma'lum kalitlarni dataclasses.fields() bilan aniqlang.
4. Protocol bilan almashtiriladigan komponentlar
Retriever protokolini yozing (search(query: str, k: int) -> list[str])
va ikkita amalga oshirishni: KeywordRetriever (so'z mosligi soni bo'yicha)
va RandomRetriever (seed bilan). answer(question, retriever) funksiyasi
istalganini qabul qilsin. mypy bilan tekshiring (15-bobdan oldin ham
uv add --dev mypy && uv run mypy fayl.py ishlaydi).
- 01Dunder metodlar klassingizni Python sintaksisiga ulaydi: len, [], for, +, ==, (), with.
- 02__repr__ har doim yozing; __str__ — foydalanuvchi uchun chiroyli ko'rinish.
- 03__eq__ yozsangiz __hash__ yo'qoladi; teng obyektlar teng hashga ega bo'lishi shart.
- 04Operator metodlari noma'lum tip uchun NotImplemented qaytarsin; 2 * v uchun __rmul__.
- 05__len__ + __getitem__ — dataset; __call__ — chaqiriladigan obyekt (model(x)).
- 06@dataclass: __init__/__repr__/__eq__ avtomatik; field(default_factory=...), frozen, slots, __post_init__.
- 07ABC — meros orqali shartnoma; Protocol — strukturaviy (duck typing) shartnoma.
- 08Enum/StrEnum — sehrli satrlar o'rniga; __slots__ — ko'p kichik obyektlar uchun xotira.
Mustahkamlash
Test va amaliy masalalar
Avval testni ishlang — har javobdan keyin izoh chiqadi. Keyin masalalarni shu yerning o'zida yechib, brauzerdagi haqiqiy Python bilan avtomatik tekshiring. Natijalaringiz kurs progressi sahifasida jamlanadi.
Bilimni tekshirish
Bob bo'yicha test
12 ta savol · har javobdan keyin to'g'ri yoki xatoligi va izoh ko'rsatiladi · oxirida natija foizda.
Amaliy masalalar
Kodni yozing va «Tekshirish»ni bosing (yoki Ctrl/⌘ + Enter). Kodingiz shu brauzerda saqlanib turadi.
01Vector klassi
Vector(*values) klassi: __repr__ → Vector(1.0, 2.0); __len__; __getitem__; __eq__ va __hash__; v + w (uzunlik farqi → ValueError); v * 2 va 2 * v; v @ w — skalyar ko'paytma; abs(v) — uzunlik. Qiymatlarni float tuple sifatida saqlang.
02Dataset klassi
ListDataset(items, transform=None): len(ds), ds[i] (transform qo'llangan), ds[1:3] — yangi ListDataset, x in ds (asl elementlar bo'yicha). transform slice'dagi datasetga ham o'tsin.
03Frozen konfiguratsiya
@dataclass(frozen=True) bilan GenerationConfig: model: str, max_tokens: int = 1024, effort: str = "medium". __post_init__ da tekshiring: max_tokens 1..128000 oralig'ida, effort — low/medium/high/xhigh/max dan biri, aks holda ValueError. to_params() — lug'at qaytarsin.