¿Es la herencia de Python un estilo de herencia "es-a" o un estilo compositivo?

10

Dado que Python permite la herencia múltiple, ¿cómo es la herencia idiomática en Python?

En lenguajes con herencia única, como Java, la herencia se usaría cuando se pudiera decir que un objeto "es-a" de otro objeto y se desea compartir el código entre los objetos (desde el objeto primario hasta el objeto secundario). Por ejemplo, podría decir que Doges un Animal:

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

Pero dado que Python admite herencia múltiple, podemos crear un objeto componiendo muchos otros objetos juntos. Considere el siguiente ejemplo:

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

¿Es este un uso aceptable o bueno de la herencia múltiple en Python? En lugar de decir que la herencia es cuando un objeto "es-a" de otro objeto, creamos un Usermodelo compuesto por UserServicey LoggingService.

Toda la lógica para las operaciones de la base de datos o la red se puede mantener separada del Usermodelo al colocarlas en el UserServiceobjeto y mantener toda la lógica para iniciar sesión en el LoggingService.

Veo algunos problemas con este enfoque son:

  • ¿Esto crea un objeto de Dios? ¿Ya que Userhereda de, o está compuesto de, UserServicey LoggingServicerealmente está siguiendo el principio de responsabilidad única?
  • Para acceder a los métodos en un objeto principal / siguiente en línea (por ejemplo, UserService.validate_credentialstenemos que usarlo super. Esto hace que sea un poco más difícil ver qué objeto va a manejar este método y no es tan claro como, por ejemplo, , instanciando UserServicey haciendo algo comoself.user_service.validate_credentials

¿Cuál sería la forma Pythonic de implementar el código anterior?

Iain
fuente

Respuestas:

9

¿Es la herencia de Python un estilo de herencia "es-a" o un estilo compositivo?

Python admite ambos estilos. Está demostrando la relación de composición has-a, donde un usuario tiene la funcionalidad de registro de una fuente y la validación de credenciales de otra fuente. Las bases LoggingServicey UserServiceson mixins: proporcionan funcionalidad y no están destinadas a ser creadas por ellos mismos.

Al componer mixins en el tipo, tiene un usuario que puede iniciar sesión, pero que debe agregar su propia funcionalidad de creación de instancias.

Esto no significa que uno no pueda apegarse a una sola herencia. Python también lo admite. Si su capacidad de desarrollo se ve obstaculizada por la complejidad de la herencia múltiple, puede evitarla hasta que se sienta más cómodo o llegue a un punto en el diseño en el que cree que vale la pena.

¿Esto crea un objeto de Dios?

El registro parece algo tangencial: Python tiene su propio módulo de registro con un objeto registrador, y la convención es que hay un registrador por módulo.

Pero ponga el módulo de registro a un lado. Quizás esto viola la responsabilidad individual, pero quizás, en su contexto específico, es crucial para definir un Usuario. Descretizar la responsabilidad puede ser controvertido. Pero el principio más amplio es que Python permite al usuario tomar la decisión.

¿Está súper menos claro?

supersolo es necesario cuando necesita delegar a un padre en la Orden de resolución de método (MRO) desde el interior de una función del mismo nombre. Usarlo en lugar de codificar una llamada al método de los padres es una práctica recomendada. Pero si no iba a codificar al padre, no lo necesita super.

En su ejemplo aquí, solo necesita hacer self.validate_credentials. selfNo está más claro, desde su perspectiva. Ambos siguen el MRO. Simplemente usaría cada uno cuando sea apropiado.

Si hubiera llamado authenticate, validate_credentialsdebería haber utilizado super(o codificar el padre) para evitar un error de recursión.

Sugerencia de código alternativo

Entonces, suponiendo que la semántica esté bien (como el registro), lo que haría es, en clase 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
fuente
1
Estoy en desacuerdo. La herencia siempre crea una copia de la interfaz pública de una clase en sus subclases. Esta no es una relación "tiene-a". Este es un subtipo, simple y llanamente, y por lo tanto es inapropiado para la aplicación descrita.
Julio
@Jules ¿Con qué no estás de acuerdo? Dije muchas cosas que son demostrables e hice conclusiones que lógicamente siguen. Usted es incorrecto cuando dice: "La herencia siempre crea una copia de la interfaz pública de una clase en sus subclases". En Python, no hay copia: los métodos se buscan dinámicamente de acuerdo con el orden de resolución del método del algoritmo C3 (MRO).
Aaron Hall
1
El punto no se trata de detalles específicos de cómo funciona la implementación, se trata de cómo se ve la interfaz pública de la clase. En el caso del ejemplo, los Userobjetos tienen en sus interfaces no solo los miembros definidos en la Userclase, sino también los definidos en UserServicey LoggingService. Esta no es una relación "tiene-a", porque la interfaz pública se copia (aunque no mediante copia directa, sino mediante una búsqueda indirecta en las interfaces de las superclases).
Julio
Has-a significa Composición. Las mixinas son una forma de composición. La clase de usuario no es UserService o LoggingService, pero tiene esa funcionalidad. Creo que la herencia de Python es más diferente de la de Java de lo que crees.
Aaron Hall
@AaronHall Estás simplificando demasiado (esto tiende a contradecir tu otra respuesta , que encontré por casualidad). Desde el punto de vista de la relación de subtipo, un usuario es tanto un servicio de usuario como un servicio de registro. Ahora, el espíritu aquí es componer funcionalidades para que un usuario tenga tales y tales funcionalidades. Los mixins en general no requieren ser implementados con herencia múltiple. Sin embargo, esta es la forma habitual de hacerlo en Python.
coredump
-1

Aparte del hecho de que permite múltiples superclases, la herencia de Python no es sustancialmente diferente a la de Java, es decir, los miembros de una subclase también son miembros de cada uno de sus supertipos [1]. El hecho de que Python use duck-typing tampoco hace ninguna diferencia: su subclase tiene todos los miembros de sus superclases, por lo que puede ser utilizada por cualquier código que pueda usar esas subclases. El hecho de que la herencia múltiple se implemente de manera efectiva mediante el uso de la composición es un error: que el problema es la copia automatizada de las propiedades de una clase a otra, y no importa si usa composición o simplemente adivina mágicamente cómo se supone que los miembros para trabajar: tenerlos está mal.

Sí, esto viola la responsabilidad individual, porque le está dando a sus objetos la capacidad de realizar acciones que no son lógicamente parte de lo que están diseñados para hacer. Sí, crea objetos "dios", que es esencialmente otra forma de decir lo mismo.

Al diseñar sistemas orientados a objetos en Python, también se aplica la misma máxima que se predica en los libros de diseño de Java: prefiera la composición a la herencia. Lo mismo es cierto para (la mayoría [2]) otros sistemas con herencia múltiple.

[1]: se podría llamar a eso una relación "es-a", aunque personalmente no me gusta el término porque sugiere la idea de modelar el mundo real, y el modelado orientado a objetos no es lo mismo que el mundo real.

[2]: No estoy tan seguro acerca de C ++. C ++ admite "herencia privada", que es esencialmente composición sin necesidad de especificar un nombre de campo cuando desea utilizar los miembros públicos de la clase heredada. No afecta a la interfaz pública de la clase en absoluto. No me gusta usarlo, pero no veo ninguna buena razón para no hacerlo.

Jules
fuente