Start | klasser arv
 

Klasser: Arv



Denna beskrivning fokuserar på att beskriva grunderna i tänket och hur det fungerar praktiskt. Hur man ska jobba för att analysera i större projekt, dvs objektorienterad analys, metoder m.m. det är en annan story som kräver en annan ansats. Nedan fokuserar främst på hur arv i python fungerar på kodnivå.

poängen med arv

återanvändning av kod


Så, poängen med arv är bland annat att slippa uppfinna hjulet på nytt. Genom att ärva från existerande klasser behövs enbart tid läggas på att specialisera det som saknas och det kan ärvas kan återanvändas.

återspeglar verkligheten


Arv introducerar en hierarkisk struktur i designen som bättre återspeglar verkligheten bakom hur system hänger ihop. Det normala är att många olika processer har en gemensam kärna och specialiserade varianter och med en klass-hierarki kan data i programmet bättre återspegla hur saker fungerar.

lättare underhålla


Samtidigt innebär detta att koden blir mindre och enklare att underhålla, man slipper samma information på fler ställen. En ändring behövs bara göras på ett ställe. Detta bidrar också till färre fel.

arv i python

Syntaxen är rätt så enkel. I nedan är A en basklass (superklass). B ärver från A och C ärver från B.
class A: pass class B(A): pass class C(B): pass
I C, nedan, finns alla 3 metoder eftersom C ärver från B och A. Så vi kan skapa en instans av C och utan problem anropa både hejB() och hejA().
class A: def hejA(self): print("A") class B(A): def hejB(self): print("B") class C(B): def hejC(self): print("C") x = C() x.hejA() x.hejB() x.hejC()
Som du ser när vi skapat en instans av C, så har vi fri tillgång till metoderna från B och A pga C ärver allt från B och A. Vad händer om metoderna har samma namn (överlagrade)?
class A: def hej(self): print("A") class B(A): def hej(self): print("B") class C(B): def hej(self): print("C") x = C() x.hej()
Då anropas den metod från den mest specialiserade klassen, dvs C. Om metoden hade saknats i klassen C, då hade metoden från B anropats. Se nedan.
class A: def hej(self): print("A") class B(A): def hej(self): print("B") class C(B): pass x = C() x.hej()
Ibland vill man nå en metod i en högre klass. Det är möjligt med super().
class A: def hej(self): print("A") class B(A): def hej(self): print("B") super().hej() class C(B): def hej(self): print("C") super().hej() x = C() x.hej()
Den mest specialiserade klassen C kommer anropas först, men hej() i C kommer anropa hej() i klassen ovanför, som också kommer anropa hej() i klassen ovanför.

Vilken initiering kommer köras nedan? Svaret är C, det är ju faktiskt objektet C vi skapar. C kommer ärva alla eventuella metoder och attribut från B och A, med det finns ingen logik i att köra __init__ från super -klasserna.
class A: def __init__(self): print("A") class B(A): def __init__(self): print("B") class C(B): def __init__(self): print("C") x = C()
Det kan ju dock hända att vi VILL köra __init__ från superklasserna. Ja, då får vi anropa den med super().
class A: def __init__(self): print("A") class B(A): def __init__(self): print("B") super().__init__() class C(B): def __init__(self): print("C") super().__init__() x = C()
Så, ett mer begripbart exempel kan se ut såhär.
class Rektangel: def __init__(self, b, h): self.bredd = b self.hojd = h def area(self): return(self.bredd * self.hojd) class Kvadrat(Rektangel): def __init__(self, s): super().__init__(s, s) r = Rektangel(10,5) print(r.area()) k = Kvadrat(5) print(k.area())

exempel: elever och lärare


Tänk dig en skola, där springer det omkring massor av personer. Vissa är elever, några är lärare, några bara råkar vara där, på studiebesök.

Alla personer har ett namn. Eller hur? Så om du ber någon presentera sig, så kan de säga sitt namn. Ta en lärare. Det är en person, så läraren har ett namn. Men läraren undervisar också i ett ämne. Elever är också personer, så de har ett namn. Men de går också i en klass. Detta är en typisk klass-hierarki, därgemensam information är namn och sedan finns olika specialiseringar, antingen ämne eller klass.





Vi skapar nedan en klass Person. Denna klass innehåller personens namn samt en metod pres() där personen presenterar sig med namnet.

Därefter skapar vi 2 specialiseringar, Elev respektive Lärare. Eleven presenterar sig även med sin klass och läraren med sitt ämne.
class Person: def __init__(self, namn): self.namn = namn def pres(self): return("Jag heter "+self.namn) class Elev(Person): def __init__(self, namn, klass): super().__init__(namn) self.klass = klass def pres(self): return(super().pres()+", elev i " +self.klass) class Larare(Person): def __init__(self, namn, amne): super().__init__(namn) self.amne = amne def pres(self): return(super().pres()+", lärare i "+self.amne) personer = [Larare("Kalle","Matte"), Elev("Anna","T1C"), Elev("Emma","IT1"), Person("Niklas"), Larare("Olle","Fysik") ] for p in personer: print(p.pres())
Du ser att det är enkelt att skapa en lista med lite olika personer och se hur lätt det är att skapa en loop där alla presenterar sig! Poängen med denna hierarki är att den är lätt bygga vidare på i alla tänkbara riktningar. Den är också ganska lätt att förstå, när man förstått syntaxen.

geometriska figurer


I dataspel eller datorer allmänt kanske man vill representera grafiska figurer på ett smart sätt. Gemensamt för alla geometriska figurer är att de behöver en koordinat (x,y) där figuren skall ritas ut. Men de olika typerna av figurer har sedan olika behov av data, en cirkel bestäms av en radie medans en rektangel beskrivs av höjd och bredd.



Nedan skapar vi metoden rita(). Den rita som anropas är den som finns i den specialiserade klassen. Man säger att funktionen rita() överlagras, dvs ersätter rita() från lägre nivåer. Som du märker, så anropas aldrig rita() i basklassen. Tur det, för basklassen har ingen aning vad den ska rita. Basklassen vet bara x,y.
class Form: def __init__(self, x, y): self.xpos = x self.ypos = y def rita(self): print("jag vet inte vad jag ska rita") class Cirkel(Form): def __init__(self, x, y, r): super().__init__(x, y) self.radie = r def rita(self): print("ritar cirkel") class Rektangel(Form): def __init__(self, x, y, h, b): super().__init__(x, y) self.hojd = h self.bredd = b def rita(self): print("ritar rektangel") shapes = [ Rektangel(11,11,5,10), Rektangel(11,11,5,10), Cirkel(12,12,8), Cirkel(12,12,8) ] for s in shapes: s.rita()
Ja, vi kan skoja till det lite och faktiskt rita riktiga geometriska figurer. För att rita med canvas i en webbläsare måste man först skapa ett canvasobjekt, det är det som kallas ctx. Därefter fungerar det så att vi börjar rita genom att anropa ctx.beginPath() och vi avslutar med ctx.stroke(). Klassen Render ritar med metoden rend(). Där loopar den igenom alla shapes och anropar metoden rita() för varje shape, som i sin tur ritar objektet.
import math from browser import document as doc class Render: def __init__(self, shapes): self.shapes = shapes self.canvas = doc["canvaz21"] self.canvas.width = 300 self.canvas.height = 250 self.ctx = self.canvas.getContext('2d') self.ctx.strokeStyle = 'rgba(100, 100, 100, 1)' self.ctx.lineWidth=2 self.ctx.clearRect(0, 0, self.canvas.width, self.canvas.height) def rend(self): for s in self.shapes: self.ctx.beginPath() s.rita(self.ctx) self.ctx.stroke() class Form: def __init__(self, x, y): self.x, self.y = x, y class Cirkel(Form): def __init__(self, x, y, r): super().__init__(x, y) self.radie = r def rita(self, c): c.arc(self.x,self.y,self.radie,0,2*math.pi); class Rektangel(Form): def __init__(self, x, y, h, b): super().__init__(x, y) self.h, self.b = h, b def rita(self, c): c.moveTo(self.x, self.y) c.lineTo(self.x, self.y+self.h) c.lineTo(self.x+self.b, self.y+self.h) c.lineTo(self.x+self.b, self.y) c.lineTo(self.x, self.y) class Triangel(Form): def __init__(self, x, y, h, b): super().__init__(x, y) self.h, self.b = h, b def rita(self, c): c.moveTo(self.x, self.y) c.lineTo(self.x, self.y+self.h) c.lineTo(self.x+self.b, self.y+self.h) c.lineTo(self.x, self.y) shapes = [ Rektangel(20,20,80,80), Triangel(120,30,50,120), Rektangel(190,150,20,20), Cirkel(60,60,18), Cirkel(140,140,40) ] r = Render(shapes) r.rend()
Klasshierarkin kan även handla om en hierarki matematiska relationer ...

exempel växelströmskretsar


På sidan om dundermetoder i python skapade vi en klass komplexa tal. Den ska vi använda nu!

I fysiken har du säkert lärt dig räkna på kretsscheman. Seriekoppling och parallellkoppling. Dock har du nog bara räknat på "enkla" likströmskretsar. Det är dock inte svårare räkna på växelströmskretsar. Man kan använda samma metoder som i likströmsläran, för växelström, om man räknar med komplexa tal.

Så vi skapar en klass elektronikkomponenter och i denna klass lägger vi in parallellkoppling och seriekoppling. Vi använder dundermetoden __mod__ för parallellkoppling (dvs %) och överlagrar __add__ för seriekoppling (dvs +), för att få rätt datatyp.

Vi skapar sedan klasserna resistans (motstånd), induktans (spolar) och kapacitans (kondensatorer) och dessa ärver i sin tur från elektronikkomponenter, så att de går att serie och parallell -koppla. De kommer inte innehålla något förutom att de vid instansiering räknar ut reaktans och sätter det komplexa talet.




I nedan schema (se schemat under koden) är kondensatorn 636 µF och spolen är 9.6 mH. Uppgiften är då att beräkna strömmen (vars facit redan står i kretsschemat, då bilden kommer från krets-simulatorn) om spänningen är 240 volt.
import math # för tangens och PI class komplex: def __init__(self,r,i): self.r = r self.i = i def __str__(self): return("{:+f} {:+f}i".format(self.r, self.i)) def __add__(self,other): return(komplex(self.r+other.r,self.i+other.i)) def __sub__(self,other): return(komplex(self.r-other.r,self.i-other.i)) def __mul__(self,other): return(komplex( self.r*other.r-self.i*other.i, self.r*other.i+self.i*other.r)) def __invert__(self): konj = komplex(self.r,-self.i) namn = self.r**2+self.i**2 return(komplex(konj.r/namn, konj.i/namn)) def __rtruediv__(self,other): return(self.__invert__()*other) def __abs__(self): return((self.r**2+self.i**2)**0.5) def arg(self): alfa = math.atan(self.i/self.r) return(math.degrees(alfa)) class elektronikkomponenter(komplex): # parallellkoppling av komponenter med % def __mod__(self, other): this = komplex(self.r,self.i) inv = ~(~this + ~other) return(elektronikkomponenter(inv.r, inv.i)) # seriekoppling av komponenter med + def __add__(self,other): return(elektronikkomponenter(self.r+other.r,self.i+other.i)) class resistans(elektronikkomponenter): def __init__(self,r): super().__init__(r,0) class induktans(elektronikkomponenter): def __init__(self,L,f): X = 2*math.pi*f*L super().__init__(0,X) class kapacitans(elektronikkomponenter): def __init__(self,C,f): X = 1/(2*math.pi*f*C) super().__init__(0,-X) class spanning(elektronikkomponenter): def __init__(self,U): super().__init__(U, 0) frekvens = 50 # + är seriekoppling z1 = resistans(4) + induktans(0.00955,frekvens) z2 = resistans(10) z3 = resistans(12) + kapacitans(0.000636,frekvens) # % är parallellkoppling ztot = z1%z2%z3 i = spanning(240)/ztot print("i =",i) print("Abs(i) =",abs(i)) print("Arg =",i.arg())
Det är alltså detta kopplings-schema vi räknar på. Frekvensen är 50 Hz. Svaret ska bli drygt 82A och 15 grader fasvinkel ifall spänningen är 240 volt.



Kopplingen finns i en elkrets simulator ifall du vill dubbelkolla.

Lite offtopic (elära) för dig som är intresserad: Det som skiljer de 3 komponentslagen ovan ifrån varandra om vi räknar med komplexa tal, är att resistorer har en ren resistans, dvs ingen reaktans, enbart real-del. Induktorer saknar realdel och har istället en positiv imaginärdel (induktans). Kapacitanser har istället en negativ imaginärdel (kapacitans). Räknar vi på detta sätt med komplexa tal, så gäller serie och parallell -kopplingsgrejerna från likströmsläran. Vi kan lägga till formlerna för reaktansen direkt i koden, dvs XL=2πwL och XC=1/(2πwC), för respektive induktor och kondensator och kan då ge dessa komponenters värden samt frekvensen ifråga.

Lite frågor

Betrakta följande
class A: pass class B(A): pass class C(B): pass
Säg att vi skapar en instans av B, vad av nedan gäller för denna instans då?
  Vi kan nå alla publika metoder och attribut i A, B och C
  Vi kan nå alla publika metoder och attribut i A och B
  Vi kan nå alla publika metoder och attribut i B
18.149852752686 ms