Batayan

Mga class at OOP sa Python: __init__, self, method, inheritance, at dataclass

Na-update: · Skillaria.Top

Ang OOP sa Python ay nakasentro sa mga class: inilalarawan ng class ang mga field at method, at ang mga object (instance) ay nalilikha sa pagtawag sa class; pinupuno ng __init__ ang mga field, at tinutukoy ng self ang kasalukuyang object. Pinapayagan ng inheritance na gamitin ang gawi ng magulang at i-override ang ilang method, at ang decorator na @dataclass ay bumubuo ng __init__, __repr__, at __eq__ para sa mga class na pangunahing nag-iimbak ng datos.

Ang class sa Python ay blueprint: inilalarawan nito kung anong datos ang iniimbak ng object at ano ang puwedeng gawin dito. Ang object (instance) ay ang aktuwal na produkto mula sa blueprint na iyon. Nakasandig ang OOP sa Python sa tatlong bagay: mga class na may method, inheritance, at mga special method gaya ng __init__ at __repr__. Isa-isahin natin ang lahat gamit ang iisang halimbawa — ang pagsubaybay ng gastos.

Bakit kailangan ng class kung may dictionary naman

Puwedeng itago ang datos sa dictionary: {"title": "Kape", "amount": 250}. Ang problema, hindi alam ng dictionary kung alin ang tamang key. Ang typo na expense.get("amout", 0) ay hindi magbibigay ng error — magbabalik ito ng zero, at unti-unting magiging mali ang ulat nang hindi namamalayan.

Sa object ng class, nakadeklara nang maaga ang mga field. Ang pag-access sa attribute na wala ay agad na bumabagsak sa AttributeError, at ang modernong Python ay nagmumungkahi pa ng tamang pangalan. Bukod doon, pinagsasama ng class ang datos at ang mga aksyon dito sa iisang lugar: ang gastos mismo ay marunong magkalkula ng halaga kasama ang buwis, at ang ulat ay marunong magkalkula ng kabuuan.

Paano gumawa ng class sa Python

Idinedeklara ang class gamit ang salitang class, at ayon sa PEP 8, isinusulat ang pangalan sa CamelCase. Pinakasimpleng halimbawa ng class:

class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("Kape", 250)
taxi = Expense("Taksi", 300)
print(coffee.title, coffee.amount)   # Kape 250

Ang pagtawag na Expense("Kape", 250) ay lumilikha ng bagong instance ng class. Ang coffee at taxi ay dalawang magkaibang object ng iisang uri, may kanya-kanyang halaga ang bawat field. Puwedeng suriin ang pagiging miyembro gamit ang isinstance(coffee, Expense) — magbabalik ito ng True.

Constructor na __init__: pag-initialize ng class

Madalas tawagin ang __init__ na constructor ng class, pero mas tumpak ang "initializer". Ang method na __new__ ang gumagawa ng object mismo, at ang __init__ ay tinatawag kaagad pagkatapos nito upang ilagay ang mga ipinasang argumento sa mga field. Wala itong kailangang ibalik (sa totoo, dapat itong magbalik ng None).

Sa loob ng __init__, kapaki-pakinabang na suriin ang input, para hindi lumitaw sa programa ang mga object na tiyak na mali:

class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("Hindi maaaring negatibo ang halaga")
        self.title = title
        self.amount = amount

Ano ang self sa Python

Ang self ay reference sa mismong object na kasalukuyang ginagamit ng method. Kapag isinulat mo ang coffee.with_tax(), sa likod nito ay tinatawag ang Expense.with_tax(coffee) at ipinapasa ang object bilang unang argumento. Kaya nasa unahan ang self sa deklarasyon ng method, at hindi ito isinusulat sa pagtawag.

Ang self ay hindi keyword kundi kombensiyon: teknikal na puwedeng ibang pangalan ang parameter, pero walang gumagawa nito. Karaniwang pagkakamali ng baguhan ang makalimutan ang self sa deklarasyon ng method. Sa ganoong kaso, babagsak ang tawag sa TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given.

Mga method ng class at special method

Ang method ay function na idinedeklara sa loob ng class. Nakikita nito ang mga field ng object sa pamamagitan ng 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("Kape", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('Kape', 250)

Kung walang __repr__, ang ipapakita ay ganito: <__main__.Expense object at 0x7f...> — walang silbi para sa debugging. Ang mga method na may dalawang underscore sa magkabilang gilid ay tinatawag na special (dunder) method: sila mismo ang tinatawag ng Python. Ang __str__ ang humahawak sa "magandang" output para sa user, ang __eq__ sa paghahambing gamit ang ==, at ang __len__ sa len(obj).

@classmethod at @staticmethod

Gumagana sa instance ang karaniwang method. Ang method ng klase na may decorator na @classmethod ay tumatanggap ng mismong klase (cls) bilang unang argumento — madalas itong gamitin bilang alternatibong constructor. Ang static method na @staticmethod ay hindi tumatanggap ng object o klase: ordinaryong function ito na lohikal na kabilang sa klase.

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("Tanghalian;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False

Destructor ng class: __del__

May method na __del__ sa Python na tinatawag bago masira ang object ng garbage collector. Huwag umasa rito sa pagpapalaya ng resource: hindi garantisado ang sandali ng pagtawag. Para sa mga file at koneksiyon, gamitin ang context manager na with o ang tahasang method na close().

Mga class attribute at instance attribute

Ang mga field na itinakda gamit ang self.name = ... ay pag-aari ng partikular na object. Ang mga variable na ideklara mismo sa katawan ng class ay class attribute: pareho ang halaga para sa lahat ng instance. Dito karaniwang inilalagay ang mga constant ng klase.

class Expense:
    CURRENCY = "PHP"          # class attribute, constant

    def __init__(self, title, amount):
        self.title = title    # mga instance attribute
        self.amount = amount

a = Expense("Kape", 250)
b = Expense("Tsaa", 150)
print(a.CURRENCY, Expense.CURRENCY)   # PHP PHP

Mapanganib na bitag ang mutable na class attribute. Kung ideklara sa katawan ng class ang rows = [] at idagdag dito sa pamamagitan ng self.rows.append(...), iisa ang listahan para sa lahat ng object. Laging gawin ang mga mutable na field sa loob ng __init__.

Makikita ang mga field ng object sa pamamagitan ng vars(obj) o obj.__dict__, at mababasa ang attribute ayon sa pangalan mula sa string gamit ang getattr(obj, "amount").

Mga property ng class: @property

Ang property ay method na binabasa sa labas na parang ordinaryong field, nang walang panaklong. Kapaki-pakinabang ito para sa mga halagang kinakalkula: hindi na kailangang itago nang hiwalay ang kabuuan ng ulat, at hindi ito makakalimutang i-update.

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("Kape", 250))
r.add(Expense("Taksi", 300))
print(r.total)   # 550

Kapag kailangang kontrolin ang pagsusulat, idinadagdag ang setter gamit ang @total.setter. Kung walang setter, ang pag-assign na r.total = 0 ay magdudulot ng AttributeError.

Tungkol naman sa encapsulation: sa mahigpit na kahulugan, walang tunay na private na field sa Python. Ang iisang underscore (_cache) ay kasunduan na "huwag galawin mula sa labas", at ang dalawang underscore (__secret) ay nag-a-activate ng name mangling, para hindi aksidenteng ma-override ng subclass ang field.

Inheritance ng mga class sa Python

Pinapayagan ng inheritance na gumawa ng child class batay sa parent class: nakukuha ng anak ang lahat ng field at method, at ino-override ang mga kailangan. Nakasulat ang parent class sa loob ng panaklong.

class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # tawagin ang __init__ ng magulang
        self.month = month

    def add(self, expense):       # pag-override ng method
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # panatilihin ang gawi ng magulang

m = MonthlyReport("Oktubre")
m.add(Expense("Kape", 250))   # [Oktubre] + Expense('Kape', 250)
print(m.total)                # 250 — minana ang property

super() ang nagbabalik ng intermediary object para tawagin ang mga method ng magulang. Karaniwang pagkakamali ang makalimutan ang super().__init__() sa __init__ ng anak: hindi malilikha ang field na rows, at babagsak sa AttributeError ang unang add.

Puwedeng suriin ang relasyon gamit ang issubclass(MonthlyReport, Report). Lahat ng class sa Python 3 ay tahasang nagmamana mula sa object — kaya nagmula rito ang mga pangunahing __repr__, __eq__, at iba pang method. Sinusuportahan din ng Python ang multiple inheritance (class C(A, B)); makikita ang pagkakasunod ng paghahanap ng method sa C.__mro__.

Abstract class sa Python

Naglalatag ang abstract class ng interface: anong mga method ang dapat ipatupad ng mga subclass. Hindi maaaring gumawa ng instance nito. Ginagamit dito ang module na 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 ...

Ang subclass na hindi nagpatupad ng lahat ng abstract method ay mananatiling abstract din, at magbibigay ng parehong error kapag gumawa ng object.

Composition sa halip na malalalim na hierarchy

Pagkatapos matutunan ang inheritance, natutukso tayong gumawa ng mahabang hagdan: pangunahing gastos, gastos sa pera, gastos sa piso — at ganoon nang lima ang lebel. Bihira itong gawin sa Python. Inilalarawan ng inheritance ang relasyong "is-a", samantalang ang composition ay "has-a": ang ulat ay hindi listahan ng gastos; naglalaman ito ng listahan sa field na rows.

Praktikal na tuntunin: kung kailangan lang gamitin ang kakayahan ng ibang object, ilagay ito sa field, huwag magmana. Para sa datos, sapat na ang dataclass, at ang lohika ay puwedeng itago sa ordinaryong function.

Dataclass sa Python: class na walang paulit-ulit na code

Para sa mga class na pangunahing nag-iimbak ng datos, nakakapagod isulat nang manu-mano ang __init__, __repr__, at __eq__. Ginagawa ito ng decorator na @dataclass mula sa module na dataclasses, batay sa mga annotation ng field:

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("Hindi maaaring negatibo ang halaga")

e = Expense("Kape", 250)
print(e)                           # Expense(title='Kape', amount=250, tags=[])
print(e == Expense("Kape", 250))   # True

Ang pagsusuri ng halaga na dating isinusulat sa __init__ ay inililipat sa __post_init__ — tinatawag ito kaagad pagkatapos ng nabuong initializer. Ang mutable na default na halaga ay itinatakda lamang gamit ang field(default_factory=list): ang anyong tags: list = [] ay ipinagbabawal ng dataclass at naglalabas ng ValueError.

frozen at slots

Ginagawang hindi nababago ng frozen=True ang object, at ang slots=True (mula Python 3.10) ay gumagawa ng __slots__ at nagtitipid sa memorya. Ang pagtatangkang baguhin ang field ng frozen na object ay nagdudulot ng FrozenInstanceError. Para makakuha ng object na may bagong halaga, gumawa ng kopya:

from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("Kape", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='Kape', amount=250) Expense(title='Kape', amount=300)

Mula Python 3.13, ginagawa rin ng pangkalahatang function na copy.replace() ang parehong bagay. Ang frozen na dataclass object ay puwedeng ilagay sa set at gamitin bilang key ng dictionary — mayroon itong __hash__.

Mga bagong bagay sa class: Self, @override, __slots__

May dalawang kapaki-pakinabang na kasangkapan para sa typing ng class. Ang typing.Self (Python 3.11, PEP 673) ay nangangahulugang "magbabalik ng object ng parehong klase" — gumagana ito kahit sa mga method na minana. Ang decorator na @override (Python 3.12, PEP 698) ay nagmamarka ng method na dapat mag-override ng method ng magulang: kung may typo sa pangalan, sasabihin ng mypy o pyright na walang base method na may ganoong pangalan.

from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # puwedeng tawagin nang sunod-sunod

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)

Sa Python 3.15, inalis ang isang makitid na limitasyon: pinapayagan na ang di-walang-lamang __slots__ sa mga subclass ng tuple, at maaaring ilista ang __dict__ at __weakref__ sa __slots__ ng anumang class. Sa 3.14, ang ganitong deklarasyon sa anak ng tuple ay bumabagsak sa TypeError: nonempty __slots__ not supported for subtype of 'tuple'.

Mga karaniwang pagkakamali sa class

PagkakamaliSintomasPaano ayusin
Nakalimutan ang self sa methodTypeError: ... takes 0 positional arguments but 1 was givenLaging self ang unang parameter ng method
Field sa __init__ na walang self.Nawawala ang halaga pagkatapos ng __init__Isulat ang self.amount = amount
Listahan bilang class attributeDumadaloy ang datos sa pagitan ng mga objectGumawa ng listahan sa __init__ o gamit ang default_factory
Walang super().__init__()AttributeError sa mga field ng magulangTawagin ang initializer ng magulang
Pagtawag sa property na may panaklongTypeError: 'int' object is not callableBasahin ang property nang walang panaklong: r.total

Buod

Nagsisimula ang OOP sa Python sa simpleng bagay: inilalarawan ng class ang mga field at method, pinupuno ng __init__ ang mga field, at tinutukoy ng self ang kasalukuyang object. Pinalalawak ng inheritance at super() ang gawi, nagtatakda ng interface ang abstract class, at gumagawa ng mga kinakalkulang field ang @property. Para sa mga class na datos, gamitin ang @dataclass, at sa malalalim na hierarchy, mas piliin ang composition. Tinalakay ang mga class at dataclass nang praktikal sa lektyur ng kurso ng Python, gamit ang tagasubaybay ng gastos.

Mga madalas itanong

Ano ang pagkakaiba ng class at object sa Python?

Ang class ay paglalarawan ng uri: anong mga field at method ang mayroon ito. Ang object, o instance, ay nalilikha sa pagtawag sa class, halimbawa Expense("Kape", 250), at iniimbak ang mga partikular na halaga ng field.

Kailangan bang isulat ang __init__ sa class?

Hindi. Kung walang __init__, ginagamit ang minana mula sa object, na hindi tumatanggap ng argumento. Sa dataclass, ginagawa ito ng decorator.

Ano ang pagkakaiba ng __str__ at __repr__?

Ang __repr__ ay hindi malabong representasyon para sa developer at debugger; ang __str__ ay magandang output para sa user sa print at f-string. Kapag walang __str__, ginagamit ng Python ang __repr__.

Kailan dapat pumili ng dataclass at kailan ng ordinaryong class?

Angkop ang dataclass kapag pangunahing nag-iimbak ng datos ang class at kailangan ang handang __init__, __repr__, at paghahambing. Mas maginhawa ang ordinaryong class kapag mahalaga ang masalimuot na lohika ng initialization, o mas mahalaga ang gawi kaysa datos.

May mga private na field ba sa Python?

Walang mahigpit na privacy. Ang iisang underscore sa unahan ng pangalan ay kasunduan na "hindi para sa panlabas na gamit"; ang dalawang underscore ay nag-a-activate ng name mangling na nagpoprotekta sa field laban sa aksidenteng pag-override sa subclass.

Pag-aralan ito sa video

Detalyadong tinatalakay ang paksang ito, may praktis, sa kursong “Python 3.15: от новичка до профи”.

Pumunta sa kurso