Czy dziedziczenie Pythona jest stylem dziedziczenia „czy jest”, czy stylem kompozytorskim?

10

Biorąc pod uwagę, że Python pozwala na wielokrotne dziedziczenie, jak wygląda dziedziczenie idiomatyczne w Pythonie?

W językach z pojedynczym dziedziczeniem, takich jak Java, dziedziczenie byłoby stosowane, gdy można by powiedzieć, że jeden obiekt „jest-a” innego obiektu i chcesz współdzielić kod między obiektami (od obiektu nadrzędnego do obiektu podrzędnego). Na przykład można powiedzieć, że Dogjest to Animal:

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

Ale ponieważ Python obsługuje wielokrotne dziedziczenie, możemy utworzyć obiekt, tworząc wiele innych obiektów razem. Rozważ poniższy przykład:

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

Czy jest to dopuszczalne lub dobre wykorzystanie wielokrotnego dziedziczenia w Pythonie? Zamiast mówić, że dziedziczenie ma miejsce, gdy jeden obiekt „jest-a” innego obiektu, tworzymy Usermodel złożony z UserServicei LoggingService.

Całą logikę operacji bazy danych lub sieci można trzymać osobno od Usermodelu, umieszczając je w UserServiceobiekcie i zachowując całą logikę logowania LoggingService.

Widzę pewne problemy z tym podejściem:

  • Czy to tworzy obiekt Boga? Skoro Userdziedziczy lub składa się z niego UserServicei LoggingServiceczy rzeczywiście przestrzega zasady pojedynczej odpowiedzialności?
  • Aby uzyskać dostęp do metod na obiekcie nadrzędnym / następnym w linii (np. UserService.validate_credentialsMusimy użyć super. To sprawia, że ​​nieco trudniej jest zobaczyć, który obiekt będzie obsługiwał tę metodę i nie jest tak jasny, jak powiedzmy , tworzenie instancji UserServicei robienie czegoś takiegoself.user_service.validate_credentials

Jaki byłby Pythoniczny sposób na implementację powyższego kodu?

Iain
źródło

Odpowiedzi:

9

Czy dziedziczenie Pythona jest stylem dziedziczenia „czy jest”, czy stylem kompozytorskim?

Python obsługuje oba style. Demonstrujesz relację składu, w której użytkownik ma funkcję rejestrowania z jednego źródła i weryfikację referencji z innego źródła. LoggingServiceI UserServicezasadami są wstawek: zapewniają funkcjonalność i nie są przeznaczone do wystąpienia same.

Komponując miksy w tym typie, masz użytkownika, który może się logować, ale musi dodać własną funkcję tworzenia instancji.

Nie oznacza to, że nie można trzymać się pojedynczego dziedzictwa. Python też to obsługuje. Jeśli twoja zdolność do rozwoju jest utrudniona przez złożoność wielokrotnego dziedziczenia, możesz tego uniknąć, dopóki nie poczujesz się bardziej komfortowo lub nie osiągniesz punktu, w którym uważasz, że jest to warte kompromisu.

Czy to tworzy obiekt Boga?

Rejestrowanie wydaje się nieco styczne - Python ma własny moduł rejestrujący z obiektem rejestrującym, a konwencja polega na tym, że dla każdego modułu jest jeden rejestrator.

Ale odłóż moduł logowania na bok. Być może narusza to odpowiedzialność pojedynczą, ale być może w twoim konkretnym kontekście kluczowe znaczenie ma zdefiniowanie użytkownika. Odpowiedzialność deskretyzacyjna może być kontrowersyjna. Ale ogólniejsza zasada mówi, że Python pozwala użytkownikowi podjąć decyzję.

Czy super jest mniej jasne?

superjest konieczne tylko wtedy, gdy musisz delegować do elementu nadrzędnego w metodzie Order Resolution Order (MRO) z wnętrza funkcji o tej samej nazwie. Najlepszym rozwiązaniem jest używanie go zamiast twardego kodowania wywołania metody rodzica. Ale jeśli nie zamierzasz na sztywno kodować rodzica, nie potrzebujesz super.

W twoim przykładzie musisz tylko to zrobić self.validate_credentials. selfz twojej perspektywy nie jest bardziej jasne. Oboje podążają za MRO. Po prostu użyłbym każdego z nich w razie potrzeby.

Gdybyś zadzwonił authenticate, validate_credentialsmusiałbyś użyć super(lub zakodować kod nadrzędny), aby uniknąć błędu rekurencji.

Sugestia alternatywnego kodu

Zakładając, że semantyka jest OK (jak rejestrowanie), chciałbym zrobić w klasie 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
Aaron Hall
źródło
1
Nie zgadzam się. Dziedziczenie zawsze tworzy kopię publicznego interfejsu klasy w jej podklasach. To nie jest związek typu „ma”. Jest to podtytuły, proste i proste, a zatem nieodpowiednie dla opisywanej aplikacji.
Jules
@Jules Z czym się nie zgadzasz? Powiedziałem wiele rzeczy, które można udowodnić, i wyciągnąłem logiczne wnioski. Ci nieprawidłowe, gdy mówisz „Dziedziczenie zawsze tworzy kopię publicznego interfejsu klasy w jego podklasy.” W Pythonie nie ma kopii - metody są wyszukiwane dynamicznie zgodnie z kolejnością rozwiązywania metod algorytmu C3 (MRO).
Aaron Hall
1
Nie chodzi o konkretne szczegóły działania implementacji, ale o to, jak wygląda publiczny interfejs klasy. W tym przykładzie Userobiekty mają w swoich interfejsach nie tylko elementy zdefiniowane w Userklasie, ale także elementy zdefiniowane w UserServicei LoggingService. Nie jest to relacja typu „ nie ma”, ponieważ interfejs publiczny jest kopiowany (choć nie przez bezpośrednie kopiowanie, ale raczej przez pośrednie wyszukiwanie interfejsów superklas).
Jules
Ma-a oznacza kompozycję. Mixiny są formą Kompozycji. Klasa Użytkownik jest nie UserService lub LoggingService, ale ma tę funkcjonalność. Myślę, że dziedzictwo Pythona różni się bardziej niż Java, niż się wydaje.
Aaron Hall
@AaronHall Zbytnio upraszczasz (to może zaprzeczać innej odpowiedzi , którą znalazłem przypadkiem). Z punktu widzenia relacji podtypu użytkownik jest jednocześnie usługą użytkownika i usługą rejestrowania. Teraz duch polega na takim komponowaniu funkcjonalności, aby użytkownik miał takie i takie funkcje. Mixiny na ogół nie wymagają implementacji z wielokrotnym dziedziczeniem. Jest to jednak zwykły sposób na wykonanie tego w Pythonie.
Coredump
-1

Poza faktem, że pozwala na wiele nadklas, dziedziczenie Pythona nie różni się zasadniczo od Java, tzn. Członkowie podklasy są również członkami każdego z ich nadtypów [1]. Fakt, że Python używa pisania kaczego, również nie ma znaczenia: twoja podklasa ma wszystkie elementy swoich nadklas, więc może być używana przez dowolny kod, który mógłby używać tych podklas. Fakt, że wielokrotne dziedziczenie jest skutecznie wdrażane przy użyciu kompozycji, jest czerwonym śledziem: problemem jest automatyczne kopiowanie właściwości jednej klasy do drugiej, i nie ma znaczenia, czy używa ona kompozycji, czy po prostu magicznie zgaduje, w jaki sposób członkowie powinni do pracy: posiadanie ich jest złe.

Tak, narusza to pojedynczą odpowiedzialność, ponieważ dajesz swoim przedmiotom możliwość wykonywania działań, które nie są logicznie częścią tego, do czego zostały zaprojektowane. Tak, tworzy obiekty „boga”, co jest zasadniczo innym sposobem na powiedzenie tego samego.

Podczas projektowania systemów obiektowych w Pythonie obowiązuje również ta sama maksyma, którą głoszą podręczniki projektowe Java: wolą kompozycję niż dziedziczenie. To samo dotyczy (większości [2]) innych systemów z wielokrotnym dziedziczeniem.

[1]: można nazwać to relacją „jest-a”, chociaż osobiście nie podoba mi się ten termin, ponieważ sugeruje ideę modelowania świata rzeczywistego, a modelowanie obiektowe nie jest tym samym co świat rzeczywisty.

[2]: Nie jestem pewien co do C ++. C ++ obsługuje „prywatne dziedziczenie”, które jest zasadniczo kompozycją bez konieczności określania nazwy pola, gdy chcesz użyć publicznych elementów odziedziczonej klasy. W ogóle nie wpływa to na publiczny interfejs klasy. Nie lubię go używać, ale nie widzę żadnego dobrego powodu, aby tego nie robić .

Jules
źródło