2.22. Arv

Arv låter en klass utvidga en annan klass – börja med alla dess metoder och attribut, lägg sedan till eller åsidosätt det som är annorlunda. Originalet kallas bas (eller förälder); utvidgningen kallas underklass.

En basklass Shape som pekar nedåt mot en underklass Square; Square åsidosätter en metod (area) och ärver resten.

Square återanvänder varje metod på Shape och åsidosätter de som den behöver specialisera.

2.22.1. Att definiera en underklass

Basklassen placeras inom parenteserna på class-raden:

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

    def describe(self):
        return self.name + " with area " + str(self.area())

    def area(self):
        return 0

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def area(self):
        return self.side * self.side

s = Square(4)
print(s.describe())

Utmatning:

square with area 16

Square ärver describe från Shape oförändrad och åsidosätter area för att returnera ett riktigt värde. Basens describe fungerar fortfarande eftersom den anropar self.area() – som vid körtid löses upp till åsidosättningen.

2.22.2. super()

super() returnerar en proxy som låter en metod anropa basklassens version av en metod. Den vanligaste användningen är att anropa basens __init__ från en underklass __init__ så att basen får initiera sina egna attribut:

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

Att glömma super().__init__ är en vanlig källa till buggar: attribut som basen skulle ha satt blir aldrig satta, och de ärvda metoderna som förlitar sig på dem kraschar senare.

2.22.2.1. Den implicita basklassen

Varje klass ärver implicit från object, roten i Pythons typhierarki. Metoder som __repr__, __eq__ och maskineriet bakom attributåtkomst kommer alla därifrån även när en klass inte deklarerar någon bas. Att skriva class Foo(object): explicit och att skriva class Foo: är likvärdigt i modern Python; den senare är den konventionella formen.

2.22.3. När arv hjälper – och när det inte gör det

Använd arv när en klass verkligen är en mer specifik sorts av en annan och delar det mesta av beteendet. Det klassiska testet är ”är-en”-kontrollen: en Square är en Shape.

När två klasser bara råkar dela några få hjälpfunktioner är arv överdrivet. Ta till komposition i stället: låt en klass hålla en instans av den andra som ett attribut och använda den genom det attributet. Formen är ”har-en”, inte ”är-en”:

class Logger:
    def log(self, msg):
        print("[log]", msg)

# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
    def run(self):
        self.log("starting")

# composition: Worker HAS-A Logger
class WorkerComposes:
    def __init__(self):
        self.logger = Logger()

    def run(self):
        self.logger.log("starting")

Båda fungerar. Den komponerande versionen är oftast bättre eftersom den:

  • Håller gränssnitten små. WorkerComposes exponerar endast run och logger. WorkerInherits exponerar också log – anropare kan skriva worker.log(...) direkt, oavsett om det var avsikten eller inte.

  • Frikopplar livslängder. Loggern kan bytas ut, delas mellan workers eller konstrueras olika per worker, allt utan att röra Worker-klassen. En underklass är fastbultad vid sin bas.

  • Undviker krockar mellan metodnamn. Två baser som båda definierar run krockar när en klass försöker ärva från båda; två attribut krockar aldrig.

En praktisk tumregel:

  • Använd arv när underklassen verkligen är en sorts av basen och varje metod på basen är meningsfull även på den – Square är en Shape; MemoryError är en Exception.

  • Använd komposition för allt annat – när du bara vill ha beteendet hos en annan klass, inte dess identitet. Den mesta verkliga koden använder komposition betydligt oftare än den använder arv.