A herança do Python é um estilo de herança "is-a" ou um estilo de composição?

10

Dado que o Python permite herança múltipla, como é a herança idiomática no Python?

Em linguagens com herança única, como Java, a herança seria usada quando se poderia dizer que um objeto "é-a" de outro objeto e você deseja compartilhar código entre os objetos (do objeto pai ao objeto filho). Por exemplo, você poderia dizer que Dogé um Animal:

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

Porém, como o Python suporta herança múltipla, podemos criar um objeto compondo muitos outros objetos juntos. Considere o exemplo abaixo:

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

Esse é um uso aceitável ou bom de herança múltipla em Python? Em vez de dizer que herança é quando um objeto "é-a" de outro objeto, criamos um Usermodelo composto por UserServicee LoggingService.

Toda a lógica para operações de banco de dados ou rede pode ser mantida separada do Usermodelo, colocando-as no UserServiceobjeto e mantendo toda a lógica para efetuar login no LoggingService.

Vejo alguns problemas com essa abordagem:

  • Isso cria um objeto de Deus? Uma vez que Userherda, ou é composta por, UserServicee LoggingServiceestá realmente seguindo o princípio da responsabilidade única?
  • Para acessar métodos em um objeto pai / próxima linha (por exemplo, UserService.validate_credentialstemos que usar super. Isso torna um pouco mais difícil ver qual objeto vai lidar com esse método e não é tão claro quanto, por exemplo, , instanciando UserServicee fazendo algo comoself.user_service.validate_credentials

Qual seria a maneira Pythonic de implementar o código acima?

Iain
fonte

Respostas:

9

A herança do Python é um estilo de herança "is-a" ou um estilo de composição?

O Python suporta os dois estilos. Você está demonstrando o relacionamento de composição tem-um, em que um usuário possui funcionalidade de log de uma fonte e validação de credencial de outra fonte. As bases LoggingServicee UserServicesão mixins: elas fornecem funcionalidade e não devem ser instanciadas por elas mesmas.

Ao compor mixins no tipo, você tem um Usuário que pode Logar, mas que deve adicionar sua própria funcionalidade de instanciação.

Isso não significa que não se possa manter a herança única. O Python também suporta isso. Se sua capacidade de desenvolver for prejudicada pela complexidade da herança múltipla, você poderá evitá-la até se sentir mais confortável ou chegar a um ponto no design em que acredite que valha a pena.

Isso cria um objeto de Deus?

O registro parece um pouco tangencial - o Python possui seu próprio módulo de registro com um objeto de registro e a convenção é que existe um registro por módulo.

Mas reserve o módulo de registro de lado. Talvez isso viole a responsabilidade única, mas talvez, no seu contexto específico, seja crucial para definir um Usuário. Descrever a responsabilidade pode ser controverso. Mas o princípio mais amplo é que o Python permite que o usuário tome a decisão.

Super é menos claro?

supersó é necessário quando você precisa delegar a um pai na Ordem de Resolução de Método (MRO) de dentro de uma função com o mesmo nome. Usá-lo em vez de codificar uma chamada para o método dos pais é uma prática recomendada. Mas se você não codificava o pai, não precisa super.

No seu exemplo aqui, você só precisa fazer self.validate_credentials. selfnão é mais claro, da sua perspectiva. Ambos seguem o MRO. Eu simplesmente usaria cada um, quando apropriado.

Se você tivesse telefonado authenticate, validate_credentialsteria que usar super(ou codificar o pai) para evitar um erro de recursão.

Sugestão de código alternativo

Então, supondo que a semântica esteja correta (como o log), o que eu faria é na 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
Aaron Hall
fonte
1
Discordo. A herança sempre cria uma cópia da interface pública de uma classe em suas subclasses. Este não é um relacionamento "tem um". Isso é subtipo, puro e simples e, portanto, é inadequado para o aplicativo descrito.
Jules
@Jules Com o que você não concorda? Eu disse muitas coisas que são demonstráveis ​​e tirei conclusões que seguem logicamente. Você está incorreto quando diz: "A herança sempre cria uma cópia da interface pública de uma classe em suas subclasses". No Python, não há cópia - os métodos são pesquisados ​​dinamicamente de acordo com a ordem de resolução de método do algoritmo C3 (MRO).
Aaron Hall
1
O ponto não é sobre detalhes específicos de como a implementação funciona, mas sobre como é a interface pública da classe. No caso do exemplo, os Userobjetos têm em suas interfaces não apenas os membros definidos na Userclasse, mas também os definidos em UserServicee LoggingService. Este não é um relacionamento "tem-a", porque a interface pública é copiada (embora não via cópia direta, mas sim por uma pesquisa indireta nas interfaces das superclasses).
Jules
Has-a significa Composição. Mixins são uma forma de composição. A classe User não é um UserService ou LoggingService, mas possui essa funcionalidade. Eu acho que a herança do Python é mais diferente da do Java do que você imagina.
Aaron Hall
@AaronHall Você está simplificando demais (isso tende a contradizer outra resposta que eu achei por acaso). Do ponto de vista do relacionamento de subtipo, um Usuário é um UserService e um LoggingService. Agora, o espírito aqui é compor funcionalidades para que um Usuário tenha tais e tais funcionalidades. Os mixins em geral não precisam ser implementados com herança múltipla. No entanto, esta é a maneira usual de fazer isso em Python.
Coredump 23/05
-1

Além do fato de permitir várias superclasses, a herança do Python não é substancialmente diferente da Java, ou seja, membros de uma subclasse também são membros de cada um de seus supertipos [1]. O fato de o Python usar a digitação com patos também não faz diferença: sua subclasse possui todos os membros de suas superclasses, portanto, pode ser usada por qualquer código que possa usar essas subclasses. O fato de a herança múltipla ser efetivamente implementada usando a composição é um problema: a cópia automatizada das propriedades de uma classe para outra é o problema, e não importa se ela usa composição ou apenas adivinha magicamente como os membros devem trabalhar: tê-los está errado.

Sim, isso viola a responsabilidade única, porque você está dando aos seus objetos a capacidade de executar ações que não fazem parte lógica do que eles foram projetados para fazer. Sim, ele cria objetos "divinos", que é essencialmente outra maneira de dizer a mesma coisa.

Ao projetar sistemas orientados a objetos em Python, a mesma máxima que é pregada pelos livros de design Java também se aplica: prefira composição a herança. O mesmo acontece com (a maioria [2]) outros sistemas com herança múltipla.

[1]: você poderia chamar isso de um relacionamento "é-a", embora eu pessoalmente não goste do termo porque sugere a idéia de modelar o mundo real, e modelagem orientada a objetos não é a mesma que o mundo real.

[2]: Não tenho tanta certeza sobre C ++. O C ++ suporta "herança privada", que é essencialmente composição, sem a necessidade de especificar um nome de campo quando você deseja usar os membros públicos da classe herdada. Não afeta a interface pública da classe. Não gosto de usá-lo, mas não vejo nenhuma boa razão para não fazê- lo.

Jules
fonte