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.

A base class Shape pointing down to a subclass Square; Square overrides one method (area) and inherits the rest.

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.