Madarasa na OOP katika Python: __init__, self, mbinu, urithi na dataclass
OOP katika Python inategemea madarasa: darasa linaeleza sifa na mbinu, na vitu (mifano) huundwa kwa kuliita darasa; __init__ hujaza sifa, na self inaonyesha kitu cha sasa. Urithi huruhusu kuchukua tabia ya mzazi na kubadilisha baadhi ya mbinu, na decorator @dataclass hutengeneza __init__, __repr__ na __eq__ kwa madarasa yanayoshikilia data zaidi.
Darasa katika Python ni mchoro: linaeleza data ambayo kitu kinahifadhi na kile kinachoweza kufanyiwa. Kitu (mfano, au instance) ni bidhaa halisi iliyotengenezwa kwa mchoro huo. OOP katika Python inasimama juu ya mambo matatu: madarasa yenye mbinu, urithi, na mbinu maalum kama __init__ na __repr__. Hapa tutaziangalia zote kwa mpangilio, kwa mfano mmoja: ufuatiliaji wa matumizi.
Kwa nini madarasa, wakati kuna kamusi
Data inaweza kuhifadhiwa kwenye kamusi: {"title": "Kahawa", "amount": 250}. Tatizo ni kwamba kamusi haijui ni funguo zipi ni sahihi. Hitilafu ya kuandika expense.get("amout", 0) haitatoa kosa — itarudisha sifuri, na ripoti itaanza kudanganya kimyakimya.
Kitu cha darasa kina sehemu zilizotangazwa mapema. Kufikia sifa isiyokuwepo kunasababisha AttributeError mara moja, na Python ya kisasa hata inapendekeza jina sahihi. Zaidi ya hayo, darasa linakusanya data na vitendo vyake mahali pamoja: matumizi yenyewe yanaweza kuhesabu kodi, na ripoti yenyewe inahesabu jumla.
Jinsi ya kuunda darasa katika Python
Darasa linatangazwa kwa neno class, na jina kwa mujibu wa PEP 8 linaandikwa kwa mtindo wa CamelCase. Mfano rahisi kabisa:
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
coffee = Expense("Kahawa", 250)
taxi = Expense("Teksi", 300)
print(coffee.title, coffee.amount) # Kahawa 250
Wito Expense("Kahawa", 250) huunda mfano mpya wa darasa. coffee na taxi ni vitu viwili tofauti vya aina moja, kila kimoja na thamani zake. Kuangalia kama kitu ni cha darasa fulani kunafanywa kwa isinstance(coffee, Expense) — itarudisha True.
Mjenzi __init__: kuanzisha darasa
__init__ mara nyingi huitwa konstrukta ya darasa, ingawa kwa usahihi ni kianzishi (initializer). Kitu chenyewe huundwa na mbinu __new__, kisha __init__ huitwa mara baada ya hapo na kuweka hoja zilizopitishwa kwenye sifa. Haihitaji kurudisha kitu chochote (kwa usahihi, lazima irudishe None).
Katika __init__ ni rahisi kukagua data ya ingizo, ili programu isiwe na vitu visivyo sahihi kwa makusudi:
class Expense:
def __init__(self, title, amount):
if amount < 0:
raise ValueError("Kiasi hakiwezi kuwa hasi")
self.title = title
self.amount = amount
self katika Python ni nini
self ni marejeo ya kitu hasa ambacho mbinu inafanya kazi nacho kwa sasa. Unapoandika coffee.with_tax(), Python kwa kweli huita Expense.with_tax(coffee) na kupitisha kitu kama hoja ya kwanza. Ndiyo maana self huandikwa kwanza kwenye tangazo la mbinu, lakini wakati wa wito haiandikwi.
self si neno la hifadhi, bali ni makubaliano: kitaalamu parameta inaweza kuitwa chochote, lakini hakuna anayefanya hivyo. Kosa la kawaida la wanaoanza ni kusahau self kwenye tangazo la mbinu. Kisha wito unaanguka na TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given.
Mbinu za darasa na mbinu maalum
Mbinu ni kazi iliyotangazwa ndani ya darasa. Inaona sifa za kitu kupitia self:
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
def with_tax(self, rate=0.2):
return self.amount * (1 + rate)
def __repr__(self):
return f"Expense({self.title!r}, {self.amount})"
coffee = Expense("Kahawa", 250)
print(coffee.with_tax()) # 300.0
print(coffee) # Expense('Kahawa', 250)
Bila __repr__, matokeo yangeonekana kama <__main__.Expense object at 0x7f...> — hayana msaada kwa utatuzi. Mbinu zenye mistari miwili ya chini pande zote mbili huitwa maalum (dunder): Python huziita yenyewe. __str__ inahusika na uchapishaji "mzuri" kwa mtumiaji, __eq__ na ulinganisho kwa ==, __len__ na len(obj).
@classmethod na @staticmethod
Mbinu ya kawaida hufanya kazi na mfano. Mbinu ya darasa yenye decorator @classmethod inapokea darasa lenyewe (cls) kama hoja ya kwanza — mara nyingi hutumika kama konstrukta mbadala. Mbinu tuli @staticmethod haipokei kitu wala darasa: ni kazi ya kawaida inayohusiana kimantiki na darasa.
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
@classmethod
def from_csv(cls, line):
title, amount = line.split(";")
return cls(title, float(amount))
@staticmethod
def is_valid_amount(value):
return value >= 0
e = Expense.from_csv("Chakula cha mchana;520")
print(e.amount) # 520.0
print(Expense.is_valid_amount(-1)) # False
Mharibu wa darasa: __del__
Python ina mbinu __del__ inayoitwa kabla kitu hakijaondolewa na mkusanyaji taka (garbage collector). Usitegemee kwa kuachilia rasilimali: wakati wa wito haujahakikishwa. Kwa faili na miunganisho tumia meneja wa muktadha with au wito wa wazi close().
Sifa za darasa na sifa za mfano
Sehemu zilizowekwa kupitia self.jina = ... ni za kitu fulani. Vigezo vilivyotangazwa moja kwa moja ndani ya mwili wa darasa ni sifa za darasa: ni za pamoja kwa mifano yote. Hapo ndipo mara nyingi huhifadhiwa thamani za kudumu za darasa.
class Expense:
CURRENCY = "TZS" # sifa ya darasa, thamani ya kudumu
def __init__(self, title, amount):
self.title = title # sifa za mfano
self.amount = amount
a = Expense("Kahawa", 250)
b = Expense("Chai", 150)
print(a.CURRENCY, Expense.CURRENCY) # TZS TZS
Mtego hatari ni sifa ya darasa inayoweza kubadilika. Ukitangaza rows = [] ndani ya darasa na kuongeza kupitia self.rows.append(...), orodha hiyo itakuwa ya pamoja kwa vitu vyote. Sehemu zinazoweza kubadilika zinapaswa kila mara kuundwa ndani ya __init__.
Unaweza kuona sehemu za kitu kupitia vars(obj) au obj.__dict__, na kusoma sifa kwa jina lililo kwenye mfuatano kwa getattr(obj, "amount").
Sifa za darasa: @property
Property ni mbinu inayosomwa kutoka nje kama sehemu ya kawaida, bila mabano. Ni rahisi kwa thamani zinazohesabiwa: jumla ya ripoti haihitaji kuhifadhiwa tofauti, na huna hatari ya kusahau kuisasisha.
class Report:
def __init__(self):
self.rows = []
def add(self, expense):
self.rows.append(expense)
@property
def total(self):
return sum(e.amount for e in self.rows)
r = Report()
r.add(Expense("Kahawa", 250))
r.add(Expense("Teksi", 300))
print(r.total) # 550
Ikiwa unahitaji kudhibiti uandikaji, ongeza setter kwa @total.setter. Bila setter, mgawo r.total = 0 utasababisha AttributeError.
Kuhusu encapsulation: sehemu za faragha kwa maana kali hazipo katika Python. Mstari mmoja wa chini (_cache) ni makubaliano ya "usiguse kutoka nje", na mistari miwili (__secret) huwasha name mangling, ili sehemu isifunikwe kwa bahati mbaya na mrithi.
Urithi wa madarasa katika Python
Urithi unaruhusu kuunda darasa la mtoto kutoka kwa darasa la mzazi: mtoto anapata sehemu na mbinu zote, na zile zinazohitajika anazibadilisha. Darasa la mzazi linaandikwa kwenye mabano.
class MonthlyReport(Report):
def __init__(self, month):
super().__init__() # itisha __init__ ya mzazi
self.month = month
def add(self, expense): # kubadilisha mbinu
print(f"[{self.month}] + {expense}")
super().add(expense) # kuhifadhi tabia ya mzazi
m = MonthlyReport("Oktoba")
m.add(Expense("Kahawa", 250)) # [Oktoba] + Expense('Kahawa', 250)
print(m.total) # 250 — property imerithiwa
super() hurudisha kitu cha kati cha kuita mbinu za mzazi. Kusahau super().__init__() kwenye __init__ ya mtoto ni kosa la kawaida: sehemu rows haijaundwa, na add ya kwanza inaanguka na AttributeError.
Uhusiano wa urithi unakaguliwa kwa issubclass(MonthlyReport, Report). Madarasa yote katika Python 3 yanarithi kimyakimya kutoka object — ndiyo chanzo cha __repr__, __eq__ na mbinu nyingine za msingi. Python inaruhusu pia urithi wa aina nyingi (class C(A, B)); mpangilio wa kutafuta mbinu unaonyeshwa na C.__mro__.
Darasa dhahania katika Python
Darasa dhahania (abstract class) linaelezea kiolesura: ni mbinu zipi lazima madarasa ya mrithi yazitekeleze. Huwezi kuunda mfano wake. Kwa hili kuna moduli abc:
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, report): ...
class CsvExporter(Exporter):
def export(self, report):
return "\n".join(f"{e.title};{e.amount}" for e in report.rows)
Exporter() # TypeError: Can't instantiate abstract class Exporter ...
Darasa la mrithi lisilotekeleza mbinu zote dhahania nalo pia linabaki dhahania, na wakati wa kuunda kitu litatoa kosa lilelile.
Muundo (composition) badala ya miti mirefu ya urithi
Baada ya kujifunza urithi, inavutia kujenga ngazi: tumizi la msingi, tumizi la fedha, tumizi la shilingi — na kadhalika hadi ngazi tano. Katika Python hilo hufanyika mara chache. Urithi unaeleza uhusiano wa "ni", na muundo unaeleza "ina": ripoti si orodha ya matumizi, bali ina orodha hiyo kwenye sehemu rows.
Kanuni ya vitendo: ikiwa unahitaji tu uwezo wa kitu kingine, weka kitu hicho kwenye sehemu, badala ya kurithi. Kwa data mara nyingi dataclass inatosha, na mantiki inaweza kuwekwa kwenye kazi za kawaida.
Dataclasses katika Python: darasa bila msimbo wa kurudiarudia
Kwa madarasa yanayoshikilia data zaidi, kuandika __init__, __repr__ na __eq__ kwa mkono ni kazi ya kuchosha. Decorator @dataclass kutoka moduli dataclasses huzitengeneza kutoka kwa maelezo ya aina za sehemu:
from dataclasses import dataclass, field
@dataclass
class Expense:
title: str
amount: float
tags: list[str] = field(default_factory=list)
def __post_init__(self):
if self.amount < 0:
raise ValueError("Kiasi hakiwezi kuwa hasi")
e = Expense("Kahawa", 250)
print(e) # Expense(title='Kahawa', amount=250, tags=[])
print(e == Expense("Kahawa", 250)) # True
Ukaguzi wa thamani uliokuwa ukiandikwa kwenye __init__ unahamishiwa kwenye __post_init__ — inaitwa mara baada ya kianzishi kilichotengenezwa. Thamani chaguo-msingi zinazoweza kubadilika zinatolewa tu kwa field(default_factory=list): andiko la tags: list = [] dataclass inalikataa na kutoa ValueError.
frozen na slots
Parameta frozen=True hufanya kitu kisibadilike, na slots=True (tangu Python 3.10) huunda __slots__ na kuokoa kumbukumbu. Jaribio la kubadilisha sehemu ya kitu kilichogandishwa linasababisha FrozenInstanceError. Ili kupata kitu chenye thamani mpya, unaunda nakala:
from dataclasses import dataclass, replace
@dataclass(frozen=True, slots=True)
class Expense:
title: str
amount: float
e = Expense("Kahawa", 250)
# e.amount = 300 -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2) # Expense(title='Kahawa', amount=250) Expense(title='Kahawa', amount=300)
Kuanzia Python 3.13 kazi ya jumla copy.replace() inafanya kitu hicho hicho. Vitu vya dataclass vilivyogandishwa vinaweza kuwekwa kwenye seti na kutumika kama funguo za kamusi — vina __hash__.
Mambo mapya katika madarasa: Self, @override, __slots__
Kwa uandishi wa aina za madarasa kuna zana mbili muhimu. typing.Self (Python 3.11, PEP 673) inamaanisha "itarudi kitu cha darasa hilohilo" — hii inafanya kazi pia kwenye mbinu zilizorithiwa. Decorator @override (Python 3.12, PEP 698) inaweka alama kwenye mbinu ambayo lazima ibadilishe ile ya mzazi: ukikosea jina, mypy au pyright zitakuonya kwamba mbinu ya msingi yenye jina hilo haipo.
from typing import Self, override
class Report:
def __init__(self):
self.rows = []
def add(self, e) -> Self:
self.rows.append(e)
return self # inaweza kuitwa mfululizo
class MonthlyReport(Report):
@override
def add(self, e) -> Self:
return super().add(e)
Katika Python 3.15 kizuizi kidogo kimeondolewa: madarasa ya mrithi wa tuple yanaruhusiwa __slots__ zisizo tupu, na __dict__ na __weakref__ vinaweza kutajwa kwenye __slots__ ya darasa lolote. Katika 3.14 tamko kama hilo kwa mrithi wa tuple lilianguka na TypeError: nonempty __slots__ not supported for subtype of 'tuple'.
Makosa ya kawaida katika madarasa
| Kosa | Dalili | Suluhisho |
|---|---|---|
Kusahau self kwenye mbinu | TypeError: ... takes 0 positional arguments but 1 was given | Parameta ya kwanza ya mbinu daima iwe self |
Sehemu bila self. kwenye __init__ | Thamani inapotea baada ya __init__ | Andika self.amount = amount |
| Orodha kama sifa ya darasa | Data inasafiri kati ya vitu | Unda orodha ndani ya __init__ au kwa default_factory |
Hakuna super().__init__() | AttributeError kwenye sehemu za mzazi | Itisha kianzishi cha mzazi |
| Kuita property kwa mabano | TypeError: 'int' object is not callable | Soma property bila mabano: r.total |
Hitimisho
OOP katika Python inaanza kwa jambo rahisi: darasa linaeleza sehemu na mbinu, __init__ inajaza sehemu, na self inaonyesha kitu cha sasa. Urithi na super() huruhusu kupanua tabia, madarasa dhahania yanaeleza kiolesura, na @property huunda sehemu zinazohesabiwa. Kwa madarasa ya data chukua @dataclass, na kwa miti mirefu ya urithi pendelea muundo. Katika kozi ya Python madarasa na dataclasses vinafundishwa kwa vitendo kwa mfano wa kifuatiliaji cha matumizi.
Maswali ya mara kwa mara
Darasa linatofautianaje na kitu katika Python?
Darasa ni maelezo ya aina: lina sehemu na mbinu gani. Kitu, au mfano, huundwa kwa kuliita darasa, kwa mfano Expense("Kahawa", 250), na kina thamani halisi za sehemu.
Je, ni lazima kuandika __init__ katika darasa?
Hapana. Kama __init__ haijatangazwa, inatumika ile ya object, ambayo haipokei hoja zozote. Kwenye dataclass inatengenezwa na decorator.
__str__ inatofautianaje na __repr__?
__repr__ ni uwakilishi usio na utata kwa msanidi na debugger; __str__ ni uchapishaji unaoeleweka kwa mtumiaji katika print na f-strings. Ikiwa __str__ haipo, Python hutumia __repr__.
Lini nichague dataclass na lini darasa la kawaida?
Dataclass inafaa darasa linaloshikilia data zaidi na linalohitaji __init__, __repr__ na ulinganisho tayari. Darasa la kawaida ni bora pale mantiki tata ya kuanzisha au tabia ni muhimu kuliko data.
Je, Python ina sehemu za faragha?
Faragha kali haipo. Mstari mmoja wa chini mwanzoni mwa jina ni makubaliano ya "si kwa matumizi ya nje", na mistari miwili huwasha name mangling inayolinda sehemu dhidi ya kufunikwa kwa bahati mbaya na mrithi.
Mada hii inafundishwa kwa kina pamoja na mazoezi katika kozi “Python 3.15: от новичка до профи”.
Nenda kwenye kozi