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

URL: https://skillaria.top/tl/guides/python/class-python-oop

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

| Pagkakamali | Sintomas | Paano ayusin |
| --- | --- | --- |

| Nakalimutan ang `self` sa method | `TypeError: ... takes 0 positional arguments but 1 was given` | Laging `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 attribute | Dumadaloy ang datos sa pagitan ng mga object | Gumawa ng listahan sa `__init__` o gamit ang `default_factory` |

| Walang `super().__init__()` | `AttributeError` sa mga field ng magulang | Tawagin ang initializer ng magulang |

| Pagtawag sa property na may panaklong | `TypeError: 'int' object is not callable` | Basahin 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.

## FAQ

**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.


---
Source: Skillaria.Top (https://skillaria.top)
