Tilbake
5.2

5.2 Spesialmetoder: `__str__`, `__add__`, `__mul__`

Gjøre objekter utskrivbare og la dem støtte `+` og `*` — med den kritiske regelen at aritmetiske spesialmetoder returnerer en **ny instans**.

55 min
6 oppgaver
Spesialmetoder`__str__``__add__``__mul__`
Din fremgang i kapitlet
0 / 6 oppgaver
Kapitlets plass i kurset
Forkunnskaper — sist du var her. Alt i dette kapitlet står på malen fra kap. 5.1. Her er de to konstruksjonene du trenger, oppfrisket ferdig:

class Rampe:
    def __init__(self, a):
        self.a = a

    def __call__(self, t):
        return self.a*t

r = Rampe(3)
print(r.a)
print(r(4))

Utskrift:

3
12

Det du skal ha med deg videre er tre setninger:

1. __init__ kjøres av Rampe(3) og lagrer det som er fast for objektet, med self.navn = navn.
2. __call__ kjøres av r(4) og tar imot det som varierer.
3. Ingen av dem kalles ved navn. Du skriver en form — parentes etter et klassenavn, parentes etter en instans — og Python velger metoden.

Nå utvider vi det siste punktet til tre nye former: å skrive ut objektet, å legge to objekter sammen, og å gange dem.

Løkke 1 — __str__: å gjøre objektet utskrivbart (~15 min)

Problemet: objektet ser ikke ut som noe

Du har laget en klasse Vec2D for vektorer i planet, med x og y i konstruktøren. Så skriver du print(u) og får dette:

<__main__.Vec2D object at 0x104ff01f0>

Utskriften sier hvilken klasse objektet tilhører og hvor i minnet det ligger. Det heksadesimale tallet er forskjellig hver gang du kjører programmet, og det har ingen faglig interesse.

Det du vil ha, er (3, 4). Og det får du ved å fortelle Python hvordan objektet skal gjøres om til tekst — det er jobben til __str__.

`def __str__(self):` — objektet som tekst

Spesialmetoden som sier hvordan instansen skal se ut når den skrives ut. Den tar ingen argumenter utenom self, og den skal returnere en streng — nesten alltid en f-streng.

class Vec2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f'({self.x}, {self.y})'

Nå gir print(u) teksten (3, 4). Merk return: metoden skriver ikke ut noe selv. Det er print som skriver ut; __str__ bare leverer teksten.

En vanlig eksamensvariant er at teksten skal ha et bestemt format — to desimaler, enheter, eller polynomform. Da er __str__ stedet der formateringen hører.

`print(obj)` og `str(obj)` kaller `__str__`

Begge formene utløser samme metode. print(u) gjør først str(u) for å få tekst, og str(u) kaller u.__str__().

Det betyr at __str__ også virker inne i en f-streng, siden f-strenger gjør om verdiene til tekst: print(f'vektoren er {u}') gir «vektoren er (3, 4)».

Det finnes én form som ikke bruker __str__, og den overrasker alle første gang: skriver du ut en liste av objekter, får du den tekniske utskriften tilbake. Grunnen er at lister ber om __repr__, ikke __str__, for hvert element.

✏️Eksempel 1: `Vec2D` med `__str__`

Skriv en klasse Vec2D for en vektor i planet, med x og y gitt i konstruktøren, og en __str__ som gir formen (x, y). Vis at utskriften virker både i print, i en f-streng og i strengaddisjon.

class Vec2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f'({self.x}, {self.y})'

u = Vec2D(3, 4)
v = Vec2D(-1, 0.5)

print(u)
print(v)
print(f'kraften virker langs {u} og har lengde {(u.x**2 + u.y**2)**0.5}')
print(str(u) + ' og ' + str(v))

Utskrift:

(3, 4)
(-1, 0.5)
kraften virker langs (3, 4) og har lengde 5.0
(3, 4) og (-1, 0.5)

Tre observasjoner å ta med:

- print(u) og str(u) gir samme tekst, fordi de går gjennom samme metode.
- Inne i f-strengen {u} blir objektet automatisk gjort om til tekst. Du trenger ikke skrive {str(u)}.
- -1 og 0.5 skrives ut nøyaktig slik Python skriver dem. Vil du ha (-1.00, 0.50), må du formatere inne i __str__, for eksempel med f'({self.x:.2f}, {self.y:.2f})'.

📝Oppgave 1

(Innstegsoppgave, sjanger G.) En værmåling består av en temperatur i grader celsius og en vindstyrke i meter per sekund.

a) Skriv en klasse Maaling der de to tallene gis i konstruktøren.

b) Legg til en __str__ som gir teksten 12.4 grader, 3.1 m/s — altså tallene med én desimal, i den rekkefølgen, med komma og mellomrom mellom.

c) Lag en instans og skriv den ut.

`__repr__` — den tekniske utskriften

Søskenmetoden til __str__. Der __str__ er den pene teksten til mennesker, er __repr__ den tekniske utskriften Python bruker når ingen har spurt om noe pent — for eksempel for elementene i en liste.

Definerer du ikke __repr__, får du standardformen med klassenavn og minneadresse. Derfor ser en liste av objekter slik ut, selv når klassen har en fin __str__:

[<__main__.Vec2D object at 0x10907dfd0>, <__main__.Vec2D object at 0x10907df70>]

Adressene varierer fra kjøring til kjøring. På eksamen er __repr__ ikke pensum å skrive — men du bør kjenne igjen utskriften og vite at den betyr «her ble __str__ ikke brukt». Vil du skrive ut en liste med pen tekst, løkker du over den og skriver ut ett objekt av gangen.

Løkke 2 — __add__: å legge to objekter sammen (~20 min)

Pluss er også et metodekall

To krefter som virker på samme punkt, skal legges sammen komponentvis:

u+v=(ux+vx,  uy+vy)\mathbf{u} + \mathbf{v} = (u_x + v_x,\; u_y + v_y)

Du kunne skrevet en metode pluss og kalt u.pluss(v). Men Python lar deg gjøre det bedre: definerer du spesialmetoden __add__, kan du skrive u + v, og koden ser ut som matematikken.

Mekanikken er enkel når du først har sett den: når Python møter u + v, oversetter den uttrykket til u.__add__(v). Venstre operand blir self, høyre operand blir det andre argumentet.

`def __add__(self, other):`

Spesialmetoden som utløses av plusstegnet. u + v betyr u.__add__(v): venstre operand er self, høyre er other.

Metoden skal returnere resultatet av addisjonen — den skal ikke endre self. Etter w = u + v er u og v uendret, og w er noe nytt.

def __add__(self, other):
    return Vec2D(self.x + other.x, self.y + other.y)

Legg merke til at metoden lager en instans av sin egen klasse ved å skrive klassenavnet med parentes. Det er lovlig inne i klassen, og det er nettopp det som gjør resultatet til en fullverdig vektor.

`other` — den andre operanden

Navnet man bruker på høyre operand i en aritmetisk spesialmetode. Det er ingen magi i navnet — def __add__(self, v): virker like godt — men other er konvensjonen, og sensor kjenner den igjen.

Inne i metoden har du tilgang til begge objektenes attributter: self.x er venstre operands x, og other.x er høyre operands. Det er hele grunnen til at du kan regne komponentvis.

En liten, men viktig detalj: other er antatt å være av samme klasse. Er den ikke det, får du en feilmelding fra innsiden av metoden, og det er ofte den feilmeldingen som avslører at du har blandet typer.

Operator-oversettelsen `a + b` \to `a.__add__(b)`

Regelen som gjør hele Løkke 2 og 3 forståelig: hver operator har en spesialmetode, og Python oversetter operatoren til et metodekall på venstre operand.

Du skriverPython kjører
u + vu.__add__(v)
u * vu.__mul__(v)
print(u)u.__str__()
u(3)u.__call__(3)
Vec2D(1, 2)__init__ på en ny instans

Mangler metoden, får du TypeError: unsupported operand type(s). Meldingen navngir operatoren og de to typene, og den er derfor lett å lese: den forteller deg presis hvilken spesialmetode du har glemt.

📜Aritmetisk spesialmetode \to ny instans

Dette er den regelen som skiller en full G-besvarelse fra en halv. Den lyder:

> En aritmetisk spesialmetode skal returnere en NY instans av klassen — ikke et tuppel, ikke en liste, og ikke self med endrede attributter.

Malen er tre linjer:

def __add__(self, other):
    ny_x = self.x + other.x
    ny_y = self.y + other.y
    return Vec2D(ny_x, ny_y)

eller, kortere og like bra:

def __add__(self, other):
    return Vec2D(self.x + other.x, self.y + other.y)

Det er to grunner til at returtypen betyr så mye:

1. Du kan bruke resultatet videre. u + v + w virker bare hvis u + v er en Vec2D, for da har resultatet selv en __add__. Returnerer du et tuppel, stopper kjeden med en gang.
2. Resultatet kan skrives ut pent og brukes som en vektor. Et tuppel har ingen __str__ fra klassen din, ingen __mul__, ingen lengdemetode.

Den ene lovlige unntaket i dette faget: __mul__ som regner indreprodukt skal returnere et tall, fordi indreproduktet av to vektorer er et tall. Alt annet returnerer en ny instans.

✏️Eksempel 2: Hvilken metode kjøres når — og i hvilken rekkefølge?

Det vanskelige med spesialmetoder er ikke syntaksen, men å holde styr på hva som kalles når. Én linje kode kan utløse tre metoder. La oss legge inn en utskrift i hver av dem og se rekkefølgen med egne øyne.

class Vec2D:
    def __init__(self, x, y):
        print('  __init__ med', x, y)
        self.x = x
        self.y = y

    def __str__(self):
        print('  __str__ kjorer')
        return f'({self.x}, {self.y})'

    def __add__(self, other):
        print('  __add__ kjorer')
        return Vec2D(self.x + other.x, self.y + other.y)

u = Vec2D(1, 2)
v = Vec2D(3, 4)
print('nå kommer u + v:')
print(u + v)

Utskrift:

  __init__ med 1 2
  __init__ med 3 4
nå kommer u + v:
  __add__ kjorer
  __init__ med 4 6
  __str__ kjorer
(4, 6)

Les de tre siste linjene i utskriften i rekkefølge, for de er hele svaret på «hva skjer i print(u + v)»:

1. __add__ kjøres først, med self = u og other = v.
2. Inne i __add__ står Vec2D(...) — så __init__ kjøres, og lager den nye instansen med 4 og 6.
3. Først når print har fått instansen, kjøres __str__ for å gjøre den om til tekst.

Legg merke til at __str__ bare kjøres én gang, på resultatet. u og v blir aldri skrevet ut her, og de er uendret etterpå.

Dette er sporingsferdigheten sjanger A tester på klasser: gå fra innsiden av uttrykket og utover, og spør ved hvert ledd hvilken form som står der. print-linjene inne i metodene skal selvsagt bort i en eksamensbesvarelse.

📝Oppgave 2

(Midtveisnivå, sjanger A — kodesporing.) Klassen under er kjørt ferdig, slik at a og b finnes.

class Kraft:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f'[{self.x} {self.y}]'

    def __add__(self, other):
        return Kraft(self.x + other.x, self.y + other.y)

    def __call__(self, k):
        return Kraft(k*self.x, k*self.y)

a = Kraft(2, 1)
b = Kraft(-1, 3)

a) For hvert av de fire uttrykkene a + b, a(3), a(2) + b og a.y: si hvilke spesialmetoder som kjøres, i rekkefølge.

b) Hva skriver disse fire linjene ut?

print(a + b)
print(a(3))
print(a(2) + b)
print(a.y)

Løkke 3 — __mul__: to betydninger av gangetegnet (~20 min)

Ganging er to forskjellige ting

For vektorer betyr gangetegnet to ulike ting, og oppgaveteksten avgjør hvilken du skal implementere:

Skalarmultiplikasjon — vektor ganget med et tall — strekker vektoren og gir en ny vektor:

cu=(cux,  cuy)c\,\mathbf{u} = (c\,u_x,\; c\,u_y)

Indreproduktet (også kalt skalarproduktet) — vektor ganget med vektor — gir et tall:

uv=uxvx+uyvy\mathbf{u}\cdot\mathbf{v} = u_xv_x + u_yv_y

Begge kan bo i samme __mul__. Da må metoden se på hva other er, og det gjør den med isinstance.

`def __mul__(self, other):`

Spesialmetoden som utløses av gangetegnet: u * v betyr u.__mul__(v). Som for __add__ er venstre operand self.

Returtypen avhenger av hva oppgaven ber om, og det er den ene plassen i faget der «ny instans»-regelen har et unntak:

- Skalarmultiplikasjon (u * 3) skal gi en ny instans.
- Indreprodukt (u * v) skal gi et tall.

def __mul__(self, other):
    return self.x*other.x + self.y*other.y

Les oppgaveteksten nøye her. Står det «returner indreproduktet», skal det ut et tall; står det «returner produktet av vektoren og et tall», skal det ut en vektor.

Indreprodukt (skalarprodukt)

Tallet du får ved å gange komponentvis og summere: uv=uxvx+uyvy\mathbf{u}\cdot\mathbf{v} = u_xv_x + u_yv_y i planet, og med ett ledd mer i rommet.

At resultatet er et tall og ikke en vektor, er hele grunnen til at indreproduktet er unntaket fra «ny instans»-regelen. Navnet «skalarprodukt» sier det samme: produktet er en skalar.

Indreproduktet brukes til å måle vinkel og til å projisere. I eksamenssammenheng er det først og fremst en presis liten formel som er lett å implementere feil: pass på at du summerer produktene, og ikke ganger summene.

`isinstance(other, Vec2D)`

Funksjonen som svarer på «er dette objektet av denne klassen?». isinstance(other, Vec2D) er True hvis other er en Vec2D-instans, ellers False.

I __mul__ er den nøkkelen til å håndtere begge betydningene av gangetegnet:

def __mul__(self, other):
    if isinstance(other, Vec2D):
        return self.x*other.x + self.y*other.y
    else:
        return Vec2D(other*self.x, other*self.y)

Nå gir u * v et tall, og u * 3 en ny vektor. Merk at rekkefølgen u * 3 er den som virker: 3 * u treffer heltallets eget gangetegn, ikke din __mul__, og gir en feilmelding.

isinstance kommer igjen i kap. 5.3, der den brukes til å svare på om et objekt hører til en klasse eller en subklasse av den.

✏️Eksempel 3: `Vec3D` med `__add__`, `__mul__` og `__str__` (eksamensnivå)

Skriv en klasse Vec3D for vektorer i rommet med __str__ på formen (x, y, z), __add__ som legger sammen komponentvis, og __mul__ som gir indreproduktet når høyre operand er en vektor, og en skalert vektor når den er et tall.

class Vec3D:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def __str__(self):
        return f'({self.x}, {self.y}, {self.z})'

    def __add__(self, other):
        return Vec3D(self.x + other.x, self.y + other.y, self.z + other.z)

    def __mul__(self, other):
        if isinstance(other, Vec3D):
            return self.x*other.x + self.y*other.y + self.z*other.z
        else:
            return Vec3D(other*self.x, other*self.y, other*self.z)

u = Vec3D(1, 2, 3)
v = Vec3D(4, -1, 0)

print(u + v)
print(u * v)
print(u * 2)
print(u * v + u * u)
print((u + v) * 2)

Utskrift:

(5, 1, 3)
2
(2, 4, 6)
16
(10, 2, 6)

Dette er en typisk full G-besvarelse, og den er kort — under tjue linjer. Gå gjennom hva som gir uttelling:

- __init__ med tre attributter. Grunnmuren; uten den faller resten.
- __add__ returnerer Vec3D(...). Ny instans, ikke tuppel. Se hvordan (u + v) * 2 på siste linje bare virker fordi resultatet av + selv er en vektor.
- __mul__ med isinstance-testen. De to grenene har hver sin returtype, og det er meningen.
- __str__ returnerer en f-streng.

Legg merke til uttrykket u * v + u * u. Her er begge produktene tall, så plusstegnet er vanlig tallpluss — din __add__ blir ikke brukt i det hele tatt. Det er den typen detalj sporingsoppgavene elsker.

Delvis riktig gir uttelling: har du __add__ og __str__ riktig, men bare indreproduktgrenen i __mul__, ligger du fortsatt godt an. Skriv i så fall en kort kommentar om at skalartilfellet håndteres i else-grenen — det viser at du kjenner strukturen.

📝Oppgave 3
Eksamensnivå, sjanger G

Et lager fører to varer, og en ordre er et par (antall_A, antall_B).

a) Skriv en klasse Ordre med de to antallene i konstruktøren og en __str__ som gir teksten 2 stk A og 5 stk B.

b) Legg til __add__ slik at to ordrer kan slås sammen til én ny ordre.

c) Legg til __mul__ slik at ordre * pris gir totalprisen som et tall, når pris er en Ordre-instans som inneholder prisen per vare. Forklar i én setning hvorfor returtypen her er et tall og ikke en Ordre.

📝Oppgave 4
Eksamensnivå, sjanger G

En værstasjon lagrer vinden som to komponenter: o mot øst og n mot nord, begge i meter per sekund.

a) Skriv en klasse Vind der de to komponentene gis i konstruktøren, og en __str__ som gir teksten 10.0 m/s øst, 5.0 m/s nord — begge tallene med én desimal.
b) Skriv én __mul__ som dekker begge betydningene av gangetegnet: v * tall skal gi en ny Vind der begge komponentene er ganget med tallet, mens v * w med to Vind-instanser skal gi indreproduktet som et tall.
c) Lag vinden (10,5)(10, 5) og retningsvektoren (0,6, 0,8)(0{,}6,\ 0{,}8), som peker langs en fjord og har lengde 1. Skriv ut vinden, vinden ganget med 1,5, og vindkomponenten langs fjorden. Hva skjer hvis du snur siste produkt til 1.5 * v, og hva gjør du med det på eksamen?

Løkke 4 — Ulik lengde: polynomer som legges sammen (~20 min)

Når de to objektene ikke har like mange tall

Vektorer i planet har alltid to komponenter, så __add__ var enkel. Polynomer er verre: 2+3x2 + 3x og 1x+4x21 - x + 4x^2 har ulik grad, og summen skal ha den høyeste graden av de to.

Et polynom lagres som en koeffisientliste, med konstantleddet først:

p(x)=i=0ncixi,koeff=[c0,c1,,cn]p(x) = \sum_{i=0}^{n} c_i x^i, \qquad \texttt{koeff} = [c_0, c_1, \ldots, c_n]

2+3x2 + 3x blir [2, 3] og 1x+4x21 - x + 4x^2 blir [1, -1, 4]. Summen skal bli [3, 2, 4].

Mønsteret som løser dette, er å finne ut hvilken liste som er lengst, kopiere den, og legge den korteste oppå ledd for ledd.

Koeffisientliste for et polynom

Måten boka lagrer polynomer: en liste der element ii er koeffisienten til xix^i. Konstantleddet står først.

[2, 0, -1] er altså 2x22 - x^2, og len(koeff) - 1 er graden.

Rekkefølgen er den viktige detaljen, og den er lett å snu feil vei. Kontrollen tar du i __call__: løkker du med enumerate over koeffisientene og summerer c*x**i, må konstantleddet ligge på indeks 0 for at regnestykket skal stemme.

Ulik lengde i `__add__`

Når de to objektene har forskjellig antall tall, må __add__ bygge et resultat som er like langt som den lengste. Mønsteret er tre steg:

1. Finn ut hvilken av de to listene som er lengst.
2. Lag en kopi av den, med lang[:] — ikke bare ny = lang, som ville gitt to navn på samme liste.
3. Løkk over den korteste og legg den oppå: ny[i] = ny[i] + kort[i].

Punkt 2 er feil #23 i bokas feilkatalog: to navn på samme liste. Uten kopien ville du endret koeffisientene i det ene av polynomene du la sammen, og det er en feil som er nesten umulig å finne etterpå.

✏️Eksempel 4: `Polynom` med `__call__`, `__str__` og `__add__` for ulik grad (eksamensnivå)

Skriv en klasse Polynom som lagrer koeffisientene i en liste med konstantleddet først. Klassen skal kunne evalueres i et punkt, skrives ut på polynomform, og legges sammen med et annet polynom av vilkårlig grad.

class Polynom:
    def __init__(self, koeff):
        self.koeff = koeff

    def __call__(self, x):
        s = 0
        for i, c in enumerate(self.koeff):
            s += c*x**i
        return s

    def __str__(self):
        s = ''
        for i, c in enumerate(self.koeff):
            if c == 0:
                continue
            if s != '':
                s += ' + '
            if i == 0:
                s += f'{c}'
            elif i == 1:
                s += f'{c}*x'
            else:
                s += f'{c}*x**{i}'
        if s == '':
            s = '0'
        return s

    def __add__(self, other):
        lang, kort = self.koeff, other.koeff
        if len(kort) > len(lang):
            lang, kort = kort, lang
        ny = lang[:]
        for i in range(len(kort)):
            ny[i] = ny[i] + kort[i]
        return Polynom(ny)

p = Polynom([2, 3])
q = Polynom([1, -1, 4])

print(p)
print(q)
print(p + q)
print(p.koeff)
print((p + q)(2))

Utskrift:

2 + 3*x
1 + -1*x + 4*x**2
3 + 2*x + 4*x**2
[2, 3]
23

Dette er en av de tyngste klasseoppgavene som stilles, og den er verdt å gå gjennom bit for bit:

- __call__ er rekke-summeringsmønsteret du kjenner: s = 0, løkke, s += .... enumerate gir deg indeksen og koeffisienten samtidig, og indeksen er nettopp eksponenten.
- __str__ hopper over ledd med koeffisient 0, og setter + mellom leddene bare når det allerede står noe i strengen. Det er en enkel måte å slippe et + på slutten.
- __add__ bytter om på lang og kort hvis det trengs, kopierer den lengste med lang[:], og legger den korteste oppå. Resultatet er en ny Polynom.

Se på nest siste linje: p.koeff er fortsatt [2, 3] etter addisjonen. Det er kopien lang[:] som sørger for det.

Og siste linje viser hvorfor returtypen er verdt kampen: (p + q)(2) — først __add__, så __call__ på resultatet. Hadde __add__ returnert en liste, ville dette vært en feilmelding.

Uttelling: riktig struktur (fire metoder med riktige navn og returtyper) bærer mesteparten av poengene. En besvarelse som håndterer ulik lengde galt, men ellers er riktig, gjør det fortsatt godt — mens en som returnerer en liste fra __add__, mister uttelling flere steder samtidig.

📝Oppgave 5

(Midtveisnivå, sjanger A — kodesporing.) Klassen Polynom fra eksempel 4 er definert. Hva skriver disse linjene ut?

p = Polynom([0, 0, 5])
q = Polynom([1, 2])
print(p)
print(q + p)
print(p(3))
print(len((q + p).koeff))

📝Oppgave 6
Eksamensnivå, sjanger G

Utvid Polynom fra eksempel 4.

a) Legg til __mul__ som ganger polynomet med et tall og returnerer et nytt polynom.

b) Legg til en metode derivert som returnerer et nytt Polynom — den deriverte. Husk at den deriverte av cixic_i x^i er icixi1i\,c_i x^{i-1}, så konstantleddet forsvinner.

c) Vis at p(x)=1+2x+3x2p(x) = 1 + 2x + 3x^2 ganget med 2 og deretter derivert gir 4+12x4 + 12x.

API- og konstruksjonsliste

Begrepsbank

Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.

Ny instans som returverdi

Kjernekravet i sjanger G: en aritmetisk spesialmetode lager et nytt objekt og returnerer det. Formen er return Klassenavn(...) inne i klassen selv.

Grunnen er ikke pynt. Returtypen bestemmer hva du kan gjøre med resultatet etterpå — skrive det ut pent, gange det, legge det til noe mer, sende det inn i en funksjon som venter et slikt objekt.

Det ene unntaket i faget er indreproduktet, som skal gi et tall. Da er tallet det matematisk riktige svaret, og en instans ville vært galt.

Kjeding: `u + v + w`

Uttrykket regnes fra venstre: først u + v, så resultatet pluss w. Det andre plusstegnet kaller __add__resultatet av det første.

Derfor er kjeding den beste egensjekken på returtypen din. Virker u + v + w, returnerte du en instans; feiler det med «unsupported operand type(s)», returnerte du noe annet.

Samme resonnement gjelder alle kombinasjoner: (u + v) * 2, (p + q)(2), print(u + v). Alle krever at det første leddet har levert et fullverdig objekt.

f-streng i `__str__`

Formateringsverktøyet du bruker inne i __str__. Prefikset f foran anførselstegnet gjør at alt i krøllparentes regnes ut og settes inn.

def __str__(self):
    return f'({self.x:.2f}, {self.y:.2f})'

Etter kolonet står formatspesifikasjonen: .2f gir to desimaler, 8.3f gir bredde 8 og tre desimaler, 3d gir et heltall i bredde 3.

Ber oppgaveteksten om et bestemt format — «skriv ut vektoren med to desimaler» — er det her det hører, ikke i print-kallet. Da får alle utskrifter av objektet samme form.

`__str__` må returnere en streng

Returnerer __str__ noe annet — et tuppel, en liste, et tall — får du TypeError: __str__ returned non-string, og feilen kommer først i det du skriver ut objektet.

Nabofeilen er å bruke print inne i __str__ i stedet for return. Da skriver metoden ut teksten selv, returnerer None, og print(obj) skriver None etterpå.

Vil du bygge teksten i flere steg, er det helt greit: lag en lokal streng, bygg den opp med +=, og returner den til slutt.

Skalarmultiplikasjon mot indreprodukt

De to betydningene av gangetegnet, og de skilles på typen til høyre operand:

- u * 3 — høyre operand er et tall. Resultatet er en ny vektor med alle komponenter ganget med 3.
- u * v — høyre operand er en vektor. Resultatet er ett tall, indreproduktet.

Skal klassen støtte begge, tester du med isinstance og har to grener i __mul__. Ber oppgaven bare om én av dem, skriver du bare den — sensor forventer kort kode, ikke maksimal fleksibilitet.

Venstre operand bestemmer

Python spør alltid venstre operand først når den møter en operator. u * 3 blir u.__mul__(3); 3 * u blir et spørsmål til heltallet 3, som ikke vet hva en Vec2D er.

Følgen i praksis: u * 3 virker, 3 * u gir TypeError: unsupported operand type(s) for *: 'int' and 'Vec2D'.

Regelen gjelder også for pluss. u + v bruker us __add__. Er u et tall og v et objekt, får du samme type feilmelding.

Aritmetikk endrer ikke operandene
w = u + v skal lage et tredje objekt og la u og v være i fred. Det er slik tall og strenger oppfører seg, og klassen din bør oppføre seg likt.

Fellen ligger i lister: skriver du ny = self.koeff og deretter endrer ny, endrer du samtidig self.koeff, fordi de to navnene peker på samme liste. Kopien self.koeff[:] er det som holder dem fra hverandre.

En rask kontroll når du har skrevet __add__: skriv ut operandene etterpå. Er de uendret, er du trygg.

Lister av objekter bruker ikke `__str__`
print(u) gir den pene teksten, men print([u, v]) gir den tekniske: klassenavn og minneadresse for hvert element.

Grunnen er at listen selv gjøres om til tekst, og listen ber om __repr__ for hvert element — ikke __str__.

Vil du skrive ut en liste av objekter pent, løkker du over den og skriver ut ett objekt av gangen:

class Vec2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f'({self.x}, {self.y})'

for e in [Vec2D(1, 2), Vec2D(3, 4)]:
    print(e)

Utskrift:

(1, 2)
(3, 4)

Da er det __str__ som brukes, én gang per objekt.

`TypeError: unsupported operand type(s)`

Feilmeldingen når operatoren mangler en spesialmetode. Den er blant de mest lesbare i Python, fordi den navngir både operatoren og de to typene: «for +: 'Vec2D' and 'Vec2D'» betyr at klassen mangler __add__.

Bruk meldingen som en diagnose:

- Navngir den din klasse på begge sider, mangler spesialmetoden.
- Navngir den 'tuple' eller 'list' på én side, har en tidligere metode returnert feil type — nesten alltid feil #4.
- Navngir den 'int' først, står tallet på feil side av operatoren.

Å formatere et polynom i `__str__`

Mønsteret for å skrive 1 + 2*x + 5*x**2 fra en koeffisientliste, med tre valg som må tas bevisst:

1. Hopp over nullerif c == 0: continue. Ellers får du 0*x med i teksten.
2. Skilletegn før neste ledd, ikke etter — legg til + bare hvis strengen ikke er tom. Da slipper du et hengende plusstegn til slutt.
3. Tre former for eksponenten — indeks 0 gir bare tallet, indeks 1 gir c*x, resten gir c*x**i.

Oppgaveteksten avgjør det eksakte formatet. Blir det ikke helt likt fasitens, koster det lite — det er strukturen i __str__ som gir uttellingen.

Repetisjon — fem spørsmål før du går videre

Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.

Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.