L'eredità di Python è uno stile di eredità "is-a" o uno stile compositivo?


10

Dato che Python consente l'ereditarietà multipla, che aspetto ha l'ereditarietà idiomatica in Python?

In linguaggi con ereditarietà singola, come Java, l'ereditarietà verrebbe utilizzata quando si potrebbe dire che un oggetto "è-a" di un altro oggetto e si desidera condividere il codice tra gli oggetti (dall'oggetto padre all'oggetto figlio). Ad esempio, potresti dire che Dogè un Animal:

public class Animal {...}
public class Dog extends Animal {...}

Ma poiché Python supporta l'ereditarietà multipla, possiamo creare un oggetto componendo insieme molti altri oggetti. Considera l'esempio seguente:

class UserService(object):
    def validate_credentials(self, username, password):
        # validate the user credentials are correct
        pass


class LoggingService(object):
    def log_error(self, error):
        # log an error
        pass


class User(UserService, LoggingService):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        if not super().validate_credentials(self.username, self.password):
            super().log_error('Invalid credentials supplied')
            return False
         return True

È un uso accettabile o buono dell'ereditarietà multipla in Python? Invece di dire l'eredità è quando un oggetto "è-a" di un altro oggetto, creiamo un Usermodello composto da UserServicee LoggingService.

Tutta la logica per le operazioni di database o di rete può essere mantenuta separata dal Usermodello inserendoli UserServicenell'oggetto e mantenendo tutta la logica per l'accesso LoggingService.

Vedo alcuni problemi con questo approccio sono:

  • Questo crea un oggetto Dio? Dal momento che Usereredita o è composto da, UserServicee LoggingServicesta davvero seguendo il principio della singola responsabilità?
  • Per accedere ai metodi su un oggetto parent / next-in-line (ad esempio, UserService.validate_credentialsdobbiamo usare super. Questo rende un po 'più difficile vedere quale oggetto gestirà questo metodo e non è chiaro come, diciamo , istanziando UserServicee facendo qualcosa di simileself.user_service.validate_credentials

Quale sarebbe il modo Pythonic per implementare il codice sopra?

Risposte:


9

L'eredità di Python è uno stile di eredità "is-a" o uno stile compositivo?

Python supporta entrambi gli stili. Stai dimostrando la relazione has-a di composizione, in cui un utente ha la funzionalità di registrazione da una fonte e la convalida delle credenziali da un'altra fonte. Le basi LoggingServicee UserServicesono mixin: forniscono funzionalità e non intendono essere istanziate da sole.

Componendo i mixin nel tipo, si ha un utente che può accedere, ma che deve aggiungere la propria funzionalità di istanza.

Ciò non significa che non si possa attenersi alla singola eredità. Anche Python lo supporta. Se la tua capacità di sviluppo è ostacolata dalla complessità dell'eredità multipla, puoi evitarlo fino a quando non ti senti più a tuo agio o giungi a un punto del design in cui ritieni che valga la pena fare un compromesso.

Questo crea un oggetto Dio?

La registrazione sembra in qualche modo tangenziale: Python ha un proprio modulo di registrazione con un oggetto logger e la convenzione è che esiste un logger per modulo.

Ma metti da parte il modulo di registrazione. Forse questo viola la singola responsabilità, ma forse, nel tuo contesto specifico, è fondamentale per definire un Utente. Descrivere la responsabilità può essere controverso. Ma il principio più ampio è che Python consente all'utente di prendere la decisione.

Super è meno chiaro?

superè necessario solo quando è necessario delegare a un genitore nell'ordine di risoluzione del metodo (MRO) dall'interno di una funzione con lo stesso nome. Usarlo invece di codificare in modo rigido una chiamata al metodo del genitore è una buona pratica. Ma se non avessi intenzione di codificare il genitore, non hai bisogno super.

Nel tuo esempio qui, devi solo fare self.validate_credentials. selfnon è più chiaro, dal tuo punto di vista. Entrambi seguono l'MRO. Vorrei semplicemente usare ciascuno dove appropriato.

Se invece avessi chiamato authenticate, validate_credentialsavresti dovuto usare super(o codificare il genitore) per evitare un errore di ricorsione.

Suggerimento di codice alternativo

Quindi, supponendo che la semantica sia OK (come la registrazione), quello che vorrei fare è, in classe User:

    def validate_credentials(self): # changed from "authenticate" to 
                                    # demonstrate need for super
        if not super().validate_credentials(self.username, self.password):
            # just use self on next call, no need for super:
            self.log_error('Invalid credentials supplied') 
            return False
        return True

1
Non sono d'accordo. L'ereditarietà crea sempre una copia dell'interfaccia pubblica di una classe nelle sue sottoclassi. Questa non è una relazione "has-a". Questo è un sottotipo, chiaro e semplice, e quindi non è appropriato per l'applicazione descritta.
Jules,

@Jules Con cosa non sei d'accordo? Ho detto molte cose che sono dimostrabili e ho tratto conclusioni che logicamente seguono. Non si è corretti quando si dice "L'ereditarietà crea sempre una copia dell'interfaccia pubblica di una classe nelle sue sottoclassi". In Python non esiste alcuna copia: i metodi vengono cercati dinamicamente in base all'ordine di risoluzione del metodo dell'algoritmo C3 (MRO).
Aaron Hall,

1
Il punto non riguarda i dettagli specifici su come funziona l'implementazione, ma su come appare l'interfaccia pubblica della classe. Nel caso dell'esempio, gli Useroggetti hanno nelle loro interfacce non solo i membri definiti nella Userclasse, ma anche quelli definiti in UserServicee LoggingService. Questa non è una relazione "has-a", perché l'interfaccia pubblica viene copiata (anche se non tramite la copia diretta, ma piuttosto da una ricerca indiretta alle interfacce delle superclassi).
Jules,

Has-a significa Composizione. I mixin sono una forma di composizione. La classe User non è un UserService o LoggingService, ma ha quella funzionalità. Penso che l'eredità di Python sia più diversa da quella di Java di quanto pensiate.
Aaron Hall,

@AaronHall Stai semplificando troppo (questo tende a contraddirti con un'altra risposta , che ho trovato per caso). Dal punto di vista della relazione di sottotipo, un utente è sia un servizio utente che un servizio di registrazione. Ora, lo spirito qui è quello di comporre funzionalità in modo che un utente abbia tali e tali funzionalità. I mixin in generale non richiedono l'implementazione con ereditarietà multipla. Tuttavia, questo è il solito modo di farlo in Python.
coredump,

-1

A parte il fatto che consente più superclassi, l'eredità di Python non è sostanzialmente diversa da quella di Java, ovvero i membri di una sottoclasse sono anche membri di ciascuno dei loro supertipi [1]. Il fatto che Python utilizzi la tipizzazione anatra non fa alcuna differenza: la tua sottoclasse ha tutti i membri delle sue superclassi, quindi può essere utilizzata da qualsiasi codice che potrebbe usare quelle sottoclassi. Il fatto che l'ereditarietà multipla sia effettivamente implementata utilizzando la composizione è un'aringa rossa: il problema è la copia automatizzata delle proprietà di una classe in un'altra, e non importa se utilizza la composizione o indovina magicamente come dovrebbero essere i membri lavorare: averli è sbagliato.

Sì, questo viola la singola responsabilità, perché stai dando ai tuoi oggetti la possibilità di eseguire azioni che non fanno logicamente parte di ciò per cui sono progettate. Sì, crea oggetti "divini", che è essenzialmente un altro modo di dire la stessa cosa.

Quando si progettano sistemi orientati agli oggetti in Python, si applica anche la stessa massima predicata dai libri di progettazione Java: preferire la composizione all'eredità. Lo stesso vale per (la maggior parte [2]) altri sistemi con ereditarietà multipla.

[1]: potresti definire una relazione "is-a", sebbene non mi piaccia personalmente il termine perché suggerisce l'idea di modellare il mondo reale e la modellazione orientata agli oggetti non è la stessa del mondo reale.

[2]: Non sono così sicuro di C ++. Il C ++ supporta "eredità privata" che è essenzialmente composizione senza la necessità di specificare un nome campo quando si desidera utilizzare i membri pubblici della classe ereditata. Non influisce affatto sull'interfaccia pubblica della classe. Non mi piace usarlo, ma non vedo alcuna buona ragione per non farlo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.