Ugrás a lényegre

Scope és Osztály

TL;DR Tartalom
Extra feladatok

Ezek nem kötelező feladatok, csak megoldásuk közben könyebb megtanulni a dolgokat

  • Készíts osztályokat a kutyáknak, macskáknak és a medvéknek úgy, hogy ezeknek legyen ősosztálya és a legtöbb közös tulajdonságot reprezentálja.
  • Injektálj be egy osztályt a kutyák és a készített ősosztály közé úgy, hogy minden konstruktor lefusson. (Tipp: super)

Osztályok

Az osztályok egy lehetőséget biztosítanak, hogy az adatainkat és a hozzájuk kapcsolódó funkcionalitást összecsatoljuk.
Osztályainkból különböző objektumokat tudunk létrehozni, melyek saját egyedi adatokkal rendelkeznek.
Ezek mellett az egyes osztályainknak lehetnek leszármazottaik, melyek az ősosztály funkcionalitásait és adatait megöröklik.

Na de miért használnék Pythonban osztályokat?
Először is biztosítja, hogy objektum orientált szemlélettel dolgozzunk, továbbá függvényben mikor objektumot adunk át, akkor valójában a háttérben csak egy arra mutató pointert visz tovább a Python, tehát sokkal hatékonyabbá válik a program.
Továbbá az objektumainkat tudjuk a függvényen belül módosítani és nem csak egy másolatával dolgozunk.

Elnevezések

Pythonban a függvényeinket és változóinkat szokás snake_case-esítve írni. Ez annyit jelent, hogy minden betű kisbetűs és a szóköz helyett _-k vannak.

Osztályainkat pedig UpperCaseCamelCase írjuk, azaz egybe, és minden szó kezdő betűjét nagy betűvel szóköz nélkül.

Scope

Ahhoz, hogy megértsük, hogy hogyan tudunk egy objektumhoz adatot hozzá csatolni, meg kell értsük a Scope-okat is.

A Python folyamatosan menedzseli a jelenleg kezelt neveket és hozzájuk tartozó objektumokat. Egy katalógusként tudod ezt az egészet elképzelni.
Ezeket névtereknek (namespace) nevezzük és több fajtáját különböztetjük meg.

Built-in Namespace - Beépített névtér

Itt vannak azok az objektumok, melyeket bármikor elérünk a kódunkban.

>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', ... ]

Global Namespace - Globális névtér

Bármi, amit a futtatott programunkon definiáltunk.
Minden modulhoz készül egy ilyen.
A modulon belül bárhonnan elérjük az itt definiált dolgokat.

Local Namespace - Lokális névtér

Minden függvényen belül egy saját névteret készít a python. Tehát ha belül definiálunk egy változót, az csak azon a funkción belül lesz elérhető.

>>> def fv():
... a = 3
... if a == 1:
... b=2
... print(a)
... print(b)
...
>>> fv()
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in fv
UnboundLocalError: local variable 'b' referenced before assignment

Látható, hogy ennél a példánál még az if ágon belüli b is létezik, viszont nem kapott értéket és emiatt Error-t kaptunk.

Például egy nem létező változóval:

def fv2():
... a = 3
... if a == 1:
... b=2
... print(a)
... print(not_existing_variable)
...
>>> fv2()
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in fv2
NameError: name 'not_existing_variable' is not defined

Itt már azt a hibát kaptuk, hogy a változót nem is hoztuk létre.

Ezt úgy képzeld el, hogy a Python mikor sorról sorra végigment a kódon, akkor felépített minden funkción belül egy saját katalógust a változóknak és oda felírta a talált neveket. Viszont az értékeket csak akkor írta bele, ha ténylegesen kapott is.

Python osztály

Na és akkor nézzük meg, hogy hogyan néz ki egy osztály.

class OsztalyNev:
.
.
.

Hogy legyen is benne valami:

>>> class Osztalyom:
... """A simple example class""" # Dokumentáció
... i = 12345
... def f(self):
... return 'hello world'
...

Ebből tudunk egy példányt készíteni:

>>> x = Osztalyom() # Példányosítás
>>> x.i
12345

Ilyenkor meghívódott az alapértelmezett konstruktora.

Ha saját konstruktort szeretnénk, akkor a beépített __init__ függvényt tudjuk felhasználni.

A különböző objektumainknak megannyi ilyen beépített függvénye van egyébként. Például felültudjuk írni, hogy mi történjen, ha az objektumot összeszorozzuk valamivel, vagy mit írjon ki ha simán ki Printeljük és így tovább.
Ezeket Double Underscore, azaz "Dunder" metódusoknak hívják
Részletesebben

>>> class Osztalyom:
... """Egy példa osztály"""
... i = 12345 # Minden objektum által megosztott
... def f(self):
... return 'hello world'
... def __init__(self): #
... self.data = []

Egy érdekes különbség C-től, hogy a változóinkat nem definiáljuk közvetlen, hanem az init metódusában vesszük fel őket.
Ha ezt nem így tennénk, akkor valójában egy statikus minden objektum közt megosztott változót készítenénk.

A konstruktor metódusunkban feltűnhet az első paraméter. Ez az a változó, mely a jelenleg létrejövő objektumra mutat. Ha ehhez hozzáírunk új adatokat, akkor csak abban lesz benne.

>>> class Komplex:
... def __init__(self, valos, imaginarius):
... self.r = valos
... self.i = imaginarius
...
>>> x = Komplex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

Statikus változóval ilyesmi problémába üzközhetünk bele:

class Dog:

tricks = []

def __init__(self, name):
self.name = name

def add_trick(self, trick):
self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over', 'play dead']

Helyesen:

class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # Minden kutyánál saját lista lesz

def add_trick(self, trick):
self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

Erre a magyarázat, hogy ezekre az objektumokra csak mutatók készülnek. Tehát mikor mi definiáltuk az osztályunk akkor készítettünk egy új listát [], majd pedig a tricks változót rámutattuk a listánkra. Ezt követően mikor ebből új példányokat képeztünk, akkor ugyanazokat a mutató értékeket kapták az egyes leszármazottak.

Láthatóság

Pythonban az osztályokban a tagváltozók és függvények publikusan elérhetőek és nem lehet őket priváttá tenni. Erre egy konvenció, hogyha _-al kezdődik a nevük, akkor privátként kezeljük őket. Ekkor nem lesznek privátak, ugyanúgy publikok maradnak, viszont ha valaki hozzányúl a kódhoz, akkor nem fogja ezeket használni, legfeljebb Getter, Setteren keresztül.

class Lathatosag:
def __init__(self, privat_info):
self.privat_info = privat_info

def get_privat_info(self):
return self.privat_info

def set_privat_info(self, uj_ertek):
self.privat_info = uj_ertek

Öröklés

Mikor egy osztályból leöröklünk, akkor a gyerekosztály megjegyzi az ősét és később tudunk rá hivatkozni.
Az egyes függvények és változók elérhetőek a leszármazott osztály self-jében, viszont hogy pontosan melyik ősosztály függvényét hívjuk, ha egyezés van köztük az nem egyértelmű elsőre ha például több osztályunk is van.

Nézzük is meg!
(Osztálynév(Ősosztály, ...))

class A:
pass

class B(A):
pass

b = B() # Példányosítottuk
print(isinstance(b, A))
# isinstance metódus ellenőrzi, hogy leszármazottja-e
# az objektumunk A-nak

# Kimenet:
# True

Akár több osztályt is megadhatunk ősosztálynak.

Tegyünk bele pár konstruktort

class A:
def __init__(self):
print("A")

class B(A):
def __init__(self):
print("B")

Mit gondolsz, példányosításkor mi fog történni?

b = B()

# Kimenet:
# B

Nem futott le az A konstruktora. Ezt két féleképp tudjuk megoldani.

class A:
def __init__(self):
print("A")

class B(A):
def __init__(self):
A.__init__(self) # Explicit meghívjuk A konstruktorát
print("B")

b = B()

# Kimenet:
# A
# B

super

Másik megoldás, hogy használjuk a beépített super függvényt.

class A:
def __init__(self):
print("A")

class B(A):
def __init__(self):
super().__init__()
# ez super(B, self).init() -ként is írhatnánk
# alapértelmezetten az üres verzió python 3-ban
# (B, self)-re egészül
print("B")

b = B()

# Kimenet:
# A
# B

A super függvény ilyenkor egy úgynevezett metódus feloldási sorrendet követve végigjárja a megadott osztálytól kezdve az ősosztályokat és megkeresi a megfelelő függvényt, majd megáll.
Tehát a fenti példánál megtalálta A-ban az __init__ függvényt és abbahagyta a keresést, majd meghívta a függvényt.

A metódus feloldási sorrendünk ez esetben:

print(B.__mro__) # Method Resolution Order

# Kimenet:
# (<class '__main__.B'>,
# <class '__main__.A'>,
# <class 'object'>)

Mikor a super-t alapértelmezetten B-től indítottuk, akkor a következő A osztályban kereste meg az __init__ függvényt.

Azt is észrevehetted, hogy alapértelmezetten az osztályaink az object osztályból öröklődnek

Na de miért használjak super -t?

class A(object):
def __init__(self):
print('A.__init__(self) -t hívtunk')

class B(A):
def __init__(self):
print('B.__init__(self) -t hívtunk')
A.__init__(self) # Nem használtam Super-t

class C(A):
def __init__(self):
print('C.__init__(self) -t hívtunk')
super(C, self).__init__() # Használtam Supert

Ha netán úgy döntenénk, hogy ezekből az osztályokból leszeretnénk származni, de úgy, hogy A alá még helyezünk egy osztályt, így:

class Injektalt_Osztaly(A):
def __init__(self):
print('Injektalt_Osztaly.__init__(self) -t hívtunk')
super(Injektalt_Osztaly, self).__init__()

class B_bol(B, Injektalt_Osztaly):
pass

class C_bol(C, Injektalt_Osztaly):
pass

B_bol()
# Kimenet:
# B.__init__(self) -t hívtunk
# A.__init__(self) -t hívtunk

C_bol()
# Kimenet:
# C.__init__(self) -t hívtunk
# Injektalt_Osztaly.__init__(self) -t hívtunk
# A.__init__(self) -t hívtunk

Miért nem futott le a B_bol esetén?
Nézzük meg a metódus feloldási sorrendet

<class '__main__.B_bol'>,
<class '__main__.B'>,
<class '__main__.Injektalt_Osztaly'>,
<class '__main__.A'>,
<class 'object'>

B_bol osztály után meghívódott B konstruktora, mert az alapértelmezett konstruktora B-nek meghívja a super() függvényt.
Ezt követően B-ben a következő hívás A-ra fog mutatni. Ezt követően pedig a végére érünk és ha hívnánk újabb super-t, akkor már az object jönne.

Mikor a C_bol osztályból indultunk, akkor C osztályban a super függvény lefutott és az Injektalt_Osztaly-ban megtalálta a függvényünk.

Összefoglalva

Mikör öröklünk és a super függvényunk használjuk, akkor felépít egy metódus feloldási sorrendet attól függően, hogy hogyan definiáltuk az osztályainkat. Ezt követően mikor hívogatjuk a super-eket, akkor mindig a sorrendben következő fog jönni, kivéve ha átugorjuk, mert statikusan definiáljuk, hogy melyik a következő osztály.

A super-el akár más függvényeket is megtudunk így találni.