Atributi objektov

Sestavimo si dva razreda. B je izpeljan iz A. A ima metodo f in B ima metodo g.

class A:
    def f(self, x):
        return 2 * x

class B(A):
    def g(self, x):
        return x + 42

x = B()
y = B()

x.janez = "novak"

Zdaj imamo

>>> x.janez
'novak'
>>> x.g(42)
49
>>> x.f(42)
84
>>> b.__str__
<method-wrapper '__str__' of B object at 0x10d7d0c50>

Vsaka vrstica je drugačna - vsak atribut se potegne od drugod.

  • x.janez pripada objektu x. Tja smo ga postavili, ko smo rekli x.janez = "novak".

  • Ko napišemo x.g Python pogleda v x. Vidi, da je tam le janez. Vendar ne obupa. Ker je x objekt razreda B, pogleda, ali se v imenskem prostoru Bja nemara nahaja kak g. Se. x.g je torej

    >>> x.g
    <bound method B.g of <__main__.B object at 0x103e41320>>
    

    O tem bomo še kaj rekli, vendar najprej končajmo tole.

  • Ko napišemo x.f Python pogleda v x. Tam je le janez. Nato pogleda v B, vendar fja tudi tam ni. Zdaj pogleda, iz katerega razreda je izpeljan B. Vidi, da iz A, torej gre gledat tja. Tam res najde f.

    >>> x.f
    <bound method A.f of <__main__.B object at 0x105dc02e8>>
    

    V izpisu lahko vidimo, da gre za Ajev f objekta B. Da, natančno o tem bomo še kaj rekli.

  • Ko napišemo x.__str__ - metodo, ki, vemo, skrbi za izpis xa, Python pogleda v x (kjer je ni), v B (kjer je ni) in v A (kjer je ni). Nato pogleda, iz katerega razreda je izpeljan A. Navidez iz nobenega, v resnici pa so vsi razredi, ki niso izpeljani iz nobenega drugega, izpeljani iz razreda object. Izpis tu sicer ni povsem jasen:

    >>> x.__str__
    <method-wrapper '__str__' of B object at 0x10641e0b8>
    

    V resnici se v ozadju dogaja še nekaj, o čemer bi se tule težko pogovarjali, a __str__ je v bistvu metoda objecta.

Kar smo videli do sem, je snov Programiranja 1. Za tiste, ki jih moti, da niso izvedeli nič novega, dodajmo le še, da nam je Python voljan povedati, kje bo iskal metode objektov razreda B.

>>> B.__mro__
(<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

Najprej bo pogledal v B, nato A, nato object. Razred bool je - spomnimo se - izpeljan iz int. Česar ni specifično definiral bool, Python išče v int.

>>> bool.__mro__
(<class 'bool'>, <class 'int'>, <class 'object'>)

Če imamo bolj zapleteno hierarhijo razredov, je MRO še bolj zanimiv

class D:
    pass

class E:
    pass

class F(D, E):
    pass

class G(F, int):
    pass

Tule sta F in G izpeljana iz dveh razredov; vrstni red iskanja Gjevih metod je

>>> G.__mro__
(<class '__main__.G'>, <class '__main__.F'>, <class '__main__.D'>,
<class '__main__.E'>, <class 'int'>, <class 'object'>)

Atributi razredov

V gornjih izpisih smo opazili nekaj zanimivega.

>>> x.g
<bound method B.g of <__main__.B object at 0x103e41320>>

Kaj je B.g? Tudi razredi so objekti. Torej imajo tudi razredi lahko atribute. B, recimo, ima atribut g.

>>> B.g
<function B.g at 0x10d7ca488>

Kaj se dogaja tule, bomo lažje razumeli, če nekoliko dopolnimo definiciji razredov.

class A:
    baz = "Ana"

    def f(self, x):
        return 2 * x

class B(A):
    foo = 42
    bar = 13

    def g(self, x):
        return x + 42

Zdaj ima B tri stvari: "spremenljivki" foo in bar ter funkcijo g. Resno.

>>> B.foo
42
>>> B.bar
13
>>> B.g
<function B.g at 0x10d7ca510>

A ima baz in f.

>>> A.baz
'Ana'
>>> A.f
<function A.f at 0x10d7ca598>

Vse, kar ima A, ima tudi B.

>>> B.baz
'Ana'
>>> B.f
<function A.f at 0x10d7ca598>

Zadnji izpis pove, da je B.f Ajeva funckija. Seveda, gre za eno in isto reč. Kdor ne verjame, naj vpraša.

>>> B.f is A.f
True

Zdaj naredimo še objekt razreda B in se poigrajmo z njim.

>>> b = B()
>>> b.foo
42

b ima atribut foo; dobil ga je iz B. Nas to čudi? Ne bi nas smelo (več). Razredi so očitno podobni modulom. Podobni so majhnim programom. Vse, kar sestavimo v njih, je del razreda in tako vidno objektom tega razreda.

class B(A):
    import math

    foo = 42
    bar = 13
    def g(self, x):
        return x + 42

Nenavadno, vendar - zdaj ima razred B pač tudi modul math.

>>> B.math
<module 'math' from '/Users/janez/env/o3/lib/python3.5/lib-dynload/math.cpython-35m-darwin.so'>

Če ga ima razred, ga imajo tudi objekti tega razreda.

>>> b = B()
>>>
>>> b.math.sqrt(42)
6.48074069840786

Tega seveda ne počnemo - ne vem, zakaj bi. Pokazal sem samo, da bi videli mehanizem, kako reč deluje.

Metode torej niso nič drugega kot funkcije, ki so znotraj razredov. No ja, z malo dodatne magije. Sestavimo razred s funkcijo f, ki množi podani argument s faktorjem, ki ga določimo v konstruktorju.

class C:
    def __init__(self, faktor):
        self.faktor = faktor

    def f(self, x):
        return self.faktor * x

Preverimo.

>>> u = C(5)
>>> u.faktor
5
>>> u.f(12)
60

Kot mora biti. Objekt u ima atribut faktor, ker smo ga določili v konstruktorju, in metodo f, ker jo je dobil od Cja.

Če lahko do funkcije f pridemo tudi prek C.f (to je ena in ista reč, mar ni?) - jo lahko tudi pokličemo kar direktno?

>>> C.f(12)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required positional argument: 'x'

Kaj gre narobe, bomo še bolj jasno videli, če pokličemo C.f brez argumentov.

>>> C.f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 2 required positional arguments: 'self' and 'x'

Python pove, da hoče f dva argumenta, namreč self in x. Kako?! Saj vendar nikoli nismo podajali self kot argument. Tudi u.f(12) je delovalo - pa smo dali le en argument, namreč 12. Odgovor je tule:

>>> C.f
<function C.f at 0x10d7ca7b8>
>>> u.f
<bound method C.f of <__main__.C object at 0x10d7d6be0>>

C.f je funkcija, ki zahteva dva argumenta, self in x. Ko do taiste funkcije pridemo prek u.f, pa Python pripne prvi argument. Sestavi novo funkcijo, v kateri je self že postavljen na u, zato ji manjka le še en argument, namreč x. Klic u.f(12) je torej ekvivalenten klicu C.f(u, 12). Resnično,

>>> C.f(u, 12)
60

Glejte tole:

>>> s = [1, 2, 3]
>>> s.append
<built-in method append of list object at 0x10d7d43c8>
>>> list.append
<method 'append' of 'list' objects>

Objekt s je tipa list. Zato je s.append v bistvu list.append, vendar s pripetim selfom.

>>> s.append(4)
>>> list.append(s, 5)
>>> s
[1, 2, 3, 4, 5]

C.f ni vezan na katerikoli objekt (v starejših verzijah Pythona so ga imenovali unbound method), u.f pa je f vezan na u (zato bound method).

Kaj nam to koristi?

Kako deluje, je vedno koristno vedeti. Nikoli ne veš, kdaj ti pride prav. Poleg tega, je kako deluje lahko zabavno. Pokažimo, da je tudi uporabno.

Tule bo le nekaj primerov. Zares imenitno pa bo to postalo prihodnje leto, ko bomo delali zapletene reči, ki bodo, če bomo poznali te in podobne trike, zaradi tega nekoliko preprostejše.

Nevezane metode so lahko uporabne

Imamo seznam nizov.

>>> t = ["ABCDEF", "Def", "BeCD"]

Radi bi naredili seznam taistih nizov z malimi črkami. Uporabili bomo map (čeprav bi lahko tudi izpeljane sezname).

>>> list(map(lambda x: x.lower(), t))
['abcdef', 'def', 'becd']

Je res potrebno pisati lambdo, lambda x: x.lower(). Ne, rečemo lahko kar

>>> list(map(str.lower, t))
['abcdef', 'def', 'becd']

Če je x nek niz, je x.lower metoda niza x, ki bo vrnila (prav ta in le ta) x z malimi črkami. Poklicati jo moramo brez argumentov (edini argument, self, je že vezan).

str.lower pa je nevezana metoda. Podali ji bomo niz in dobili taisti niz z malimi črkami.

>>> str.lower("Janez Novak")
'janez novak'

Če premapiramo ves t čez str.lower, dobimo vse te nize z malimi črkami.

Kako bi uredili te nize?

>>> sorted(t)
['ABCDEF', 'BeCD', 'Def']

Sem rekel po abecedi? Ne, mislil sem po dolžini; se opravičujem. Vemo, vemo: sorted sprejema argument key, ki mu lahko podamo funkcijo, po kateri naj primerja nize. Podati je potrebno funkcijo, ki vrne dolžino niza, lambda x: len(x).

>>> sorted(t, key=lambda x: len(x))
['Def', 'BeCD', 'ABCDEF']

Po tem, kar smo se naučili prejšnjič in danes, vemo, da je to nepotrebno kompliciranje. Funkcija, ki vrača dolžino niza, je str.__len__. Torej

>>> sorted(t, key=str.__len__)
['Def', 'BeCD', 'ABCDEF']

Neuporabno? Kdo pa hoče urejati nize po dolžini? Kaj pa tole?

>>> t = ["aBCDEF", "Def", "BeCD"]
>>> sorted(t)
['BeCD', 'Def', 'aBCDEF']

Od kdaj je "a" po abecedi za "B" in "C"? Odkar so - za Python in bolj ali manj vse druge jezike tudi - male črke po abecedi za velikimi. Če hočemo, da Python ne dela razlik med malimi in velikimi črkami, mu recimo, naj nize primerja glede na to, kako so videti, če jih zapišemo z malimi črkami.

>>> sorted(t, key=str.lower)
['aBCDEF', 'BeCD', 'Def']

Hočemo prvi niz po abecedi - ne glede na velike in male črke?

>>> min(t, key=str.lower)
'aBCDEF'

Skupni atributi

Sestavimo takle razred.

class A:
    argumenti = []

    def f(self, x):
        self.argumenti.append(x)
        return 2 * x

In dva objekta.

>>> a = A()
>>> b = A()
>>> a.s = []
>>> b.s = []

Oba, a in b imata zdaj seznam s in seznam argumenti.

>>> a.s
[]
>>> a.argumenti
[]

Če v a.s dodamo 42, bo b.s ostal, kar je bil. Jasno. Tako mora biti.

>>> a.s.append(42)
>>> b.s
[]

Seznam argumenti pa ni v a ali b temveč v A. Vsak objekt ima svoj s, vsi pa imajo skupen argumenti.

>>> a.s is b.s
False
>>> a.argumenti is b.argumenti
True

Aha:

>>> a.argumenti.append("Tine")
>>> b.argumenti
['Tine']

To izkorišča metoda f, ki shranjuje vse argumente, s katerimi je bila kdajkoli poklicana.

>>> a.f(13)
26
>>> a.f("Ana")
'AnaAna'
>>> b.f(17)
34

In zdaj poglejmo spisek:

>>> A.argumenti
['Tine', 13, 'Ana', 17]

Pogledali smo ga prek razreda A. Seveda bi ga lahko tudi prek posameznega objekta,

>>> a.argumenti
['Tine', 13, 'Ana', 17]

vendar je argumenti stvar razreda, pa ga zato tudi glejmo prek razreda.

__rmul__ = __mul__

To prirejanje smo uporabili, ko smo definirali razred Vector:

class Vector:
    # Najprej nekaj drugih metod, nato pa.

    def __mul__(self, other):
        if isinstance(other, Vector):
            return sum(x * y for x, y in zip(self.values, other.values))
        else:
            return Vector(*[x * other for x in self.values])

    __rmul__ = __mul__

Zdaj razumemo tudi tole, ne? Če je razred kot majhen program, lahko v njem prosto računamo.

class A:
    from math import sqrt

    a = 42
    b = sqrt(a + 7)
    t = sqrt

Če zdaj naredimo objekt tega (ne preveč uporabnega) razreda, ima vse, kar smo sestavili v tem razredu.

>>> a = A()
>>> a.a
42
>>> a.b
7.0
>>> a.t
<built-in function sqrt>
>>> a.t(25)
5.0

Tisti __rmul__ = __mul__ le naredi novo spremenljivko (ki jo zunaj vidimo kot atribut) __rmul__ in ki pomeni isto kot __mul__.

Privzete vrednosti atributov

Sestavimo drugačen množilnik. Namesto, da bi nastavljal faktor v konstruktorju, bo nastavljen kar kot atribut razreda C.

class C:
    faktor = 1

    def f(self, x):
        return self.faktor * x

Sestavimo dva objekta.

>>> c = C()
>>> d = C()

Obe imata atribut faktor - gre kar za C-jev faktor.

>>> c.faktor
1
>>> d.faktor
1

Metoda f, jasno, deluje.

>>> c.f(12)
12
>>> d.f(12)
12

Zdaj pa postavimo c.faktor.

>>> c.faktor = 5
>>> c.f(12)
60
>>> d.f(12)
12

Ko metoda f išče self.faktor, najprej pogleda v self, šele nato v C. Kadar je self c, bo self.faktor cjef faktor. Ta je 5, zato dobimo 60. Ko je self enak d, pa Python ugotovi, da d nima faktorja, zato ga išče (in najde) v razredu C.

Zadnja sprememba: nedelja, 17. april 2016, 13.01