Yaliyomo

Wiki 7: OOP Sehemu ya 2 — Inheritance, Polymorphism, Dataclasses na Type Hints

Mwezi 2 | Lengo kuu: Mwanafunzi atumie inheritance na super(), aelewe polymorphism na abstract classes, aandike dunder methods (__len__, __eq__, __add__), na atumie @dataclass pamoja na type hints.

Maandalizi ya Mwalimu (soma kabla ya wiki kuanza)

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

  1. Kuunda subclass inayorithi kutoka parent class, na kutumia super().__init__(...).
  2. Ku-override method na kuita toleo la parent kwa super().
  3. Kutumia isinstance() na issubclass().
  4. Kueleza polymorphism na kuandika code inayofanya kazi na objects za aina tofauti.
  5. Kutengeneza abstract class kwa ABC na @abstractmethod.
  6. Kuandika __len__, __eq__, __add__ (na kuelewa dunders zinavyounganishwa na operators).
  7. Kutumia @dataclass (pamoja na field(default_factory=list) na order=True).
  8. Kuandika type hints: int, str, list[int], dict[str, float], Optional[str], str | None.

Kipindi 1 — Inheritance, super() na Method Overriding

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani ya Wiki 6 (Simu, Joto, Duka JSON)
  2. Tatizo: code inayojirudia kwenye classes zinazofanana
  3. Inheritance ya kwanza: class Mbwa(Mnyama)
  4. super().__init__()
  5. Mapumziko
  6. Overriding + kuita method ya parent; isinstance, issubclass
  7. Zoezi darasani
  8. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

Inheritance (urithi) inamaanisha class mpya (child / subclass) inapata attributes na methods zote za class iliyopo (parent / base / superclass), kisha inaweza kuongeza mpya au kubadilisha zilizopo.

python
class Mnyama:            # parent
    ...
class Mbwa(Mnyama):      # child: "Mbwa ni Mnyama"
    ...

Uhusiano huu unaitwa "is-a" (Mbwa ni Mnyama). Linganisha na Wiki 6, composition "has-a" (Duka lina Bidhaa). Kanuni rahisi: kama sentensi "X ni aina ya Y" ina maana, tumia inheritance; vinginevyo composition.

Method overriding: child anaandika method yenye jina lile lile kama la parent → toleo la child linatumika kwa objects za child.

super() inarudisha "parent" ili tuite methods zake. Muhimu zaidi kwenye __init__: child akiwa na __init__ yake, ya parent haiitwi yenyewe — lazima tuite super().__init__(...), vinginevyo attributes za parent hazitawekwa.

Python inapotafuta obj.method: inaangalia object → class yake → parent → parent wa parent ... → object (mzazi wa classes zote). Mpangilio huu unaitwa MRO (Method Resolution Order): Mbwa.__mro__. Taja tu; multiple inheritance (class C(A, B)) ipo lakini hatuifundishi kwa kina.

  • isinstance(obj, Mnyama) → True kama obj ni Mnyama au subclass yake.
  • issubclass(Mbwa, Mnyama) → True.

Exceptions za Wiki 5 zilikuwa inheritance tayari! class SalioHalitoshi(Exception) — ndiyo maana except Exception inashika na SalioHalitoshi pia. Hii ni "aha moment" nzuri kwa mwanafunzi.

Sema hivi kwa mwanafunzi

  • "Inheritance ni kama urithi wa familia: mtoto anarithi rangi ya macho ya mzazi (attributes) na tabia (methods), lakini anaweza kuwa na kipaji chake mwenyewe (method mpya), au kufanya jambo kwa njia yake (override)."
  • "Gari, pikipiki na bajaji zote ni vyombo vya usafiri: zina namba ya usajili, zinaweza 'kuwasha'. Badala ya kuandika hivyo mara tatu, tunaandika Chombo mara moja."
  • "super() ni kama kumwambia mzazi: 'Baba, fanya sehemu yako kwanza (kujaza jina na umri), kisha mimi nitaongeza yangu.'"

Andika code hii live (share screen)

Hatua 1: Tatizo — bila_urithi.py

python
class Mbwa:
    def __init__(self, jina, umri):
        self.jina = jina
        self.umri = umri
    def kula(self):
        print(f"{self.jina} anakula.")
    def toa_sauti(self):
        print("Woof!")

class Paka:
    def __init__(self, jina, umri):
        self.jina = jina
        self.umri = umri
    def kula(self):
        print(f"{self.jina} anakula.")
    def toa_sauti(self):
        print("Meow!")

Uliza: "Unaona nini kinachojirudia?" (__init__ na kula.)

Hatua 2: Inheritance — urithi.py

python
class Mnyama:
    def __init__(self, jina, umri):
        self.jina = jina
        self.umri = umri

    def kula(self):
        print(f"{self.jina} anakula.")

    def toa_sauti(self):
        print("...")

    def __str__(self):
        return f"{type(self).__name__} {self.jina} ({self.umri} yrs)"


class Mbwa(Mnyama):
    def toa_sauti(self):           # override
        print("Woof!")


class Paka(Mnyama):
    def toa_sauti(self):
        print("Meow!")


m = Mbwa("Simba", 3)
p = Paka("Pusi", 2)
m.kula()           # imerithiwa
m.toa_sauti()      # ya Mbwa
p.toa_sauti()
print(m, p, sep=" | ")

Output:

text
Simba anakula.
Woof!
Meow!
Mbwa Simba (3 yrs) | Paka Pusi (2 yrs)

Eleza type(self).__name__ — jina la class halisi ya object.

Hatua 3: super().__init__

python
class Mbwa(Mnyama):
    def __init__(self, jina, umri, aina):
        super().__init__(jina, umri)   # parent anaweka jina, umri
        self.aina = aina               # child anaongeza

    def toa_sauti(self):
        print("Woof!")

    def leta_mpira(self):              # method mpya
        print(f"{self.jina} ameleta mpira!")


m = Mbwa("Simba", 3, "German Shepherd")
print(m.jina, m.aina)
m.leta_mpira()

Kisha onyesha kosa kwa makusudi: futa mstari wa super().__init__ na endesha → AttributeError: 'Mbwa' object has no attribute 'jina'. Rudisha.

Hatua 4: Override + kuita parent — wafanyakazi.py

python
class Mfanyakazi:
    def __init__(self, jina, mshahara):
        self.jina = jina
        self.mshahara = mshahara

    def malipo_ya_mwezi(self):
        return self.mshahara

    def maelezo(self):
        return f"{self.jina}: TSh {self.malipo_ya_mwezi():,}"


class Meneja(Mfanyakazi):
    def __init__(self, jina, mshahara, posho):
        super().__init__(jina, mshahara)
        self.posho = posho

    def malipo_ya_mwezi(self):
        return super().malipo_ya_mwezi() + self.posho

    def maelezo(self):
        return super().maelezo() + " (Meneja)"


wafanyakazi = [
    Mfanyakazi("Asha", 800_000),
    Meneja("Juma", 1_500_000, 300_000),
]
for w in wafanyakazi:
    print(w.maelezo())

print(isinstance(wafanyakazi[1], Mfanyakazi))   # True
print(isinstance(wafanyakazi[0], Meneja))       # False
print(issubclass(Meneja, Mfanyakazi))           # True
print(Meneja.__mro__)

Output:

text
Asha: TSh 800,000
Juma: TSh 1,800,000 (Meneja)
True
False
True
(<class '__main__.Meneja'>, <class '__main__.Mfanyakazi'>, <class 'object'>)

Sisitiza: maelezo() ya parent inaita self.malipo_ya_mwezi() — na kwa Juma, self ni Meneja, kwa hiyo toleo la Meneja linatumika (pamoja na posho). Hili ni muhimu sana na linaanzisha polymorphism.

Hatua 5: Exceptions ni inheritance

python
class KosaLaBenki(Exception):
    pass

class SalioHalitoshi(KosaLaBenki):
    pass

class AkauntiImefungwa(KosaLaBenki):
    pass

try:
    raise AkauntiImefungwa("Akaunti 1001 imefungwa.")
except KosaLaBenki as e:          # inashika subclasses zote
    print(type(e).__name__, "->", e)

Output: AkauntiImefungwa -> Akaunti 1001 imefungwa.

Maswali ya kumuuliza mwanafunzi

  1. Child class inapata nini kutoka parent? — Attributes na methods zote.
  2. Override ni nini? — Child kuandika method yenye jina lile lile ili kubadilisha tabia.
  3. Usipoita super().__init__() kwenye __init__ ya child, nini kinatokea? — Attributes za parent haziwekwi → AttributeError baadaye.
  4. isinstance(Mbwa(...), Mnyama) ni nini? — True.
  5. Tofauti ya "is-a" na "has-a"? — Inheritance (Mbwa ni Mnyama) vs composition (Duka lina Bidhaa).
  6. Kwa nini except KosaLaBenki inashika SalioHalitoshi? — Kwa sababu SalioHalitoshi ni subclass yake.

Makosa ya kawaida

Kosa Ujumbe wa error Suluhisho
Kusahau super().__init__ AttributeError: 'Mbwa' object has no attribute 'jina' Ongeza super().__init__(jina, umri)
Kupitisha self kwa super: super().__init__(self, jina, umri) TypeError: Mnyama.__init__() takes 3 positional arguments but 4 were given Usipitishe self
Kusahau () kwenye super: super.__init__(...) TypeError: descriptor '__init__' requires a 'super' object but received a 'str' super().__init__(...)
Kurithi kutoka object badala ya class: class Mbwa(Mnyama()) TypeError ya metaclass/arguments Weka jina la class bila ()
Kusahau return kwenye method iliyoovaride TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' return super().malipo_ya_mwezi() + ...

Zoezi darasani

Zoezi 1.1: Class Chombo (namba, kasi_ya_juu) yenye maelezo(). Subclasses Gari (ina viti) na Pikipiki (ina ina_helmet: bool). Kila moja ione maelezo() yake ikiita ya parent.

Zoezi 1.2: Class Akaunti (mmiliki, salio, toa, weka). Subclass AkauntiYaAkiba isiyoruhusu salio kushuka chini ya 10,000 (override toa), na yenye ongeza_riba(asilimia).

Zoezi 1.3: Kwenye wafanyakazi.py, ongeza Mkandarasi(Mfanyakazi) analipwa kwa saa (saa, kiwango_kwa_saa). Override malipo_ya_mwezi. Chapisha jumla ya malipo ya wote.

Jibu
python
# 1.1
class Chombo:
    def __init__(self, namba, kasi_ya_juu):
        self.namba = namba
        self.kasi_ya_juu = kasi_ya_juu

    def maelezo(self):
        return f"{self.namba} (kasi ya juu {self.kasi_ya_juu} km/h)"


class Gari(Chombo):
    def __init__(self, namba, kasi_ya_juu, viti):
        super().__init__(namba, kasi_ya_juu)
        self.viti = viti

    def maelezo(self):
        return "Gari " + super().maelezo() + f", viti {self.viti}"


class Pikipiki(Chombo):
    def __init__(self, namba, kasi_ya_juu, ina_helmet):
        super().__init__(namba, kasi_ya_juu)
        self.ina_helmet = ina_helmet

    def maelezo(self):
        helmet = "ina helmet" if self.ina_helmet else "haina helmet"
        return "Pikipiki " + super().maelezo() + f", {helmet}"


for c in [Gari("T 123 ABC", 180, 5), Pikipiki("MC 456 XYZ", 120, True)]:
    print(c.maelezo())

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

    def weka(self, kiasi):
        self.salio += kiasi

    def toa(self, kiasi):
        if kiasi > self.salio:
            raise ValueError("Salio halitoshi")
        self.salio -= kiasi


class AkauntiYaAkiba(Akaunti):
    KIWANGO_CHA_CHINI = 10_000

    def toa(self, kiasi):
        if self.salio - kiasi < self.KIWANGO_CHA_CHINI:
            raise ValueError(f"Lazima ubaki na angalau {self.KIWANGO_CHA_CHINI:,}")
        super().toa(kiasi)

    def ongeza_riba(self, asilimia):
        self.salio += self.salio * asilimia / 100


a = AkauntiYaAkiba("Neema", 50_000)
a.toa(30_000)
try:
    a.toa(15_000)
except ValueError as e:
    print(e)              # Lazima ubaki na angalau 10,000
a.ongeza_riba(5)
print(a.salio)            # 21000.0

# 1.3
class Mkandarasi(Mfanyakazi):
    def __init__(self, jina, saa, kiwango_kwa_saa):
        super().__init__(jina, 0)
        self.saa = saa
        self.kiwango_kwa_saa = kiwango_kwa_saa

    def malipo_ya_mwezi(self):
        return self.saa * self.kiwango_kwa_saa


wote = [Mfanyakazi("Asha", 800_000),
        Meneja("Juma", 1_500_000, 300_000),
        Mkandarasi("Baraka", 120, 5_000)]
jumla = 0
for w in wote:
    print(w.maelezo())
    jumla += w.malipo_ya_mwezi()
print(f"Jumla: TSh {jumla:,}")    # Jumla: TSh 3,200,000

Kipindi 2 — Polymorphism, Abstract Classes na Dunder Methods

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (maumbo ya kijiometri)
  2. Polymorphism + duck typing
  3. Abstract classes: ABC, @abstractmethod
  4. Mapumziko
  5. Dunder methods: __len__, __eq__, __add__ (+ __lt__, __contains__ kwa ufupi)
  6. Zoezi darasani
  7. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

Polymorphism ("maumbo mengi") — code moja inafanya kazi na objects za aina tofauti, kila moja ikijibu kwa njia yake. Mfano: for m in wanyama: m.toa_sauti() — hatujali ni Mbwa au Paka; kila mmoja anajua sauti yake. Tayari tuliona hili na malipo_ya_mwezi().

Duck typing: Python haihitaji objects ziwe na parent mmoja ili polymorphism ifanye kazi — inatosha ziwe na method yenye jina hilo. "Kama inatembea kama bata na inalia kama bata, basi ni bata." Hata len() inafanya kazi kwa str, list, dict — polymorphism ya built-in.

Abstract class — class ambayo haikusudiwi kuundiwa objects moja kwa moja; ni "mkataba" unaosema "kila subclass LAZIMA iwe na methods hizi."

python
from abc import ABC, abstractmethod

class Umbo(ABC):
    @abstractmethod
    def eneo(self): ...
  • Umbo() → TypeError: Can't instantiate abstract class Umbo without an implementation for abstract method 'eneo' (ujumbe wa Python 3.12+).
  • Subclass isiyoandika eneo → nayo haiwezi kuundwa. Faida: kosa linaonekana mapema (wakati wa kuunda object), si baadaye ndani ya programu.
  • Abstract class inaweza kuwa na methods za kawaida pia, zinazotumia zile abstract (mf. maelezo() inayoita self.eneo()).
  • ... (Ellipsis) au pass kwenye mwili wa abstract method — zote sawa.

Dunder methods (double underscore) ndizo zinazofanya operators na built-in functions zifanye kazi na objects zetu: | Unaandika | Python inaita | |-----------|---------------| | len(x) | x.__len__() — lazima irudishe int ≥ 0 | | x == y | x.__eq__(y) | | x + y | x.__add__(y) | | x < y | x.__lt__(y) (inahitajika kwa sorted()) | | item in x | x.__contains__(item) | | str(x), print(x) | x.__str__() |

Bila __eq__, == inalinganisha utambulisho (je ni object lile lile?), si maudhui. Pesa(100) == Pesa(100) → False mpaka tuandike __eq__.

Ndani ya __eq__/__add__, kama other si aina tunayoijua, rudisha NotImplemented (si raise) — Python itajaribu njia nyingine au itarusha TypeError yenyewe.

Kumbuka: ukiandika __eq__, Python inafanya object lisiwe "hashable" (huwezi kuliweka kwenye set au kama key ya dict) mpaka uandike __hash__. Usiingie kwa kina; taja tu kama mwanafunzi atauliza.

Sema hivi kwa mwanafunzi

  • "Polymorphism ni kama mwalimu kusema 'Simameni!' darasani. Kila mwanafunzi anasimama kwa njia yake — mmoja haraka, mwingine polepole — lakini amri ni moja."
  • "Abstract class ni kama tangazo la kazi: 'Mwombaji LAZIMA ajue kuendesha gari.' Tangazo lenyewe haliendeshi gari; ni sharti kwa kila atakayeajiriwa."
  • "Dunder methods ni mlango wa nyuma unaoiruhusu Python kutumia alama kama + na == kwa vitu vyetu. Unapoandika a + b, nyuma ya pazia Python inasema a.__add__(b)."

Andika code hii live (share screen)

Hatua 1: Polymorphism — polymorphism.py

python
class Mbwa:
    def toa_sauti(self):
        return "Woof!"

class Paka:
    def toa_sauti(self):
        return "Meow!"

class Ng'ombe:
    pass

Simama! Onyesha kosa: SyntaxError: unterminated string literal — apostrophe haiwezi kuwa kwenye jina. Badilisha kuwa Ngombe:

python
class Ngombe:
    def toa_sauti(self):
        return "Mooo!"

class Redio:                 # si mnyama kabisa!
    def toa_sauti(self):
        return "Habari za saa hii..."

for kitu in [Mbwa(), Paka(), Ngombe(), Redio()]:
    print(type(kitu).__name__, ":", kitu.toa_sauti())

Output:

text
Mbwa : Woof!
Paka : Meow!
Ngombe : Mooo!
Redio : Habari za saa hii...

Sema: "Redio haina uhusiano na wanyama, lakini kwa sababu ina toa_sauti, inafanya kazi. Hiyo ni duck typing."

Hatua 2: Abstract class — maumbo.py

python
from abc import ABC, abstractmethod
import math


class Umbo(ABC):
    @abstractmethod
    def eneo(self):
        ...

    @abstractmethod
    def mzunguko(self):
        ...

    def maelezo(self):                  # method ya kawaida
        return (f"{type(self).__name__}: eneo={self.eneo():.2f}, "
                f"mzunguko={self.mzunguko():.2f}")


class Mstatili(Umbo):
    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)


class Duara(Umbo):
    def __init__(self, nusu_kipenyo):
        self.r = nusu_kipenyo

    def eneo(self):
        return math.pi * self.r ** 2

    def mzunguko(self):
        return 2 * math.pi * self.r


maumbo = [Mstatili(3, 4), Duara(1)]
for u in maumbo:
    print(u.maelezo())

try:
    Umbo()
except TypeError as e:
    print("Kosa:", e)

Output:

text
Mstatili: eneo=12.00, mzunguko=14.00
Duara: eneo=3.14, mzunguko=6.28
Kosa: Can't instantiate abstract class Umbo without an implementation for abstract methods 'eneo', 'mzunguko'

Kisha onyesha: tengeneza class Pembetatu(Umbo) yenye eneo tu (bila mzunguko) na jaribu Pembetatu() → TypeError ... abstract method 'mzunguko'.

Hatua 3: Dunders — pesa.py

python
class Pesa:
    def __init__(self, kiasi, sarafu="TZS"):
        self.kiasi = kiasi
        self.sarafu = sarafu

    def __repr__(self):
        return f"Pesa({self.kiasi!r}, {self.sarafu!r})"

    def __str__(self):
        return f"{self.sarafu} {self.kiasi:,}"

    def __eq__(self, other):
        if not isinstance(other, Pesa):
            return NotImplemented
        return self.kiasi == other.kiasi and self.sarafu == other.sarafu

    def __add__(self, other):
        if not isinstance(other, Pesa):
            return NotImplemented
        if self.sarafu != other.sarafu:
            raise ValueError("Huwezi kuongeza sarafu tofauti.")
        return Pesa(self.kiasi + other.kiasi, self.sarafu)

    def __lt__(self, other):
        if not isinstance(other, Pesa):
            return NotImplemented
        return self.kiasi < other.kiasi


a = Pesa(5000)
b = Pesa(5000)
c = Pesa(2000)
print(a == b)        # True  (bila __eq__ ingekuwa False)
print(a is b)        # False (objects tofauti)
print(a + c)         # TZS 7,000
print(sorted([a, c, Pesa(100)]))
try:
    a + Pesa(10, "USD")
except ValueError as e:
    print(e)
try:
    a + 100
except TypeError as e:
    print("TypeError:", e)

Output:

text
True
False
TZS 7,000
[Pesa(100, 'TZS'), Pesa(2000, 'TZS'), Pesa(5000, 'TZS')]
Huwezi kuongeza sarafu tofauti.
TypeError: unsupported operand type(s) for +: 'Pesa' and 'int'

Hatua 4: __len__ na __contains__ — orodha_ya_kucheza.py

python
class Wimbo:
    def __init__(self, jina, msanii, sekunde):
        self.jina = jina
        self.msanii = msanii
        self.sekunde = sekunde

    def __repr__(self):
        return f"Wimbo({self.jina!r})"


class Playlist:
    def __init__(self, jina):
        self.jina = jina
        self._nyimbo = []

    def ongeza(self, wimbo):
        self._nyimbo.append(wimbo)

    def __len__(self):
        return len(self._nyimbo)

    def __contains__(self, jina_la_wimbo):
        for w in self._nyimbo:
            if w.jina.lower() == jina_la_wimbo.lower():
                return True
        return False

    def __add__(self, other):
        mpya = Playlist(f"{self.jina} + {other.jina}")
        mpya._nyimbo = self._nyimbo + other._nyimbo
        return mpya


bongo = Playlist("Bongo Flava")
bongo.ongeza(Wimbo("Wimbo A", "Msanii 1", 210))
bongo.ongeza(Wimbo("Wimbo B", "Msanii 2", 185))
injili = Playlist("Injili")
injili.ongeza(Wimbo("Wimbo C", "Kwaya", 300))

print(len(bongo))                # 2
print("wimbo a" in bongo)        # True
print("Wimbo C" in bongo)        # False
zote = bongo + injili
print(zote.jina, len(zote))      # Bongo Flava + Injili 3
if bongo:                        # __len__ > 0 => True
    print("Playlist si tupu")

Maswali ya kumuuliza mwanafunzi

  1. Polymorphism ni nini kwa sentensi moja? — Kuita method moja kwa objects tofauti, kila moja ikijibu kwa njia yake.
  2. Kwa nini hatuwezi kuunda Umbo()? — Ni abstract; lina abstract methods zisizo na implementation.
  3. Faida ya abstract class? — Inalazimisha subclasses ziwe na methods fulani; kosa linaonekana mapema.
  4. a == b inaita method gani? — a.__eq__(b).
  5. Bila __eq__, Pesa(5) == Pesa(5) ni nini? Kwa nini? — False; inalinganisha utambulisho (is).
  6. sorted() ya objects inahitaji dunder gani? — __lt__.
  7. __len__ lazima irudishe nini? — int isiyo hasi.

Makosa ya kawaida

Kosa Ujumbe wa error Suluhisho
Kuunda abstract class TypeError: Can't instantiate abstract class Umbo without an implementation for abstract method 'eneo' Unda subclass inayoandika methods zote.
Kusahau kurithi ABC @abstractmethod haizuii chochote; Umbo() inafanya kazi class Umbo(ABC):
Jina la method tofauti kwenye subclass (Eneo vs eneo) Kosa la abstract hapo juu Majina yawe sawa kabisa.
__len__ inarudisha float au None TypeError: 'float' object cannot be interpreted as an integer / 'NoneType' object cannot be interpreted as an integer return len(self._nyimbo)
sorted() bila __lt__ TypeError: '<' not supported between instances of 'Pesa' and 'Pesa' Andika __lt__ (au tumia key=, Wiki 8).
__add__ inabadilisha self badala ya kurudisha object jipya a + b inaharibu a Rudisha Pesa(...) mpya.
__eq__ + kuweka objects kwenye set TypeError: unhashable type: 'Pesa' Ongeza __hash__ (mf. return hash((self.kiasi, self.sarafu))).

Zoezi darasani

Zoezi 2.1: Abstract class Malipo yenye @abstractmethod lipa(kiasi). Subclasses MPesa (ada 1%), Kadi (ada 2.5%), Taslimu (bila ada); kila lipa irudishe string ya maelezo pamoja na ada. Andika function lipia_wote(njia, kiasi) inayozunguka list ya njia.

Zoezi 2.2: Class Vekta (x, y) yenye __add__, __eq__, __repr__, na method urefu() (√(x²+y²)). Swali la kufikiri: kwa nini tusitumie __len__ kwa urefu wa vekta? (Jibu: __len__ lazima irudishe int isiyo hasi; urefu wa vekta ni float, mf. 2.236.)

Zoezi 2.3: Ongeza __len__, __contains__ (kwa jina la bidhaa) kwenye Duka la Wiki 6.

Jibu
python
# 2.1
from abc import ABC, abstractmethod

class Malipo(ABC):
    ADA = 0.0

    @abstractmethod
    def lipa(self, kiasi):
        ...

    def ada(self, kiasi):
        return round(kiasi * self.ADA)


class MPesa(Malipo):
    ADA = 0.01
    def lipa(self, kiasi):
        return f"M-Pesa: {kiasi:,} + ada {self.ada(kiasi):,}"


class Kadi(Malipo):
    ADA = 0.025
    def lipa(self, kiasi):
        return f"Kadi: {kiasi:,} + ada {self.ada(kiasi):,}"


class Taslimu(Malipo):
    def lipa(self, kiasi):
        return f"Taslimu: {kiasi:,} (hakuna ada)"


def lipia_wote(njia, kiasi):
    for n in njia:
        print(n.lipa(kiasi))

lipia_wote([MPesa(), Kadi(), Taslimu()], 20_000)
# M-Pesa: 20,000 + ada 200
# Kadi: 20,000 + ada 500
# Taslimu: 20,000 (hakuna ada)

# 2.2
import math

class Vekta:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if not isinstance(other, Vekta):
            return NotImplemented
        return Vekta(self.x + other.x, self.y + other.y)

    def __eq__(self, other):
        if not isinstance(other, Vekta):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __repr__(self):
        return f"Vekta({self.x}, {self.y})"

    def urefu(self):
        return math.hypot(self.x, self.y)

v = Vekta(1, 2) + Vekta(2, 2)
print(v, v == Vekta(3, 4), v.urefu())   # Vekta(3, 4) True 5.0

# 2.3 -- ndani ya class Duka (Wiki 6)
def __len__(self):
    return len(self._bidhaa)

def __contains__(self, jina):
    return jina.lower() in self._bidhaa
# len(duka) -> 3 ; "sukari" in duka -> True

Kipindi 3 — Dataclasses na Type Hints

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (Pesa / Malipo)
  2. Type hints: kwa nini, syntax, list[int], dict[str, int], Optional, |
  3. @dataclass — kuandika class fupi; field(default_factory=list); order=True; frozen=True
  4. Mapumziko
  5. Mini project: Mfumo wa wafanyakazi/mishahara (inheritance + abc + dataclass + hints)
  6. Muhtasari wa OOP (Wiki 6–7)
  7. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

Type hints ni maelezo yanayosema aina ya data inayotarajiwa:

python
def salimia(jina: str, mara: int = 1) -> str:
    return ("Habari " + jina + "! ") * mara
  • jina: str — parameter inatarajiwa kuwa str. -> str — function inarudisha str.
  • Python HAIKAGUI wakati wa kuendesha! salimia(5) bado itaendeshwa (na kuleta kosa la kawaida ndani). Faida ziko kwa: (1) wasomaji wa code, (2) VS Code (autocomplete, mistari ya onyo ya Pylance), (3) zana kama mypy.
  • Aina za makusanyo (Python 3.9+): list[int], dict[str, float], tuple[int, str], set[str].
  • Thamani inayoweza kuwa None: str | None (Python 3.10+) — sawa kabisa na Optional[str] kutoka typing. Tumia | kwa code mpya; jua Optional kwa sababu utaiona kwenye code za watu wengine.
  • Aina nyingi: int | float.
  • Variables: alama: list[int] = [].
  • Function isiyorudisha kitu: -> None.

Pylance (ndani ya VS Code) inaonyesha makosa ya aina ukiwasha: Settings → tafuta "Type Checking Mode" → basic. Hiari.

@dataclass (kutoka dataclasses) — decorator inayoandika __init__, __repr__, na __eq__ yenyewe kutokana na type hints za class attributes:

python
from dataclasses import dataclass

@dataclass
class Bidhaa:
    jina: str
    bei: int
    idadi: int = 0

Hii ni sawa na kuandika __init__(self, jina, bei, idadi=0), __repr__ na __eq__ kwa mkono! Hapa type hints zinahitajika (ndio zinazoiambia dataclass ni fields zipi).

Sheria/mitego:

  • Fields zenye default lazima zije baada ya zisizo na default (kama function parameters), vinginevyo: TypeError: non-default argument 'bei' follows default argument 'idadi'.
  • Default ya list/dict: huwezi kuandika vitu: list[str] = [] → ValueError: mutable default <class 'list'> for field vitu is not allowed: use default_factory. Tumia field(default_factory=list).
  • @dataclass(order=True) → inaongeza <, >, <=, >= (inalinganisha fields kwa mpangilio wake) — sorted() inafanya kazi.
  • @dataclass(frozen=True) → object haliwezi kubadilishwa baada ya kuundwa (FrozenInstanceError); pia linakuwa hashable.
  • __post_init__(self) → inaitwa baada ya __init__ iliyoundwa; nzuri kwa validation.
  • Dataclass bado ni class ya kawaida: unaweza kuongeza methods, @property, na kurithi.
  • dataclasses.asdict(obj) → dict (nzuri kwa JSON — tutatumia Wiki 8!).

Sema hivi kwa mwanafunzi

  • "Type hints ni kama lebo kwenye droo: 'Visu', 'Vijiko'. Lebo hazizuii mtu kuweka kijiko kwenye droo ya visu, lakini zinasaidia kila mtu kujua nini kiende wapi."
  • "@dataclass ni kama fomu iliyochapishwa tayari. Badala ya kuchora mistari ya fomu kwa mkono kila mara (__init__, __repr__), unaandika tu majina ya sehemu na printer inafanya mengine."

Andika code hii live (share screen)

Hatua 1: Type hints — hints.py

python
def wastani(alama: list[int]) -> float:
    return sum(alama) / len(alama)


def tafuta_simu(jina: str, kitabu: dict[str, str]) -> str | None:
    return kitabu.get(jina)


def salimia(jina: str, cheo: str | None = None) -> str:
    if cheo:
        return f"Habari {cheo} {jina}"
    return f"Habari {jina}"


print(wastani([80, 70, 90]))
kitabu = {"Asha": "0712000000"}
print(tafuta_simu("Asha", kitabu))
print(tafuta_simu("Juma", kitabu))
print(salimia("Juma"), "|", salimia("Neema", "Dkt."))
print(wastani.__annotations__)

Output:

text
80.0
0712000000
None
Habari Juma | Habari Dkt. Neema
{'alama': list[int], 'return': <class 'float'>}

Kisha onyesha Python haikagui: print(salimia(123)) → Habari 123 (inafanya kazi!). Sema: "Hints ni ushauri, si sheria — lakini VS Code inakuonyesha." Elea juu ya wastani kwenye VS Code uonyeshe tooltip.

Onyesha Optional:

python
from typing import Optional

def tafuta(jina: str) -> Optional[int]:   # sawa na: int | None
    return None

(Hiari, kama mypy imesakinishwa kwenye venv):

powershell
python -m pip install mypy
python -m mypy hints.py

Ukiongeza salimia(123), mypy itasema: error: Argument 1 to "salimia" has incompatible type "int"; expected "str" [arg-type].

Hatua 2: Class ya kawaida vs dataclass — dc_demo.py

python
from dataclasses import dataclass, field, asdict


class BidhaaYaZamani:
    def __init__(self, jina: str, bei: int, idadi: int = 0):
        self.jina = jina
        self.bei = bei
        self.idadi = idadi

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

    def __eq__(self, other):
        if not isinstance(other, BidhaaYaZamani):
            return NotImplemented
        return (self.jina, self.bei, self.idadi) == (other.jina, other.bei, other.idadi)


@dataclass
class Bidhaa:
    jina: str
    bei: int
    idadi: int = 0

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


b = Bidhaa("Sukari", 3000, 10)
print(b)
print(b == Bidhaa("Sukari", 3000, 10))
print(b.thamani)
print(asdict(b))

Output:

text
Bidhaa(jina='Sukari', bei=3000, idadi=10)
True
30000
{'jina': 'Sukari', 'bei': 3000, 'idadi': 10}

Sema: "Mistari 14 imekuwa 4!"

Hatua 3: default_factory, order, frozen, __post_init__

python
from dataclasses import dataclass, field


@dataclass
class Mwanafunzi:
    jina: str
    alama: list[int] = field(default_factory=list)

    def __post_init__(self):
        if not self.jina.strip():
            raise ValueError("Jina haliwezi kuwa tupu.")

    def wastani(self) -> float:
        return sum(self.alama) / len(self.alama) if self.alama else 0.0


a = Mwanafunzi("Asha")
j = Mwanafunzi("Juma")
a.alama.append(90)
print(a, j)          # alama za Juma bado ni tupu

try:
    Mwanafunzi("   ")
except ValueError as e:
    print(e)


@dataclass(order=True)
class Toleo:
    kuu: int
    ndogo: int


matoleo = [Toleo(3, 12), Toleo(3, 9), Toleo(2, 7)]
print(sorted(matoleo))
print(Toleo(3, 12) > Toleo(3, 9))


@dataclass(frozen=True)
class Nukta:
    x: float
    y: float


n = Nukta(1, 2)
try:
    n.x = 5
except Exception as e:
    print(type(e).__name__, ":", e)
print({n, Nukta(1, 2)})   # frozen => hashable, set ina moja tu

Output:

text
Mwanafunzi(jina='Asha', alama=[90]) Mwanafunzi(jina='Juma', alama=[])
Jina haliwezi kuwa tupu.
[Toleo(kuu=2, ndogo=7), Toleo(kuu=3, ndogo=9), Toleo(kuu=3, ndogo=12)]
True
FrozenInstanceError : cannot assign to field 'x'
{Nukta(x=1, y=2)}

Onyesha mtego: badilisha field(default_factory=list) kuwa = [] → ValueError: mutable default <class 'list'> for field alama is not allowed: use default_factory.

Hatua 4: Mini project — Mishahara — mishahara.py (kila kitu cha Wiki 6–7 pamoja)

python
from abc import ABC, abstractmethod
from dataclasses import dataclass, field


@dataclass
class Mfanyakazi(ABC):
    jina: str
    idara: str

    @abstractmethod
    def mshahara(self) -> int:
        ...

    def __str__(self) -> str:
        return f"{self.jina:<10}{self.idara:<10}{type(self).__name__:<12}{self.mshahara():>12,}"


@dataclass
class WaKudumu(Mfanyakazi):
    mshahara_wa_mwezi: int = 0

    def mshahara(self) -> int:
        return self.mshahara_wa_mwezi


@dataclass
class WaSaa(Mfanyakazi):
    saa: int = 0
    kiwango: int = 0

    def mshahara(self) -> int:
        return self.saa * self.kiwango


@dataclass
class Kampuni:
    jina: str
    wafanyakazi: list[Mfanyakazi] = field(default_factory=list)

    def ajiri(self, m: Mfanyakazi) -> None:
        self.wafanyakazi.append(m)

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

    def jumla_ya_mishahara(self) -> int:
        return sum(m.mshahara() for m in self.wafanyakazi)

    def wa_idara(self, idara: str) -> list[Mfanyakazi]:
        return [m for m in self.wafanyakazi if m.idara == idara]

    def ripoti(self) -> None:
        print(f"=== {self.jina} ({len(self)} wafanyakazi) ===")
        for m in self.wafanyakazi:
            print(m)
        print(f"{'JUMLA':<32}{self.jumla_ya_mishahara():>12,}")


k = Kampuni("Python Tech Ltd")
k.ajiri(WaKudumu("Asha", "IT", 1_200_000))
k.ajiri(WaKudumu("Juma", "Mauzo", 900_000))
k.ajiri(WaSaa("Baraka", "IT", saa=80, kiwango=6_000))
k.ripoti()
print("IT:", [m.jina for m in k.wa_idara("IT")])

Output:

text
=== Python Tech Ltd (3 wafanyakazi) ===
Asha      IT        WaKudumu       1,200,000
Juma      Mauzo     WaKudumu         900,000
Baraka    IT        WaSaa            480,000
JUMLA                              2,580,000
IT: ['Asha', 'Baraka']

Maelezo kwa mwalimu: Dataclass ya child inapanga fields hivi: kwanza za parent (jina, idara), kisha zake (saa, kiwango). Kama parent angekuwa na field yenye default, field yoyote ya child isiyo na default ingeleta TypeError: non-default argument 'saa' follows default argument. Ndiyo maana tumeweka = 0 kwenye fields za child — ni mazoea salama. Pia @dataclass haiongezi __str__, kwa hiyo __str__ yetu inatumika na print, wakati __repr__ ni ya dataclass. Comprehensions ([m.jina for m in ...]) ni Wiki 8; ni sawa kuzitumia kama "onyesho la mbele".

Maswali ya kumuuliza mwanafunzi

  1. Je, Python inazuia salimia(123) kama jina: str? — Hapana; hints hazikaguliwi wakati wa kuendesha.
  2. str | None na Optional[str] — tofauti? — Hakuna; ni sawa. | ni syntax mpya (3.10+).
  3. @dataclass inatuandikia methods gani? — __init__, __repr__, __eq__.
  4. Kwa nini alama: list[int] = [] inakataliwa? — Objects zote zingeshiriki list moja (mtego wa Wiki 6); tumia field(default_factory=list).
  5. order=True inaongeza nini? — Ulinganisho <, >, n.k., hivyo sorted() inafanya kazi.
  6. frozen=True inafanya nini? — Object haliwezi kubadilishwa baada ya kuundwa.
  7. Ni ipi ya kutumia: class ya kawaida au dataclass? — Dataclass kama class inahifadhi data hasa; class ya kawaida kama kuna logic nyingi ya __init__ au encapsulation kali.

Makosa ya kawaida

Kosa Ujumbe wa error Suluhisho
Field isiyo na type hint kwenye dataclass: jina = "x" Haiwi field (haipo kwenye __init__): TypeError: Bidhaa.__init__() takes ... positional arguments Andika jina: str = "x"
Default kabla ya isiyo na default TypeError: non-default argument 'bei' follows default argument 'idadi' Weka fields zenye default mwishoni.
List kama default ValueError: mutable default <class 'list'> for field alama is not allowed: use default_factory field(default_factory=list)
Kusahau from dataclasses import dataclass NameError: name 'dataclass' is not defined Import.
Kubadilisha frozen dataclass dataclasses.FrozenInstanceError: cannot assign to field 'x' Unda object jipya: dataclasses.replace(n, x=5)
list[int] kwenye Python ya zamani (<3.9) TypeError: 'type' object is not subscriptable Tumia Python 3.12 (au from typing import List).

Zoezi darasani

Zoezi 3.1: Ongeza type hints kwenye functions 3 ulizoandika Wiki 4–5 (mf. angalia_alama, hifadhi_mawasiliano).

Zoezi 3.2: Dataclass Kitabu (isbn: str, kichwa: str, mwandishi: str, mwaka: int, kimeazimwa: bool = False). Dataclass Rafu yenye vitabu: list[Kitabu] (default_factory), methods ongeza(k), tafuta_mwandishi(jina) -> list[Kitabu], __len__. (Hii ni maandalizi ya mradi wa Wiki 8!)

Zoezi 3.3: Badilisha Pesa ya Kipindi 2 kuwa @dataclass(frozen=True, order=True) na uongeze __add__ tu. Je, bado unahitaji kuandika __eq__ na __lt__?

Jibu
python
# 3.1 (mifano)
def angalia_alama(alama: int) -> str: ...
def hifadhi_mawasiliano(jina: str, simu: str) -> None: ...
def bei_ya(bidhaa: str, orodha: dict[str, int]) -> int: ...

# 3.2
from dataclasses import dataclass, field

@dataclass
class Kitabu:
    isbn: str
    kichwa: str
    mwandishi: str
    mwaka: int
    kimeazimwa: bool = False


@dataclass
class Rafu:
    vitabu: list[Kitabu] = field(default_factory=list)

    def ongeza(self, k: Kitabu) -> None:
        self.vitabu.append(k)

    def tafuta_mwandishi(self, jina: str) -> list[Kitabu]:
        matokeo = []
        for k in self.vitabu:
            if jina.lower() in k.mwandishi.lower():
                matokeo.append(k)
        return matokeo

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


r = Rafu()
r.ongeza(Kitabu("978-1", "Kinjeketile", "Ebrahim Hussein", 1969))
r.ongeza(Kitabu("978-2", "Rosa Mistika", "Euphrase Kezilahabi", 1971))
print(len(r))
print(r.tafuta_mwandishi("kezilahabi"))

# 3.3
from dataclasses import dataclass

@dataclass(frozen=True, order=True)
class Pesa:
    kiasi: int
    sarafu: str = "TZS"

    def __add__(self, other: "Pesa") -> "Pesa":
        if not isinstance(other, Pesa):
            return NotImplemented
        if self.sarafu != other.sarafu:
            raise ValueError("Sarafu tofauti")
        return Pesa(self.kiasi + other.kiasi, self.sarafu)

print(Pesa(100) + Pesa(50))            # Pesa(kiasi=150, sarafu='TZS')
print(Pesa(100) == Pesa(100))          # True
print(sorted([Pesa(5), Pesa(1)]))      # [Pesa(kiasi=1, ...), Pesa(kiasi=5, ...)]
# Hapana: dataclass inatengeneza __eq__ na __lt__ yenyewe.
# (Tahadhari: order=True inalinganisha (kiasi, sarafu) kama tuple.)

"Pesa" kwenye quotes: ndani ya class, jina Pesa bado halijakamilika, kwa hiyo tunaliandika kama string (forward reference). Taja tu.


Kazi ya Nyumbani

(Inatakiwa: kipindi kijacho)

Kazi 1 — Maumbo (baada ya Kipindi 1): Class Umbo (bila abc bado) yenye eneo() inayorudisha 0. Subclasses Mraba(upande), Pembetatu(kitako, kimo), Duara(r) zikioveride eneo(). Tengeneza list ya maumbo 5 na uchapishe eneo la jumla.

Kazi 2 — Mfumo wa wanyama wa shamba (baada ya Kipindi 2): Abstract class Mnyama (jina, abstract toa_sauti() na chakula()), subclasses 3. Class Shamba yenye __len__, __contains__ (kwa jina), __add__ (kuunganisha mashamba mawili), na method wakati_wa_kula() inayozunguka wanyama wote (polymorphism).

Kazi 3 — Rafu ya vitabu (baada ya Kipindi 3): Endeleza Zoezi 3.2: ongeza kwenye Rafu methods azima(isbn) -> None (rusha custom exception KitabuHakipo au KimeshaAzimwa), rudisha(isbn), vinavyopatikana() -> list[Kitabu]. Type hints kila mahali. Hii itakuwa msingi wa Mradi wa Mwezi 2 — iletwe!

Jibu
python
# Kazi 1
import math

class Umbo:
    def eneo(self) -> float:
        return 0

class Mraba(Umbo):
    def __init__(self, upande: float):
        self.upande = upande
    def eneo(self) -> float:
        return self.upande ** 2

class Pembetatu(Umbo):
    def __init__(self, kitako: float, kimo: float):
        self.kitako = kitako
        self.kimo = kimo
    def eneo(self) -> float:
        return 0.5 * self.kitako * self.kimo

class Duara(Umbo):
    def __init__(self, r: float):
        self.r = r
    def eneo(self) -> float:
        return math.pi * self.r ** 2

maumbo = [Mraba(2), Mraba(3), Pembetatu(4, 5), Duara(1), Duara(2)]
jumla = 0
for u in maumbo:
    jumla += u.eneo()
print(f"Eneo la jumla: {jumla:.2f}")   # 4+9+10+3.14+12.57 = 38.71
python
# Kazi 2
from abc import ABC, abstractmethod

class Mnyama(ABC):
    def __init__(self, jina: str):
        self.jina = jina

    @abstractmethod
    def toa_sauti(self) -> str: ...

    @abstractmethod
    def chakula(self) -> str: ...

class Ngombe(Mnyama):
    def toa_sauti(self): return "Mooo"
    def chakula(self): return "majani"

class Kuku(Mnyama):
    def toa_sauti(self): return "Kokoriko"
    def chakula(self): return "mahindi"

class Mbuzi(Mnyama):
    def toa_sauti(self): return "Meee"
    def chakula(self): return "majani na matawi"

class Shamba:
    def __init__(self, jina: str):
        self.jina = jina
        self.wanyama: list[Mnyama] = []

    def ongeza(self, m: Mnyama) -> None:
        self.wanyama.append(m)

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

    def __contains__(self, jina: str) -> bool:
        return any(m.jina == jina for m in self.wanyama)

    def __add__(self, other: "Shamba") -> "Shamba":
        mpya = Shamba(f"{self.jina} & {other.jina}")
        mpya.wanyama = self.wanyama + other.wanyama
        return mpya

    def wakati_wa_kula(self) -> None:
        for m in self.wanyama:
            print(f"{m.jina} ({type(m).__name__}) anasema {m.toa_sauti()} na anakula {m.chakula()}")

a = Shamba("Shamba la Babu")
a.ongeza(Ngombe("Bahati"))
a.ongeza(Kuku("Kiroboto"))
b = Shamba("Shamba la Mama")
b.ongeza(Mbuzi("Zawadi"))
c = a + b
print(len(c), "Zawadi" in c, "Simba" in c)    # 3 True False
c.wakati_wa_kula()
python
# Kazi 3
from dataclasses import dataclass, field


class KosaLaMaktaba(Exception):
    pass

class KitabuHakipo(KosaLaMaktaba):
    pass

class KimeshaAzimwa(KosaLaMaktaba):
    pass


@dataclass
class Kitabu:
    isbn: str
    kichwa: str
    mwandishi: str
    mwaka: int
    kimeazimwa: bool = False


@dataclass
class Rafu:
    vitabu: list[Kitabu] = field(default_factory=list)

    def ongeza(self, k: Kitabu) -> None:
        self.vitabu.append(k)

    def _tafuta(self, isbn: str) -> Kitabu:
        for k in self.vitabu:
            if k.isbn == isbn:
                return k
        raise KitabuHakipo(f"Hakuna kitabu chenye ISBN {isbn}")

    def azima(self, isbn: str) -> None:
        k = self._tafuta(isbn)
        if k.kimeazimwa:
            raise KimeshaAzimwa(f"'{k.kichwa}' kimeshaazimwa.")
        k.kimeazimwa = True

    def rudisha(self, isbn: str) -> None:
        k = self._tafuta(isbn)
        k.kimeazimwa = False

    def vinavyopatikana(self) -> list[Kitabu]:
        return [k for k in self.vitabu if not k.kimeazimwa]

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


r = Rafu()
r.ongeza(Kitabu("978-1", "Kinjeketile", "Ebrahim Hussein", 1969))
r.ongeza(Kitabu("978-2", "Rosa Mistika", "Euphrase Kezilahabi", 1971))
r.azima("978-1")
try:
    r.azima("978-1")
except KosaLaMaktaba as e:
    print(e)                          # 'Kinjeketile' kimeshaazimwa.
print([k.kichwa for k in r.vinavyopatikana()])   # ['Rosa Mistika']

Ukiulizwa swali usilolijua

Checklist ya mwisho wa wiki

  • Anaunda subclass na kutumia super().__init__() kwa usahihi.
  • Anaoveride method na kuita toleo la parent kwa super().
  • Anatumia isinstance na anaelewa kwa nini except Exception inashika custom exceptions.
  • Anaeleza polymorphism na duck typing kwa mfano.
  • Anaunda abstract class kwa ABC + @abstractmethod na anaelewa kosa la kuiunda moja kwa moja.
  • Anaandika __len__, __eq__, __add__ (na anajua NotImplemented).
  • Anatumia @dataclass, field(default_factory=list), order=True, frozen=True.
  • Anaandika type hints: list[int], dict[str, int], str | None, -> None.
  • Anaelewa kuwa Python haikagui type hints wakati wa kuendesha.
  • Kazi 3 (Rafu ya vitabu) imekamilika — itatumika kwenye mradi wa Wiki 8.