Pregunta ¿Cuál es el sentido de la herencia en Python?


Supongamos que tiene la siguiente situación

#include <iostream>

class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
    void speak() { std::cout << "woff!" <<std::endl; }
};

class Cat : public Animal {
    void speak() { std::cout << "meow!" <<std::endl; }
};

void makeSpeak(Animal &a) {
    a.speak();
}

int main() {
    Dog d;
    Cat c;
    makeSpeak(d);
    makeSpeak(c);
}

Como puede ver, makeSpeak es una rutina que acepta un objeto Animal genérico. En este caso, Animal es bastante similar a una interfaz Java, ya que contiene solo un método virtual puro. makeSpeak no sabe la naturaleza del Animal que pasa. Simplemente le envía la señal de "hablar" y deja el último enlace para que se encargue de qué método llamar: ya sea Cat :: speak () o Dog :: speak (). Esto significa que, en lo que respecta a makeSpeak, el conocimiento de qué subclase se pasa en realidad es irrelevante.

¿Pero qué hay de Python? Veamos el código para el mismo caso en Python. Tenga en cuenta que trato de ser lo más parecido posible al caso de C ++ por un momento:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

Ahora, en este ejemplo, ves la misma estrategia. Usas la herencia para aprovechar el concepto jerárquico de que tanto los perros como los gatos son animales. Pero en Python, no hay necesidad de esta jerarquía. Esto funciona igual de bien

class Dog:
    def speak(self):
        print "woff!"

class Cat:
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

En Python puede enviar la señal "hablar" a cualquier objeto que desee. Si el objeto puede manejarlo, se ejecutará, de lo contrario generará una excepción. Supongamos que agrega un avión de clase a ambos códigos y envía un objeto de Avión para hacerSpeak. En el caso de C ++, no compilará, ya que Airplane no es una clase derivada de Animal. En el caso de Python, se generará una excepción en el tiempo de ejecución, que incluso podría ser un comportamiento esperado.

Por otro lado, supongamos que agrega una clase MouthOfTruth con un método speak (). En el caso de C ++, o bien tendrá que refactorizar su jerarquía, o tendrá que definir un método makeSpeak diferente para aceptar objetos MouthOfTruth, o en java podría extraer el comportamiento en una CanSpeakIface e implementar la interfaz para cada uno. Hay muchas soluciones ...

Lo que me gustaría señalar es que aún no he encontrado una sola razón para usar la herencia en Python (aparte de los marcos y árboles de excepciones, pero creo que existen estrategias alternativas). no es necesario implementar una jerarquía derivada de base para realizar polimórficamente. Si desea utilizar la herencia para reutilizar la implementación, puede lograr lo mismo a través de la contención y la delegación, con la ventaja añadida de que puede modificarla en tiempo de ejecución y definir claramente la interfaz del contenido, sin correr el riesgo de efectos secundarios no deseados.

Entonces, al final, la pregunta es: ¿cuál es el sentido de la herencia en Python?

Editar: gracias por las respuestas muy interesantes. De hecho, puede usarlo para la reutilización de código, pero siempre tengo cuidado al reutilizar la implementación. En general, tiendo a hacer árboles de herencia muy poco profundos o ningún árbol en absoluto, y si una funcionalidad es común lo refactorizo ​​como una rutina de módulo común y luego lo llamo desde cada objeto. Veo la ventaja de tener un único punto de cambio (por ejemplo, en lugar de agregar a Dog, Cat, Moose, etc., solo agrego a Animal, que es la ventaja básica de la herencia), pero puedes lograr lo mismo con una cadena de delegación (por ejemplo, a la JavaScript). No estoy afirmando que sea mejor, solo otra forma.

También encontré una publicación similar referente a esto.


76
2018-06-19 23:28


origen


Respuestas:


Usted se está refiriendo a la mecanografía de pato en tiempo de ejecución como herencia "primordial", sin embargo, creo que la herencia tiene sus propios méritos como enfoque de diseño e implementación, siendo una parte integral del diseño orientado a objetos. En mi humilde opinión, la cuestión de si puede lograr algo de otro modo no es muy relevante, porque en realidad podría codificar Python sin clases, funciones y más, pero la pregunta es qué tan bien diseñado, robusto y legible será su código.

Puedo dar dos ejemplos de que la herencia es el enfoque correcto, en mi opinión, estoy seguro de que hay más.

Primero, si codifica inteligentemente, su función makeSpeak puede querer validar que su entrada es de hecho un Animal, y no solo que "puede hablar", en cuyo caso el método más elegante sería usar la herencia. Nuevamente, puedes hacerlo de otras maneras, pero esa es la belleza del diseño orientado a objetos con herencia: tu código "realmente" verificará si la entrada es un "animal".

En segundo lugar, y claramente más sencillo, es la encapsulación, otra parte integral del diseño orientado a objetos. Esto se vuelve relevante cuando el ancestro tiene miembros de datos y / o métodos no abstractos. Tome el siguiente ejemplo tonto, en el cual el ancestro tiene una función (speak_twice) que invoca una función en ese entonces abstracta:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

    def speak_twice(self):
        self.speak()
        self.speak()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

Asumiendo "speak_twice" es una característica importante, no desea codificarla en Dog and Cat, y estoy seguro de que puede extrapolar este ejemplo. Claro, podría implementar una función autónoma de Python que acepte algún objeto de tipo pato, verifique si tiene una función de hablar e invocarla dos veces, pero eso no es elegante y le falta el punto número 1 (valide que es un Animal). Peor aún, y para fortalecer el ejemplo de encapsulación, ¿qué pasaría si una función miembro en la clase descendiente deseara usar "speak_twice"?

Se vuelve aún más claro si la clase antecesora tiene un miembro de datos, por ejemplo "number_of_legs" que es utilizado por métodos no abstractos en el ancestro como "print_number_of_legs", pero se inicia en el constructor de la clase descendiente (por ejemplo, Dog lo inicializaría con 4, mientras que Snake lo inicializaría con 0).

Una vez más, estoy seguro de que hay un sinfín de más ejemplos, pero básicamente todo software (lo suficientemente grande) que se basa en el diseño orientado a objetos sólidos requerirá herencia.


79
2018-06-19 23:35



La herencia en Python tiene que ver con la reutilización del código. Factorice la funcionalidad común en una clase base e implemente una funcionalidad diferente en las clases derivadas.


12
2018-06-20 04:51



La herencia en Python es más conveniente que cualquier otra cosa. Encuentro que es mejor usarlo para proporcionar una clase con "comportamiento predeterminado".

De hecho, existe una comunidad significativa de desarrolladores de Python que argumentan en contra del uso de la herencia en absoluto. Hagas lo que hagas, no solo no exageres. Tener una jerarquía de clases demasiado complicada es una forma segura de etiquetarlo como "programador de Java", y simplemente no se puede tener. :-)


9
2018-06-20 03:44



Creo que el objetivo de la herencia en Python no es hacer que el código se compile, es por la verdadera razón de la herencia que es extender la clase a otra clase secundaria y anular la lógica en la clase base. Sin embargo, la tipificación de pato en Python hace que el concepto de "interfaz" sea inútil, ya que solo se puede verificar si el método existe antes de la invocación sin necesidad de utilizar una interfaz para limitar la estructura de clase.


8
2018-06-19 23:34



Creo que es muy difícil dar una respuesta concreta y significativa con ejemplos tan abstractos ...

Para simplificar, hay dos tipos de herencia: interfaz e implementación. Si necesita heredar la implementación, Python no es tan diferente de los lenguajes de OO estáticos como C ++.

La herencia de la interfaz es donde hay una gran diferencia, con consecuencias fundamentales para el diseño de su software en mi experiencia. Los lenguajes como Python no lo obligan a usar la herencia en ese caso, y evitar la herencia es un buen punto en la mayoría de los casos, porque es muy difícil corregir una opción de diseño incorrecta más adelante. Ese es un punto bien conocido planteado en cualquier buen libro OOP.

Hay casos en los que es aconsejable usar herencia para interfaces en Python, por ejemplo, para complementos, etc. ... En esos casos, Python 2.5 y posteriores carecen de un enfoque elegante "integrado", y varios grandes frameworks diseñaron sus propias soluciones (zope, trac, twister). Python 2.6 y superior tiene Clases de ABC para resolver esto.


7
2018-06-20 03:31



En C ++ / Java / etc., el polimorfismo es causado por la herencia. Abandona esa creencia mal concebida, y los lenguajes dinámicos se abren para ti.

Esencialmente, en Python no existe una interfaz tanto como "la comprensión de que ciertos métodos son invocables". Bonita ondulada a mano y con un sonido académico, ¿no? Significa que porque usted llama "hablar", claramente espera que el objeto tenga un método de "hablar". Simple, ¿eh? Esto es muy Liskovian porque los usuarios de una clase definen su interfaz, un buen concepto de diseño que te lleva a un TDD más saludable.

Entonces, lo que queda es, como otro cartel cortésmente logró evitar decir, un truco para compartir códigos. Podría escribir el mismo comportamiento en cada clase "infantil", pero eso sería redundante. Es más fácil heredar o combinar la funcionalidad que es invariante en toda la jerarquía de herencia. Más pequeño, el código DRY-er es mejor en general.


5
2018-06-20 01:01



No es herencia que el tipado de patos no tenga sentido, son interfaces, como la que elegiste al crear una clase de animales totalmente abstracta.

Si hubiera usado una clase de animales que introdujera algún comportamiento real para que sus descendientes los usen, entonces las clases de perros y gatos que introdujeron algún comportamiento adicional deberían ser una razón para ambas clases. Es solo en el caso de que la clase antecesora no contribuya con un código real para las clases descendientes que su argumento sea correcto.

Debido a que Python puede conocer directamente las capacidades de cualquier objeto, y debido a que esas capacidades son mutables más allá de la definición de la clase, la idea de usar una interfaz abstracta pura para "decirle" al programa qué métodos se pueden llamar es algo inútil. Pero ese no es el único, ni siquiera el principal, punto de herencia.


5
2017-10-15 22:28



Puede evitar la herencia en Python y casi cualquier otro idioma. Sin embargo, todo se trata de reutilización de código y simplificación de código.

Solo un truco semántico, pero después de construir tus clases y clases base, ni siquiera tienes que saber qué es posible con tu objeto para ver si puedes hacerlo.

Digamos que tiene d, que es un perro que subclasificó a Animal.

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

Si lo que el usuario ingresó está disponible, el código ejecutará el método adecuado.

Usando esto puedes crear cualquier combinación de monstruosidad híbrida de Mamíferos / Reptiles / Aves que quieras, y ahora puedes hacer que diga '¡Ladra!' ¡mientras vuela y saca su lengua bífida y lo manejará correctamente! ¡Diviértete con eso!


1
2018-06-24 23:40



No veo mucho sentido en la herencia.

Cada vez que he utilizado la herencia en sistemas reales, me quemé porque me llevó a una red de dependencias enredadas, o simplemente me di cuenta a tiempo de que estaría mucho mejor sin ella. Ahora, lo evito tanto como sea posible. Simplemente nunca tengo un uso para eso.

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

En una ocasión, a James Gosling se le preguntó en una conferencia de prensa una pregunta similar a la siguiente: "Si pudieras volver atrás y hacer Java de manera diferente, ¿qué dejarías afuera?". Su respuesta fue "Clases", a lo que se rieron. Sin embargo, hablaba en serio y explicó que, en realidad, no eran las clases las que constituían el problema, sino la herencia.

En cierto modo, lo veo como una dependencia a las drogas: te proporciona una solución rápida que te hace sentir bien, pero al final te confunde. Con eso me refiero a que es una forma conveniente de reutilizar el código, pero obliga a un acoplamiento no saludable entre el niño y la clase de padres. Los cambios en el padre pueden romper al niño. El niño depende del padre para cierta funcionalidad y no puede alterar esa funcionalidad. Por lo tanto, la funcionalidad proporcionada por el niño también está vinculada al padre; solo puede tener ambas.

Mejor es proporcionar una única clase de cliente frente a una interfaz que implemente la interfaz, utilizando la funcionalidad de otros objetos que se componen en el momento de la construcción. Al hacer esto a través de interfaces diseñadas apropiadamente, se puede eliminar todo acoplamiento y proporcionamos una API altamente composable (Esto no es nada nuevo, la mayoría de los programadores ya lo hacen, simplemente no lo suficiente). Tenga en cuenta que la clase de implementación no debe simplemente exponer la funcionalidad, de lo contrario, el cliente solo debe usar las clases compuestas directamente: debe hacer algo nuevo combinando esa funcionalidad.

Existe el argumento del campo de la herencia de que las implementaciones de la delegación pura sufren porque requieren muchos métodos de "pegamento" que simplemente transmiten los valores a través de una "cadena" de delegación. Sin embargo, esto es simplemente reinventar un diseño similar a una herencia usando la delegación. Los programadores con demasiados años de exposición a diseños basados ​​en herencia son particularmente vulnerables a caer en esta trampa, ya que, sin darse cuenta, pensarán en cómo implementarían algo utilizando la herencia y luego lo convertirían en delegación.

La separación adecuada de las preocupaciones como el código anterior no requiere métodos de pegamento, ya que cada paso es en realidad valor agregado, por lo que en realidad no son métodos de "pegamento" (si no agregan valor, el diseño es defectuoso).

Todo se reduce a esto:

  • Para el código reutilizable, cada clase debe hacer una sola cosa (y hazlo bien).

  • La herencia crea clases que hacen más de una cosa, porque son mezclado con clases para padres.

  • Por lo tanto, usar la herencia hace que las clases sean difíciles de reutilizar.


1
2017-07-01 20:55



Otro pequeño punto es el tercer ejemplo de Op, no se puede llamar a isinstance (). Por ejemplo, pasando el ejemplo de 3er a otro objeto que toma y "Animal" escribe que las llamadas hablan sobre él. Si lo haces, no deberías verificar el tipo de perro, el tipo de gato, etc. No estoy seguro si la comprobación de instancias es realmente "Pythonic", debido a la vinculación tardía. Pero entonces tendrías que implementar de alguna manera que AnimalControl no intente lanzar los tipos Cheeseburger en el camión, porque las Cheeseburgers no hablan.

class AnimalControl(object):
    def __init__(self):
        self._animalsInTruck=[]

    def catachAnimal(self,animal):
        if isinstance(animal,Animal):
            animal.speak()  #It's upset so it speak's/maybe it should be makesNoise
            if not self._animalsInTruck.count <=10:
                self._animalsInTruck.append(animal) #It's then put in the truck.
            else:
                #make note of location, catch you later...
        else:
            return animal #It's not an Animal() type / maybe return False/0/"message"

1
2017-07-11 14:14