Yaliyomo

Wiki 6: OOP Sehemu ya 1 — Classes na Objects

Mwezi 2 | Lengo kuu: Mwanafunzi aelewe na atumie classes, objects, __init__, self, methods, class vs instance attributes, encapsulation (_private, @property) na __str__/__repr__, na kujenga mini project ya BankAccount na Duka.

Maandalizi ya Mwalimu (soma kabla ya wiki kuanza)

  • Soma: Python Tutorial — "Classes", sehemu 9.1–9.4 tu: https://docs.python.org/3/tutorial/classes.html . Usijali sehemu za inheritance/iterators — ni Wiki 7–8.
  • Zoezi binafsi: Andika class ya Mbwa na AkauntiYaBenki mwenyewe mara mbili bila kuangalia, mpaka uelewe self ni nini. Endesha kila mfano wa "Andika code hii live".
  • Wazo kuu la kukumbuka: Class ni ramani (blueprint); object ni kitu halisi kilichojengwa kwa ramani hiyo. Ukielewa hili, mengine yote yanafuata.
  • Andaa folder: python_kozi\wiki06\. Hakuna package mpya ya kusakinisha.
  • Tumia Python Tutor (hiari): https://pythontutor.com — inaonyesha objects kwa michoro; nzuri kuonyesha self kwa mwanafunzi.

Malengo ya Wiki (mwisho wa wiki mwanafunzi ataweza...)

  1. Kueleza class, object (instance), attribute, na method kwa maneno yake mwenyewe.
  2. Kuandika class yenye __init__ na kutengeneza objects kadhaa.
  3. Kueleza self ni nini na kwa nini ni parameter ya kwanza ya kila method.
  4. Kutofautisha class attributes na instance attributes.
  5. Kulinda data kwa _jina (convention) na @property / setter yenye validation.
  6. Kuandika __str__ na __repr__ ili objects zichapishwe vizuri.
  7. Kujenga mfumo mdogo (BankAccount, Duka) wenye classes mbili zinazoshirikiana.

Kipindi 1 — Classes, Objects, __init__ na self

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani ya Wiki 5 (word counter, daftari la alama)
  2. Kwa nini OOP? Tatizo la dicts nyingi + functions zilizotawanyika
  3. Class ya kwanza, objects, attributes
  4. __init__
  5. Mapumziko
  6. Methods na self
  7. Zoezi darasani
  8. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

OOP (Object-Oriented Programming) ni mtindo wa kupanga code ambapo tunaunganisha data (attributes) na tabia (methods) zinazohusiana ndani ya kitu kimoja — object.

Kumbuka Wiki 5: tulikuwa na dict ya mwanafunzi {"jina": "Asha", "alama": 88} na functions tofauti kama ongeza_alama(mwanafunzi, n). Hiyo inafanya kazi, lakini programu ikikua: dict zinaweza kuwa na keys zilizokosewa ("jna"), hakuna mahali pamoja panaposema "mwanafunzi ana nini na anaweza kufanya nini". Class inatatua hilo.

Maneno muhimu:

  • class — ramani/kiolezo. Kwa kawaida jina linaanza na herufi kubwa: Mwanafunzi, AkauntiYaBenki (PascalCase).
  • object / instance — kitu halisi kilichoundwa kutoka class: asha = Mwanafunzi("Asha").
  • attribute — data ya object: asha.jina.
  • method — function iliyo ndani ya class: asha.salimia().
  • __init__ — method maalum inayoitwa yenyewe object linapoundwa. Kazi yake: kuweka attributes za mwanzo. (Si "constructor" kamili kwa maana ya kiufundi, lakini ni sawa kuiita hivyo darasani.)
  • self — object lenyewe. Ukiandika asha.salimia(), Python inafanya Mwanafunzi.salimia(asha) — kwa hiyo self ndani ya method ni asha. Ndiyo maana kila method ina self kama parameter ya kwanza, lakini hatuipitishi sisi tunapoita.

Jina self si keyword — ni convention tu, lakini tumia self daima.

Sema hivi kwa mwanafunzi

  • "Class ni ramani ya nyumba ambayo fundi anayo. Ramani moja inaweza kujenga nyumba 100. Kila nyumba (object) ina rangi yake, mmiliki wake, lakini zote zimejengwa kwa ramani moja."
  • "Fikiria fomu ya kujiunga na shule. Fomu tupu ni class. Kila fomu iliyojazwa — ya Asha, ya Juma — ni object. Sehemu za fomu (jina, umri) ni attributes."
  • "self ni neno 'mimi'. Asha akisema 'jina langu ni...' anazungumza kuhusu yeye. Juma akisema maneno yale yale, anazungumza kuhusu Juma. Method moja, lakini self inabadilika kulingana na nani anaiita."
  • "__init__ ni kama siku ya kuzaliwa: mtoto anapewa jina na tarehe ya kuzaliwa papo hapo."

Andika code hii live (share screen)

Hatua 1: Tatizo — kwa_nini_oop.py

python
asha = {"jina": "Asha", "alama": 88}
juma = {"jna": "Juma", "alama": 70}    # kosa la kuandika! Python haitusaidii

def salimia(mwanafunzi):
    print("Habari, mimi ni", mwanafunzi["jina"])

salimia(asha)
salimia(juma)   # KeyError: 'jina'

Sema: "Dict haijui mwanafunzi anatakiwa awe na nini. Tutatengeneza 'ramani'."

Hatua 2: Class tupu na attributes — class_ya_kwanza.py

python
class Mwanafunzi:
    pass

a = Mwanafunzi()
b = Mwanafunzi()
a.jina = "Asha"
b.jina = "Juma"
print(a.jina, b.jina)
print(type(a))
print(a is b)

Output:

text
Asha Juma
<class '__main__.Mwanafunzi'>
False

Sema: "Objects mbili tofauti kutoka ramani moja."

Hatua 3: __init__

python
class Mwanafunzi:
    def __init__(self, jina, alama):
        self.jina = jina
        self.alama = alama

asha = Mwanafunzi("Asha", 88)
juma = Mwanafunzi("Juma", 70)
print(asha.jina, asha.alama)
print(juma.jina, juma.alama)

Eleza kwa kuchora (au kwa maneno): Mwanafunzi("Asha", 88) → Python inaunda object tupu → inaita __init__(object_huo, "Asha", 88) → self.jina = "Asha" inaweka attribute kwenye object huo → object linarudishwa na kuwekwa kwenye asha.

Onyesha kosa: Mwanafunzi("Asha") → TypeError: Mwanafunzi.__init__() missing 1 required positional argument: 'alama'. "Sasa Python inatulinda!"

Hatua 4: Methods

python
class Mwanafunzi:
    def __init__(self, jina, alama=0):
        self.jina = jina
        self.alama = alama

    def salimia(self):
        print(f"Habari, mimi ni {self.jina}.")

    def ongeza_alama(self, kiasi):
        self.alama += kiasi

    def amefaulu(self):
        return self.alama >= 50


asha = Mwanafunzi("Asha", 45)
asha.salimia()
print(asha.amefaulu())
asha.ongeza_alama(10)
print(asha.alama, asha.amefaulu())

juma = Mwanafunzi("Juma")
juma.salimia()
print(juma.alama)

Output:

text
Habari, mimi ni Asha.
False
55 True
Habari, mimi ni Juma.
0

Onyesha Mwanafunzi.salimia(asha) inatoa matokeo yale yale kama asha.salimia() — "hii ndiyo siri ya self".

Hatua 5: Objects kwenye list

python
darasa = [Mwanafunzi("Asha", 88), Mwanafunzi("Juma", 42), Mwanafunzi("Neema", 67)]
for m in darasa:
    hali = "amefaulu" if m.amefaulu() else "hajafaulu"
    print(f"{m.jina}: {m.alama} - {hali}")

Maswali ya kumuuliza mwanafunzi

  1. Tofauti ya class na object? — Class ni ramani; object ni kitu halisi kilichoundwa nayo.
  2. __init__ inaitwa lini? — Yenyewe, kila object jipya linapoundwa.
  3. Kwenye asha.salimia(), self ni nani? — asha.
  4. Kwa nini self.jina = jina na si jina = jina tu? — jina = jina ni local variable inayopotea function ikiisha; self.jina inahifadhiwa kwenye object.
  5. Method ni nini? — Function iliyo ndani ya class inayofanya kazi na data ya object.
  6. Kwa nini asha.alama na juma.alama hazichanganyiki? — Kila object lina attributes zake.

Makosa ya kawaida

Kosa Ujumbe wa error Suluhisho
Kusahau self kwenye method TypeError: Mwanafunzi.salimia() takes 0 positional arguments but 1 was given def salimia(self):
Kusahau self. ndani ya method NameError: name 'jina' is not defined self.jina
Kuandika __init au _init_ (underscore moja) TypeError: Mwanafunzi() takes no arguments Underscore mbili kila upande: __init__
Kuita method bila () Inachapisha <bound method Mwanafunzi.salimia of ...> asha.salimia()
Kupitisha self mwenyewe: asha.salimia(asha) TypeError: ... takes 1 positional argument but 2 were given Usipitishe self; Python inafanya.
Indentation: method nje ya class NameError au method haipo: AttributeError: 'Mwanafunzi' object has no attribute 'salimia' Methods ziwe ndani (indented) ya class.

Zoezi darasani

Zoezi 1.1: Tengeneza class Gari yenye attributes aina, rangi, kasi (default 0). Methods: ongeza_kasi(kiasi), punguza_kasi(kiasi) (kasi isishuke chini ya 0), taarifa() inayochapisha hali ya gari.

Zoezi 1.2: Tengeneza class Mstatili (rectangle) yenye urefu na upana, na methods eneo() na mzunguko(). Tengeneza mistatili 3 kwenye list, chapisha eneo la kila mmoja na lile kubwa zaidi.

Zoezi 1.3: Class Kikapu (shopping basket): __init__ inaanza na list tupu self.vitu. Methods: ongeza(jina, bei), jumla(), onyesha().

Jibu
python
# 1.1
class Gari:
    def __init__(self, aina, rangi, kasi=0):
        self.aina = aina
        self.rangi = rangi
        self.kasi = kasi

    def ongeza_kasi(self, kiasi):
        self.kasi += kiasi

    def punguza_kasi(self, kiasi):
        self.kasi = max(0, self.kasi - kiasi)

    def taarifa(self):
        print(f"{self.aina} ya rangi {self.rangi} inaenda {self.kasi} km/h")

g = Gari("Toyota IST", "nyeupe")
g.ongeza_kasi(60)
g.punguza_kasi(100)
g.taarifa()   # Toyota IST ya rangi nyeupe inaenda 0 km/h

# 1.2
class Mstatili:
    def __init__(self, urefu, upana):
        self.urefu = urefu
        self.upana = upana

    def eneo(self):
        return self.urefu * self.upana

    def mzunguko(self):
        return 2 * (self.urefu + self.upana)

mistatili = [Mstatili(2, 3), Mstatili(5, 4), Mstatili(1, 10)]
kubwa = mistatili[0]
for m in mistatili:
    print(f"{m.urefu}x{m.upana}: eneo={m.eneo()}, mzunguko={m.mzunguko()}")
    if m.eneo() > kubwa.eneo():
        kubwa = m
print("Kubwa zaidi:", kubwa.urefu, "x", kubwa.upana)   # 5 x 4

# 1.3
class Kikapu:
    def __init__(self):
        self.vitu = []

    def ongeza(self, jina, bei):
        self.vitu.append((jina, bei))

    def jumla(self):
        total = 0
        for _, bei in self.vitu:
            total += bei
        return total

    def onyesha(self):
        for jina, bei in self.vitu:
            print(f"{jina:<10}{bei:>8,}")
        print(f"{'JUMLA':<10}{self.jumla():>8,}")

k = Kikapu()
k.ongeza("Sukari", 3000)
k.ongeza("Mafuta", 7500)
k.onyesha()

Kipindi 2 — Class vs Instance Attributes, Encapsulation na @property

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (class Simu)
  2. Class attributes vs instance attributes
  3. Encapsulation: _salio convention, kwa nini tulinde data
  4. Mapumziko
  5. @property na setter yenye validation
  6. Zoezi darasani
  7. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

Instance attribute — inawekwa kwa self.x = ... (kawaida ndani ya __init__), na ni ya object mmoja tu. Class attribute — inawekwa moja kwa moja ndani ya class (nje ya method) na inashirikiwa na objects zote:

python
class Mwanafunzi:
    shule = "Python School"    # class attribute
    idadi = 0                  # class attribute
    def __init__(self, jina):
        self.jina = jina       # instance attribute
        Mwanafunzi.idadi += 1

Python inapotafuta obj.x: inaangalia kwanza object, kisha class. Mtego: asha.shule = "Nyingine" HAIBADILISHI class attribute — inaunda instance attribute mpya kwa asha tu inayoficha ya class. Ili kubadilisha kwa wote: Mwanafunzi.shule = "...". Kwa sababu hiyo, kwenye __init__ tunaandika Mwanafunzi.idadi += 1, si self.idadi += 1.

Mtego mkubwa zaidi: usiweke list/dict kama class attribute kama unataka kila object awe na yake — wote watashiriki list moja!

Encapsulation = kuficha maelezo ya ndani na kudhibiti jinsi data inavyobadilishwa. Mfano: salio la benki lisiwe hasi; mtu asiandike akaunti.salio = -1000000.

Python haina private kamili kama Java. Ina conventions:

  • _salio (underscore moja) — "hii ni ya ndani, usiiguse kutoka nje". Bado inawezekana, lakini ni makubaliano kati ya waandishi wa code.
  • __salio (underscore mbili, bila mbili mwishoni) — "name mangling": Python inaibadilisha jina kuwa _AkauntiYaBenki__salio, kwa hiyo obj.__salio inaleta AttributeError. Hutumiwa mara chache; tumia _ moja kwa kozi hii. Taja tu.

@property inaturuhusu kuandika method lakini kuitumia kama attribute (bila ()):

python
@property
def salio(self):          # getter: akaunti.salio
    return self._salio

Na setter inaturuhusu kuweka validation pale mtu anapoandika obj.x = thamani:

python
@salio.setter
def salio(self, thamani):
    if thamani < 0:
        raise ValueError(...)
    self._salio = thamani

Bila setter, property ni read-only: obj.salio = 5 → AttributeError: property 'salio' of 'Akaunti' object has no setter.

Property pia nzuri kwa thamani zinazokokotolewa (computed), mf. eneo ya mstatili: m.eneo badala ya m.eneo().

@property ni "decorator" — tutajifunza decorators kikamilifu Wiki 8. Kwa sasa: "ni alama inayobadilisha tabia ya function."

Sema hivi kwa mwanafunzi

  • "Class attribute ni kama jina la shule kwenye sare — wanafunzi wote wanalo. Instance attribute ni jina la mwanafunzi — kila mmoja lake."
  • "Encapsulation ni kama ATM. Huwezi kufungua mashine na kuchukua pesa mwenyewe; unatumia vitufe (methods) ambavyo vinaangalia PIN na salio kwanza."
  • "_salio ni kama bango 'Wafanyakazi tu' mlangoni. Mlango haujafungwa kwa kufuli, lakini watu wema wanaheshimu."
  • "@property ni kama mhudumu wa benki: unauliza 'salio langu ni ngapi?' naye anaangalia daftari la ndani na kukujibu. Ukitaka kubadilisha, lazima apitie sheria."

Andika code hii live (share screen)

Hatua 1: Class attributes — class_attr.py

python
class Mwanafunzi:
    shule = "Python School"
    idadi = 0

    def __init__(self, jina):
        self.jina = jina
        Mwanafunzi.idadi += 1


a = Mwanafunzi("Asha")
b = Mwanafunzi("Juma")
print(a.shule, b.shule, Mwanafunzi.shule)
print("Idadi:", Mwanafunzi.idadi)

Mwanafunzi.shule = "Python Academy"
print(a.shule, b.shule)

a.shule = "Shule Binafsi"     # mtego!
print(a.shule, b.shule, Mwanafunzi.shule)
print(a.__dict__)
print(b.__dict__)

Output:

text
Python School Python School Python School
Idadi: 2
Python Academy Python Academy
Shule Binafsi Python Academy Python Academy
{'jina': 'Asha', 'shule': 'Shule Binafsi'}
{'jina': 'Juma'}

Eleza __dict__ — "inaonyesha attributes zilizo ndani ya object mwenyewe."

Hatua 2: Mtego wa list kama class attribute

python
class Kikapu:
    vitu = []                 # MBAYA: list moja kwa wote

    def ongeza(self, kitu):
        self.vitu.append(kitu)

k1 = Kikapu()
k2 = Kikapu()
k1.ongeza("embe")
print(k2.vitu)    # ['embe']  -- k2 hakununua chochote!

Rekebisha kwa kuweka self.vitu = [] ndani ya __init__.

Hatua 3: Tatizo bila encapsulation — akaunti.py

python
class Akaunti:
    def __init__(self, mmiliki, salio=0):
        self.mmiliki = mmiliki
        self.salio = salio

a = Akaunti("Asha", 5000)
a.salio = -1_000_000     # hakuna anayezuia!
print(a.salio)

Hatua 4: _salio + methods + @property

python
class Akaunti:
    def __init__(self, mmiliki, salio=0):
        self.mmiliki = mmiliki
        self._salio = 0
        self.weka(salio)            # tumia validation hata mwanzoni

    @property
    def salio(self):
        return self._salio

    def weka(self, kiasi):
        if kiasi < 0:
            raise ValueError("Kiasi cha kuweka hakiwezi kuwa hasi.")
        self._salio += kiasi

    def toa(self, kiasi):
        if kiasi <= 0:
            raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.")
        if kiasi > self._salio:
            raise ValueError(f"Salio halitoshi (salio: {self._salio}).")
        self._salio -= kiasi


a = Akaunti("Asha", 5000)
a.weka(2000)
a.toa(1500)
print(a.salio)          # 5500  -- bila ()

try:
    a.toa(100_000)
except ValueError as e:
    print("Kosa:", e)

try:
    a.salio = 999_999
except AttributeError as e:
    print("Kosa:", e)

Output:

text
5500
Kosa: Salio halitoshi (salio: 5500).
Kosa: property 'salio' of 'Akaunti' object has no setter

Hatua 5: Setter yenye validation — mtu.py

python
class Mtu:
    def __init__(self, jina, umri):
        self.jina = jina
        self.umri = umri          # hii inaita setter hapa chini!

    @property
    def umri(self):
        return self._umri

    @umri.setter
    def umri(self, thamani):
        if not isinstance(thamani, int) or not 0 <= thamani <= 130:
            raise ValueError(f"Umri si sahihi: {thamani!r}")
        self._umri = thamani

    @property
    def ni_mtu_mzima(self):       # computed, read-only
        return self.umri >= 18


m = Mtu("Juma", 17)
print(m.umri, m.ni_mtu_mzima)
m.umri = 18
print(m.umri, m.ni_mtu_mzima)
try:
    m.umri = -3
except ValueError as e:
    print(e)
try:
    Mtu("Neema", "kumi")
except ValueError as e:
    print(e)

Output:

text
17 False
18 True
Umri si sahihi: -3
Umri si sahihi: 'kumi'

Eleza !r — inaonyesha thamani kwa repr, ndiyo maana 'kumi' ina quotes.

Maswali ya kumuuliza mwanafunzi

  1. Class attribute inaandikwa wapi? — Ndani ya class, nje ya methods.
  2. Ukiandika a.shule = "X", je b.shule inabadilika? — Hapana; inaundwa instance attribute ya a tu.
  3. Kwa nini Mwanafunzi.idadi += 1 na si self.idadi += 1? — self.idadi += 1 ingeunda instance attribute kwa kila object, na hesabu ya class isingeongezeka.
  4. _salio inamaanisha nini? — Ni ya ndani; usiiguse moja kwa moja kutoka nje (convention).
  5. Faida ya @property? — Tunasoma kama attribute lakini tunadhibiti (read-only, au validation kwenye setter).
  6. Kwa nini setter isiandike self.umri = thamani? — Ingeita setter tena na tena → RecursionError. Tumia self._umri.

Makosa ya kawaida

Kosa Ujumbe wa error Suluhisho
Setter inajiandikia yenyewe self.umri = thamani RecursionError: maximum recursion depth exceeded self._umri = thamani
Kuita property kama method a.salio() TypeError: 'int' object is not callable a.salio bila ()
Kuweka thamani kwenye property isiyo na setter AttributeError: property 'salio' of 'Akaunti' object has no setter Tumia method (weka) au ongeza setter.
Jina la setter tofauti na la property AttributeError au property inapotea Wote wawe na jina moja: @umri.setter def umri(...)
List kama class attribute Objects zote zinashiriki data self.vitu = [] ndani ya __init__
Kutumia __salio na kujaribu kusoma nje AttributeError: 'Akaunti' object has no attribute '__salio' Tumia _salio moja + property.

Zoezi darasani

Zoezi 2.1: Class Bidhaa yenye class attribute VAT = 0.18 na instance attributes jina, bei. Property bei_na_vat (read-only). Kisha badilisha Bidhaa.VAT = 0.16 uone bidhaa zote zinabadilika.

Zoezi 2.2: Class Mstatili yenye properties urefu na upana (setter zisiruhusu ≤ 0 → ValueError), na property ya computed eneo.

Zoezi 2.3: Class Mwanafunzi inayohesabu ni wanafunzi wangapi wameundwa (idadi) na kuwa na property alama (0–100 tu).

Jibu
python
# 2.1
class Bidhaa:
    VAT = 0.18

    def __init__(self, jina, bei):
        self.jina = jina
        self.bei = bei

    @property
    def bei_na_vat(self):
        return round(self.bei * (1 + Bidhaa.VAT))

s = Bidhaa("Sukari", 3000)
m = Bidhaa("Mchele", 2500)
print(s.bei_na_vat, m.bei_na_vat)   # 3540 2950
Bidhaa.VAT = 0.16
print(s.bei_na_vat, m.bei_na_vat)   # 3480 2900

# 2.2
class Mstatili:
    def __init__(self, urefu, upana):
        self.urefu = urefu
        self.upana = upana

    @property
    def urefu(self):
        return self._urefu

    @urefu.setter
    def urefu(self, v):
        if v <= 0:
            raise ValueError("Urefu lazima uwe > 0")
        self._urefu = v

    @property
    def upana(self):
        return self._upana

    @upana.setter
    def upana(self, v):
        if v <= 0:
            raise ValueError("Upana lazima uwe > 0")
        self._upana = v

    @property
    def eneo(self):
        return self.urefu * self.upana

r = Mstatili(3, 4)
print(r.eneo)      # 12
r.upana = 10
print(r.eneo)      # 30
try:
    r.urefu = 0
except ValueError as e:
    print(e)       # Urefu lazima uwe > 0

# 2.3
class Mwanafunzi:
    idadi = 0

    def __init__(self, jina, alama=0):
        self.jina = jina
        self.alama = alama
        Mwanafunzi.idadi += 1

    @property
    def alama(self):
        return self._alama

    @alama.setter
    def alama(self, v):
        if not 0 <= v <= 100:
            raise ValueError("Alama ni 0 hadi 100")
        self._alama = v

Mwanafunzi("Asha", 80)
Mwanafunzi("Juma")
print(Mwanafunzi.idadi)   # 2

Kipindi 3 — __str__, __repr__ na Mini Project: BankAccount na Duka

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (class Joto)
  2. __str__ na __repr__
  3. Mini project A: BankAccount kamili (historia ya miamala) — live
  4. Mapumziko
  5. Mini project B: Duka (inventory) — classes mbili zinazoshirikiana, mwanafunzi anaandika, wewe unaongoza
  6. Muhtasari wa wiki — maneno 7 ya OOP
  7. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

Ukichapisha object bila maandalizi: print(a) → <__main__.Akaunti object at 0x0000021F...> — haina maana kwa mtumiaji. Tunaweza kuifundisha Python namna ya kuonyesha object kwa dunder methods ("double underscore", mf. __init__). Wiki 7 tutaona dunders zaidi.

  • __str__(self) — maandishi "mazuri" kwa mtumiaji. Yanatumiwa na print(obj), str(obj), f-string f"{obj}".
  • __repr__(self) — maandishi ya "kiufundi" kwa developer, ikiwezekana yaonekane kama code inayoweza kuunda object hilo tena: Akaunti('Asha', 5000). Yanatumiwa na REPL, debugger, na ndani ya list: print([a, b]) inatumia __repr__ ya kila kitu!
  • Kama una __repr__ tu, print itaitumia pia. Kwa hiyo kama utaandika moja tu, andika __repr__.
  • Zote mbili lazima zirudishe (return) string, si print.

Mini projects: lengo ni kuunganisha kila kitu cha wiki — __init__, methods, _private, @property, exceptions (Wiki 5), __str__, na objects ndani ya objects (Duka lina Bidhaa nyingi kwenye dict). Kumbuka: Duka ina bidhaa — hii inaitwa composition ("has-a"). Wiki ijayo tutaona inheritance ("is-a").

Sema hivi kwa mwanafunzi

  • "__str__ ni kadi ya biashara unayompa mteja — nzuri na fupi. __repr__ ni kitambulisho cha taifa (NIDA) — taarifa kamili kwa ofisi."
  • "Duka lina bidhaa nyingi — kama kabati lenye mitungi. Kabati ni object moja (Duka), kila mtungi ni object (Bidhaa)."

Andika code hii live (share screen)

Hatua 1: Bila na kwa __str__/__repr__ — onyesha.py

python
class Bidhaa:
    def __init__(self, jina, bei):
        self.jina = jina
        self.bei = bei

b = Bidhaa("Sukari", 3000)
print(b)        # <__main__.Bidhaa object at 0x...>

Ongeza:

python
class Bidhaa:
    def __init__(self, jina, bei):
        self.jina = jina
        self.bei = bei

    def __str__(self):
        return f"{self.jina} - TSh {self.bei:,}"

    def __repr__(self):
        return f"Bidhaa({self.jina!r}, {self.bei!r})"


b = Bidhaa("Sukari", 3000)
print(b)
print(str(b))
print(repr(b))
print(f"Nimenunua {b}")
print([b, Bidhaa("Chumvi", 800)])

Output:

text
Sukari - TSh 3,000
Sukari - TSh 3,000
Bidhaa('Sukari', 3000)
Nimenunua Sukari - TSh 3,000
[Bidhaa('Sukari', 3000), Bidhaa('Chumvi', 800)]

Sisitiza mstari wa mwisho: list inatumia __repr__.

Hatua 2: Mini project A — BankAccount — bank_account.py

python
from datetime import datetime


class SalioHalitoshi(Exception):
    pass


class BankAccount:
    benki = "Benki ya Python"
    _namba_inayofuata = 1001

    def __init__(self, mmiliki, salio_la_mwanzo=0):
        self.mmiliki = mmiliki
        self.namba = BankAccount._namba_inayofuata
        BankAccount._namba_inayofuata += 1
        self._salio = 0
        self._miamala = []
        if salio_la_mwanzo > 0:
            self.weka(salio_la_mwanzo)

    @property
    def salio(self):
        return self._salio

    def _rekodi(self, aina, kiasi):
        wakati = datetime.now().strftime("%Y-%m-%d %H:%M")
        self._miamala.append((wakati, aina, kiasi, self._salio))

    def weka(self, kiasi):
        if kiasi <= 0:
            raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.")
        self._salio += kiasi
        self._rekodi("WEKA", kiasi)

    def toa(self, kiasi):
        if kiasi <= 0:
            raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.")
        if kiasi > self._salio:
            raise SalioHalitoshi(
                f"Unataka {kiasi:,} lakini salio ni {self._salio:,}."
            )
        self._salio -= kiasi
        self._rekodi("TOA", kiasi)

    def hamisha(self, akaunti_nyingine, kiasi):
        self.toa(kiasi)                    # kama inashindwa, haiendelei
        akaunti_nyingine.weka(kiasi)

    def taarifa(self):
        print(f"--- Taarifa ya {self.mmiliki} ({self.namba}) ---")
        for wakati, aina, kiasi, salio in self._miamala:
            print(f"{wakati}  {aina:<5}{kiasi:>10,}  salio: {salio:>10,}")

    def __str__(self):
        return f"{self.mmiliki} ({self.namba}): TSh {self._salio:,}"

    def __repr__(self):
        return f"BankAccount({self.mmiliki!r}, {self._salio!r})"


if __name__ == "__main__":
    asha = BankAccount("Asha", 50_000)
    juma = BankAccount("Juma")
    asha.toa(10_000)
    asha.hamisha(juma, 15_000)
    print(asha)
    print(juma)

    try:
        juma.toa(1_000_000)
    except SalioHalitoshi as e:
        print("Imeshindikana:", e)

    asha.taarifa()

Output (tarehe itatofautiana):

text
Asha (1001): TSh 25,000
Juma (1002): TSh 15,000
Imeshindikana: Unataka 1,000,000 lakini salio ni 15,000.
--- Taarifa ya Asha (1001) ---
2026-12-11 10:15  WEKA     50,000  salio:     50,000
2026-12-11 10:15  TOA      10,000  salio:     40,000
2026-12-11 10:15  TOA      15,000  salio:     25,000

Eleza if __name__ == "__main__": (marudio ya Wiki 4 — modules): code hii inaendeshwa tu tukiendesha file hili moja kwa moja, si tukilifanya import.

Hatua 3: Mini project B — Duka. Mwambie mwanafunzi aandike, wewe unaelekeza hatua kwa hatua. Mpe muundo kwanza (andika kama comments):

python
# Bidhaa: jina, bei, idadi; property thamani (bei*idadi); __str__
# Duka: jina, dict ya bidhaa (jina -> Bidhaa)
#   ongeza_bidhaa(bidhaa), uza(jina, idadi) -> inarudisha bei ya mauzo
#   ongeza_stock(jina, idadi), bidhaa_zinazoisha(kikomo=5), thamani_ya_stock()
#   mapato (property)

Suluhisho (duka.py):

python
class BidhaaHaipo(Exception):
    pass


class StockHaitoshi(Exception):
    pass


class Bidhaa:
    def __init__(self, jina, bei, idadi=0):
        if bei <= 0:
            raise ValueError("Bei lazima iwe zaidi ya sifuri.")
        self.jina = jina
        self.bei = bei
        self.idadi = idadi

    @property
    def thamani(self):
        return self.bei * self.idadi

    def __str__(self):
        return f"{self.jina:<10} bei {self.bei:>7,}  idadi {self.idadi:>4}"

    def __repr__(self):
        return f"Bidhaa({self.jina!r}, {self.bei!r}, {self.idadi!r})"


class Duka:
    def __init__(self, jina):
        self.jina = jina
        self._bidhaa = {}
        self._mapato = 0

    @property
    def mapato(self):
        return self._mapato

    def ongeza_bidhaa(self, bidhaa):
        self._bidhaa[bidhaa.jina.lower()] = bidhaa

    def _tafuta(self, jina):
        try:
            return self._bidhaa[jina.lower()]
        except KeyError:
            raise BidhaaHaipo(f"'{jina}' haipo dukani.") from None

    def ongeza_stock(self, jina, idadi):
        self._tafuta(jina).idadi += idadi

    def uza(self, jina, idadi):
        b = self._tafuta(jina)
        if idadi > b.idadi:
            raise StockHaitoshi(f"Zimebaki {b.idadi} tu za {b.jina}.")
        b.idadi -= idadi
        jumla = b.bei * idadi
        self._mapato += jumla
        return jumla

    def bidhaa_zinazoisha(self, kikomo=5):
        return [b for b in self._bidhaa.values() if b.idadi <= kikomo]

    def thamani_ya_stock(self):
        return sum(b.thamani for b in self._bidhaa.values())

    def __str__(self):
        mistari = [f"=== {self.jina} ==="]
        for b in self._bidhaa.values():
            mistari.append(str(b))
        mistari.append(f"Thamani ya stock: TSh {self.thamani_ya_stock():,}")
        mistari.append(f"Mapato: TSh {self._mapato:,}")
        return "\n".join(mistari)


if __name__ == "__main__":
    duka = Duka("Duka la Mama Neema")
    duka.ongeza_bidhaa(Bidhaa("Sukari", 3000, 20))
    duka.ongeza_bidhaa(Bidhaa("Mchele", 2500, 6))
    duka.ongeza_bidhaa(Bidhaa("Sabuni", 1200, 3))

    print("Mauzo:", duka.uza("sukari", 5))
    for jina, n in [("Mchele", 10), ("Unga", 1)]:
        try:
            duka.uza(jina, n)
        except (StockHaitoshi, BidhaaHaipo) as e:
            print("Kosa:", e)

    print(duka)
    print("Zinaisha:", duka.bidhaa_zinazoisha())

Output:

text
Mauzo: 15000
Kosa: Zimebaki 6 tu za Mchele.
Kosa: 'Unga' haipo dukani.
=== Duka la Mama Neema ===
Sukari     bei   3,000  idadi   15
Mchele     bei   2,500  idadi    6
Sabuni     bei   1,200  idadi    3
Thamani ya stock: TSh 63,600
Mapato: TSh 15,000
Zinaisha: [Bidhaa('Sabuni', 1200, 3)]

Maelezo kwa mwalimu: list comprehension ([b for b in ...]) na sum(... for ...) ni za Wiki 8. Kama mwanafunzi hajazizoea, mwonyeshe toleo la loop:

python
def bidhaa_zinazoisha(self, kikomo=5):
    matokeo = []
    for b in self._bidhaa.values():
        if b.idadi <= kikomo:
            matokeo.append(b)
    return matokeo

raise ... from None inaficha KeyError ya ndani ili traceback iwe safi — sema tu "ni usafi".

Kwa nini "Mchele" (6) haiko kwenye "Zinaisha"? Kwa sababu kikomo ni 5 na 6 <= 5 ni False — uliza hili kama swali la kufikiri.

Maswali ya kumuuliza mwanafunzi

  1. print(obj) inatumia nini? — __str__ (au __repr__ kama __str__ haipo).
  2. print([obj1, obj2]) inatumia nini? — __repr__ ya kila object.
  3. __str__ iwe na print ndani au return? — return string.
  4. Kwenye Duka, kwa nini _bidhaa ni dict na si list? — Kutafuta kwa jina ni haraka na rahisi.
  5. Kwa nini hamisha inaita self.toa kwanza? — Kama salio halitoshi, exception inarushwa na weka haiitwi; pesa haziundwi kutoka hewani.
  6. Duka "lina" bidhaa — hii inaitwaje? — Composition (has-a).

Makosa ya kawaida

Kosa Ujumbe wa error Suluhisho
__str__ inafanya print badala ya return TypeError: __str__ returned non-string (type NoneType) return f"..."
__str__ inarudisha namba TypeError: __str__ returned non-string (type int) Rudisha string.
Kusahau kurudisha thamani kwenye uza print(duka.uza(...)) inaonyesha None return jumla
Kuongeza object badala ya kutafuta kwa jina: duka._bidhaa[bidhaa] KeyError baadaye Tumia bidhaa.jina kama key.
Kusahau self._mapato = 0 kwenye __init__ AttributeError: 'Duka' object has no attribute '_mapato' Anzisha attributes zote kwenye __init__.

Zoezi darasani

Zoezi 3.1: Ongeza __str__ na __repr__ kwenye class Gari ya Kipindi 1.

Zoezi 3.2: Ongeza kwenye Duka method ripoti_ya_mauzo() — duka liweke rekodi ya kila mauzo (jina, idadi, jumla) kwenye list na method ichapishe zote.

Zoezi 3.3: Ongeza kwenye BankAccount kikomo cha kutoa kwa siku (class attribute KIKOMO_CHA_SIKU = 200_000). Mtu akijaribu kutoa jumla zaidi ya kikomo, rusha ValueError. (Kwa urahisi, hesabu jumla ya "TOA" zote kwenye _miamala za leo.)

Jibu
python
# 3.1
class Gari:
    def __init__(self, aina, rangi, kasi=0):
        self.aina = aina
        self.rangi = rangi
        self.kasi = kasi

    def __str__(self):
        return f"{self.aina} ({self.rangi}) - {self.kasi} km/h"

    def __repr__(self):
        return f"Gari({self.aina!r}, {self.rangi!r}, {self.kasi!r})"

# 3.2 -- mabadiliko kwenye Duka
#   __init__:  self._mauzo = []
#   uza():     baada ya kuhesabu jumla:
#              self._mauzo.append((b.jina, idadi, jumla))
def ripoti_ya_mauzo(self):
    print("--- Mauzo ---")
    for jina, idadi, jumla in self._mauzo:
        print(f"{jina:<10}{idadi:>4} x  = {jumla:>8,}")
    print(f"Jumla ya mapato: {self._mapato:,}")

# 3.3 -- mabadiliko kwenye BankAccount
from datetime import datetime

class BankAccount:
    KIKOMO_CHA_SIKU = 200_000
    # ... (sehemu nyingine kama zilivyo)

    def _iliyotolewa_leo(self):
        leo = datetime.now().strftime("%Y-%m-%d")
        jumla = 0
        for wakati, aina, kiasi, _ in self._miamala:
            if aina == "TOA" and wakati.startswith(leo):
                jumla += kiasi
        return jumla

    def toa(self, kiasi):
        if kiasi <= 0:
            raise ValueError("Kiasi lazima kiwe zaidi ya sifuri.")
        if self._iliyotolewa_leo() + kiasi > BankAccount.KIKOMO_CHA_SIKU:
            raise ValueError("Umezidi kikomo cha kutoa kwa siku.")
        if kiasi > self._salio:
            raise SalioHalitoshi(f"Salio ni {self._salio:,}.")
        self._salio -= kiasi
        self._rekodi("TOA", kiasi)

Kazi ya Nyumbani

(Inatakiwa: kipindi kijacho)

Kazi 1 (baada ya Kipindi 1): Class Simu yenye chapa, modeli, chaji (default 100). Methods: piga_simu(dakika) — kila dakika inapunguza chaji 2% (chaji isishuke chini ya 0; kama haitoshi, chapisha "Chaji haitoshi"), chaji_simu(asilimia) (isizidi 100), hali().

Kazi 2 (baada ya Kipindi 2): Class Joto (temperature) inayohifadhi _celsius. Property celsius yenye setter (isikubali chini ya -273.15), na property fahrenheit yenye getter na setter (F = C × 9/5 + 32).

Kazi 3 (baada ya Kipindi 3): Hifadhi Duka kwenye JSON! Ongeza method hifadhi(path) kwenye Duka, na function ya kawaida (nje ya class) pakia_duka(path) inayorudisha Duka jipya kutoka JSON. Ongeza __repr__ kwenye Duka. Programu iwe na menu ndogo: ongeza bidhaa, uza, onyesha, toka (ikihifadhi kabla ya kutoka).

Jibu
python
# Kazi 1
class Simu:
    def __init__(self, chapa, modeli, chaji=100):
        self.chapa = chapa
        self.modeli = modeli
        self.chaji = chaji

    def piga_simu(self, dakika):
        inahitajika = dakika * 2
        if inahitajika > self.chaji:
            print("Chaji haitoshi")
            return
        self.chaji -= inahitajika
        print(f"Umeongea dakika {dakika}.")

    def chaji_simu(self, asilimia):
        self.chaji = min(100, self.chaji + asilimia)

    def hali(self):
        print(f"{self.chapa} {self.modeli}: chaji {self.chaji}%")

s = Simu("Tecno", "Spark 20")
s.piga_simu(30)    # chaji 40
s.piga_simu(25)    # Chaji haitoshi
s.chaji_simu(80)   # 100
s.hali()
python
# Kazi 2
class Joto:
    def __init__(self, celsius=0):
        self.celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, v):
        if v < -273.15:
            raise ValueError("Chini ya absolute zero haiwezekani.")
        self._celsius = v

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

    @fahrenheit.setter
    def fahrenheit(self, f):
        self.celsius = (f - 32) * 5 / 9      # inapita validation ya celsius

j = Joto(25)
print(j.fahrenheit)   # 77.0
j.fahrenheit = 212
print(j.celsius)      # 100.0
python
# Kazi 3 -- weka chini ya classes za duka.py (Bidhaa, Duka, exceptions)
import json
from pathlib import Path

# Ndani ya class Duka ongeza:
#     def __repr__(self):
#         return f"Duka({self.jina!r}, bidhaa={len(self._bidhaa)})"
#
#     def hifadhi(self, path):
#         data = {
#             "jina": self.jina,
#             "mapato": self._mapato,
#             "bidhaa": [
#                 {"jina": b.jina, "bei": b.bei, "idadi": b.idadi}
#                 for b in self._bidhaa.values()
#             ],
#         }
#         with open(path, "w", encoding="utf-8") as f:
#             json.dump(data, f, indent=2, ensure_ascii=False)

def pakia_duka(path):
    with open(path, encoding="utf-8") as f:
        data = json.load(f)
    duka = Duka(data["jina"])
    duka._mapato = data.get("mapato", 0)
    for b in data["bidhaa"]:
        duka.ongeza_bidhaa(Bidhaa(b["jina"], b["bei"], b["idadi"]))
    return duka

FAILI = Path(__file__).parent / "duka.json"
try:
    duka = pakia_duka(FAILI)
except FileNotFoundError:
    duka = Duka("Duka Langu")

while True:
    chaguo = input("\n1.Ongeza  2.Uza  3.Onyesha  4.Toka : ")
    try:
        if chaguo == "1":
            jina = input("Jina: ")
            bei = int(input("Bei: "))
            idadi = int(input("Idadi: "))
            duka.ongeza_bidhaa(Bidhaa(jina, bei, idadi))
        elif chaguo == "2":
            jina = input("Jina: ")
            idadi = int(input("Idadi: "))
            print("Jumla:", duka.uza(jina, idadi))
        elif chaguo == "3":
            print(duka)
        elif chaguo == "4":
            duka.hifadhi(FAILI)
            print("Imehifadhiwa. Kwaheri!")
            break
    except (ValueError, BidhaaHaipo, StockHaitoshi) as e:
        print("Kosa:", e)

Kumbuka: @classmethod (njia "rasmi" ya kuunda object kutoka JSON) itatajwa Wiki 7/8 kwenye mradi wa maktaba; kwa sasa function ya kawaida inatosha.

Ukiulizwa swali usilolijua

Checklist ya mwisho wa wiki

  • Anaeleza class vs object kwa mfano wake mwenyewe.
  • Anaandika class yenye __init__ na methods bila kusahau self.
  • Anaeleza self ni nani kwenye obj.method().
  • Anatofautisha class attribute na instance attribute (na mtego wa obj.x = ...).
  • Anajua kwa nini list isiwe class attribute.
  • Anatumia _jina na @property (getter + setter yenye validation).
  • Anaandika __str__ na __repr__ zinazorudisha string.
  • Ameunda BankAccount inayorusha exception kwa salio lisilotosha.
  • Ameunda Duka lenye objects za Bidhaa (composition).
  • Kazi ya nyumbani ya Wiki 6 imekabidhiwa.