BARRANCOS DE MASCA, MACIZO DE TENO, NOROESTE DE TENERIFE
La herencia representa uno de los mecanismos clave en el desarrollo de la POO (Programación Orientada a Objetos), fundamental desde la filosofía de la reusabilidad del código, ahorro de memoria, ligereza y optimización. ¿Qué bien suena, verdad?
APROXIMACIÓN TEÓRICA
En la Programación Orientada a Objetos, el código duplicado se estima como un error, un constructo no deseado que repercute de forma negativa en la optimización de recursos, genera una sobrecarga de memoria y oscurece y vuelve más ambigua la propia legibilidad del programa. Para evitarlo debemos apoyarnos en uno de los fundamentos estrella de la POO: las herencias, que nos facultan para entablar una relación de parentesco entre dos o más clases (clases padre, clases hijas, clases hermanas) para lo que debemos acudir en primer lugar a la ABSTRACCIÓN, que refiere un procedimiento a través del cual "reunimos" todos los elementos comunes en una estructura única que denominamos SUPERCLASE (clase padre), y que luego podemos descomponer en SUBCLASES (o clases hijas) en función de nuestras necesidades de programación las cuales, además, pueden incluir sus propias especificaciones en sus bloques de código particulares (reeditables). Por otra parte, se puede decir en sentido ASCENDENTE (remontándonos en la jerarquía del código, como, aplicado a nosotros mismos, nuestros padres son ascendentes nuestros, o nuestros abuelos son ascendentes de nuestros padres que, a su vez, son ascendentes nuestros) que una SUBCLASE o clase hija deriva o extiende de tal clase padre o SUPERCLASE. Desde el punto de vista de la sintaxis del lenguaje, los nombres de las clases padre de las que se herede o se extienda deben insertarse como "argumentos" (entrecomillado. Más adelante, uno de nuestros colaboradores peludos aclarará el asunto. O eso ladra,...) entre los paréntesis de las clases hijas. Así Python sabrá que las propiedades y métodos instanciados en las clases padre pasadas como "argumentos", y sólo de éstas, podrán ser heredados o extendidos a los objetos que instanciemos en las clases hijas, lo que nos permite proporcionar funcionalidades a los mismos sin tener que reescribir código.
En 1. modelizamos una clase que nombramos como Cartera_de_Empresa. En 2. declaramos una variable de clase a la que asignamos una lista vacía donde iremos almacenando los distintos contactos que vayamos aportando. En 3. llamamos al método especial constructor/inicializador __init__(self) donde, al mismo tiempo, pasamos los parámetros iniciales. Con la autorreferencia self instanciamos los objetos (propiedades) correspondientes con sus valores de partida (un nombre de variable. Aquéllo que almacene dicha variable más adelante durante la ejecución del programa será el valor que asumirá el objeto en ese momento; si instanciamos un nuevo objeto, el valor almacenado puede, si así lo queremos, cambiarse por otro, y éste será el nuevo valor que asuma el nuevo objeto). Con 4. creamos la línea de código necesaria para añadir (append) los distintos objetos previamente instanciados cuando éstos se pasen como argumentos (todos cuanto lleven la autorreferencia self) a la lista contactos instanciada a partir de la clase Cartera_de_Empresa. Ya en 5. modelizamos una clase nueva a la que nombramos Proveedores que heredará (extenderá de) y, por tanto, será una SUBCLASE o clase hija, de la clase anterior, Cartera_de_Empresa, por el mero hecho de haber insertado su nombre entre sus paréntesis, lo que la convierte en su clase padre estableciéndose una relación de parentesco donde una clase, Proveedores, heredará ( o extenderá de) de otra clase de orden jerárquico superior, Cartera_de_Empresa. En 6. declaramos una nueva variable de clase, proveedores, a la que se le asigna una lista vacía de manera similar a como hicimos en 2. En 7. codificamos de la misma manera que hicimos en 4. En este caso, la lista a la que añadiremos objetos en la lista proveedores perteneciente a la clase hija Proveedores. En 8. modelizamos una tercera clase que llamamos Clientes que, al igual que Proveedores, será también una clase hija que heredará o extenderá de Cartera_de_Empresa. En 9. declaramos una nueva variable de clase a la que denominamos clientes y a la que asignamos una lista vacía como valor por el mismo procedimiento que empleamos en 2. y 6. De nuevo en 10. procedemos del mismo modo que hicimos ya en 4. y 7. En 11. llamamos al primer elemento (nombre) almacenado en la lista contactos que depende de la clase padre Cartera_de_Empresa. En 12. hacemos lo propio pero con los nombres almacenados en la lista proveedores que depende de la clase hija Proveedores. Igual procedemos en 13., con el primer ítem de la lista clientes que depende de la clase hija Clientes. Con 14. comprobamos que en la lista contactos de Cartera_de_Empresa tenemos almacenados todos los objetos instanciados: a, b, y c, cualquiera que fuera la clase: Cartera_de_Empresa, Proveedores, o Clientes. Pero en la llamada siguiente, Clientes.clientes, vemos que sólo tenemos uno, esto es, aquél que instanciamos a partir de la clase hija Clientes.
Lo que la herencia hace es precisamente eso: permite que un elemento b herede (también se utiliza el término "extender de") código de un elemento a, implementándolo en su propio código. Imaginemos que queremos actualizar nuestro PC de sobremesa. Acudimos a una tienda y pedimos que nos monten otro nuevo. Pero, claro, todavía tenemos piezas que podemos aprovechar del primero como la carcasa, el teclado, esa fuente de alimentación que nos venía sobrada, la memoria RAM y el disco duro, repleto de esos datos que no queremos o nos da pereza perder.
De esta forma, nuestro equipo nuevo "hereda" esos componentes todavía útiles del anterior. Pues imaginemos lo mismo con software en lugar de hardware y sin necesidad de "extraer y cambiar" nada del equipo anterior sino "replicar" lo que nos interesa del primero en el segundo, con lo cual ambos programas, el primero y el segundo, resultan perfectamente operativos y ya tenemos un concepto bastante aproximado de lo que significa herencia en programación. Vayamos concretando: la herencia genera una forma de relación entre dos (o más) clases de tal modo que una de éstas, mediante la sintaxis adecuada, tenga acceso a las propiedades y métodos instanciados en la otra sin tener que repetirlos en su propio código. Así pues, contamos con una clase que llamaremos base, principal, padre o, también, superclase (nombres no le faltan, la verdad) donde se han instanciado los atributos/propiedades y los métodos susceptibles de ser heredados (o extendidos) por la segunda clase, que llamaremos hija. Insistimos: tanto las propiedades como los métodos son "herenciables", esto es, que se pueden heredar.
EN NUESTRO CASO Y PARA NO COMPLICARNOS DEMASIADO LAS COSAS RECURRIREMOS A LLAMAR A LA CLASE PRINCIPAL COMO CLASE PADRE, Y A LA CLASE QUE HEREDA, COMO CLASE HIJA.
El funcionamiento de los mecanismos de herencia se basan en los namespaces, en los espacios de nombre, de manera que cuando instanciamos un objeto en la clase hija y llamamos a una propiedad o a un método, lo primero, lo primerísimo que hace Python, es indagar entre las propiedades y métodos propios de la clase hija, es decir, aquéllos que instanciamos nuevos en ella. Si no lo encuentra, accede al namespace de la clase padre para buscar allí coincidencias. Si la encuentra aplica en la clase hija la propiedad o método que encontró en la clase padre y a vivir que son dos días. Así funciona, mutatis mutandis, el mecanismo de herencia a nivel interno.
La sintaxis es la siguiente: una vez que tenemos modelizada una clase cualquiera a la que hemos proporcionado un nombre, por ejemplo, clase Base, podemos crear una clase hija que se modelizará como:
De esta manera ya tenemos modelizada una clase hija, con la adición de un paréntesis donde insertaremos el nombre de una clase, si vamos a trabajar con una herencia simple, o con varios nombres de clase padre, si vamos a heredar (o extender) propiedades y/o métodos de más de una clase. Si tenemos más de una clase padre, trabajaremos en lo que se llama herencia múltiple.
PERMITIDME LADRAROS UNA COSA: LOS PARÉNTESIS, COMO YA SABEMOS SE COLOCAN AL FINAL DE LOS MÉTODOS (FUNCIONES) Y, OPCIONALMENTE, AL FINAL TAMBIÉN DE LAS CLASES. EN EL CASO DE LOS MÉTODOS (FUNCIONES), NOS REFERIMOS A ELLOS COMO "ZONA DE PARÁMETROS" PORQUE EN ELLOS, SALVO QUE ESTÉN VACÍOS, SE INSERTARÁN LOS PARÁMETROS QUE NECESITAREMOS PARA QUE EL MÉTODO O FUNCIÓN SE EJECUTE CORRECTAMENTE. EN LAS CLASES, YA QUE ÉSTAS NO LLEVAN PARÁMETROS, LOS PARÉNTESIS SE UTILIZAN EXCLUSIVAMENTE PARA INSERTAR EN ELLOS LOS NOMBRES DE LAS CLASES PADRE DE LAS QUE HEREDAN O EXTIENDEN.
A partir de este momento podremos instanciar nuestros propios atributos/propiedades y/o métodos dentro del cuerpo o ámbito de la clase hija.
Cuando queramos llamar a un método de la clase padre, debemos proceder de la siguiente manera: creamos la función definida, def, correspondiente, y le asignamos un nombre. Puede venirnos bien que el nombre que le demos sea similar, parecido o una variante del nombre del método en la clase padre que vamos a extender o heredar ya que van a desempeñar la misma función. Importante: no se importa la cabecera de la función sino su cuerpo, el bloque de código correspondiente, y de ahí que tengamos que nominar al método de la clase hija creando una función definida para tal fin, ex profeso, donde se va a heredar o extender. Para hacerlo, recurrimos a la función integrada super() y, a continuación, con la notación del punto, llamamos al método que queremos heredar de la clase padre:
Veámoslo en acción con un ejemplo:Fijémonos, a parte de las explicaciones que hemos sobreimpreso en el ejemplo, que cuando llamamos a la clase hija para instanciar un objeto (en el ejemplo, a) no hemos tenido que incluir el nombre de la clase padre: efectuamos una llamada simple para instanciar objetos igual que llamamos a Triangle() para instanciar al objeto t1. También podemos heredar propiedades, como es el caso del ejemplo que proponemos a continuación:
Tengamos en cuenta lo siguiente: los métodos de la clase padre que invoquemos en la clase hija a través de la función super(), como no se están definiendo en la clase hija porque ya lo están en la clase padre desde donde se heredan o se extienden, no necesitan llevar la autorreferencia self. Ya están instanciados y no precisan instanciarse de nuevo dado que en el mecanismo de herencia también se hereda la autorreferencia a la clase padre en cuanto a las propiedades y/o métodos que se heredan. ¿Se les puede poner? Sí, no pasa nada; pero no son necesarios. Sí deben contener, evidentemente, los argumentos que se le hayan pasado en su instanciación, aunque no necesariamente, ni mucho menos, claro está, sus mismos valores (el mecanismo de herencia, entonces, tendría un campo de actuación muy limitado y no tendría prácticamente utilidad alguna).
Lo que acabamos de ver es, por así decirlo, una forma "indirecta" de heredar, a través de una función nativa diseñada específicamente por Python para este cometido. Y lo hace por una razón muy concreta. ¿Cuál? Para responder a esta pregunta mejor vemos la forma "directa" de heredar o extender de una clase padre en Python y observar las diferencias. En una herencia "directa", no tenemos más que, una vez establecida entre los paréntesis de la clase hija su filiación, invocar en ésta los métodos y propiedades de la clase padre sin más para hacer uso de ellos. Así de simple. Veamos:
HERENCIA SIN SUPER():
HERENCIA CON SUPER():
Ahí lo tenemos: el resultado es el mismo en ambos casos. Entonces, ¿por qué usar o no usar la función super()? ¿ Cuál es esa"razón concreta"? Fijémonos en cómo hemos estructurado el código en ambos casos. En el primer ejemplo hemos tenido que invocar cada método de la clase padre por separado, 1., mientras que en el segundo, y he aquí el quid de la cuestión, instanciamos un método más en la clase hija en el que importamos todos los métodos de la clase padre que queramos para que, cuando se lo invoque, los ejecute todos a la vez. ¿Cuál de las dos maneras escoger? Pues aquélla que mejor se adapte a nuestras necesidades de acuerdo al programa que estemos desarrollando. Sin embargo, a todas éstas, tengamos en cuenta que si vamos a invocar varias veces los mismos métodos de la clase padre a lo largo de nuestro código o, cuando menos, tenemos previsto hacerlo, nos convendría más disponer de un único método instanciado en la clase hija, como el que llamamos caracteristicas_básicas en nuestro ejemplo precedente, para no tener que sobreescribir los mismos métodos una y otra vez abundando en la filosofía de la simplificación del código. Resumiendo: - HERENCIA DIRECTA ➡️ sin super():
- Se invoca directamente a los métodos de la clase padre en el cuerpo de la clase hija.
- HERENCIA INDIRECTA ➡️ con super():
- Instanciamos en la clase hija un método ad hoc que, a través de la función super(), invoque los distintos métodos deseamos heredar o extender de la clase padre.
El recurso a la función integrada super() proporciona además una ventaja añadida: la posibilidad de modificar los argumentos pasados a los métodos originales en la clase padre si fuera necesario.
DEDICAREMOS MÁS ADELANTE UN CAPÍTULO EXCLUSIVO PARA TRATAR LA FUNCIÓN INTEGRADA super() CON UN POCO MÁS DE DETENIMIENTO, AHONDANDO EN SUS POSIBILIDADES Y, CÓMO NO, PROPORCIONANDO NUEVOS EJEMPLOS DE USO.
CAEN LA NIEBLA Y LA TARDE EN EL BOSQUE TERMÓFILO DE TENERIFE.
Conviene hacer la siguiente aclaración. Si modelizamos una clase de la manera que proponemos a continuación:
Sólo podremos obtener resultados de la forma que exponemos en el ejemplo que viene a continuación:
Por razones como ésta recomendamos dejar al método especial __init__(self) sin argumentos de ningún tipo, tal cual, o con una simple impresión (función integrada print()) que nos informe de que la inicialización se ha ejecutado con éxito, y dejar los argumentos para los métodos definidos por nosotros mismos: nos evitaremos más de un quebradero de cabeza. Cambiando de tercio, como en los toros, es posible invocar un método de clase instanciado en la clase padre, recordemos, con el decorador (decorator) @classmethod, y hacer uso de él en la clase hija, preferiblemente, recurriendo a la función super(). Proponemos para el caso dos ejemplos:
Como demostramos a continuación, podemos incluso asignar nuevos valores a las variables que pasamos como argumento al método de clase.
Hasta ahora hemos estudiado herencias simples en tanto que nuestras clases hijas modelizadas sólo heredaban o extendían de una única clase padre. Pero la plasticidad de Python permite que sea posible que nuestra clase hija herede o extienda de dos o más clases padre. En este caso hablaríamos de herencia múltiple. Su sintaxis es muy similar a la de la herencia simple insertando, en esta ocasión, los distintos nombres de las clases padre de las que se quiera heredar separadas por las comas preceptivas (recordemos que Python utiliza las comas como hitos para diferenciar entre distintos objetos, del mismo modo que las utilizamos nosotros mismos cuando efectuamos una lectura, por ejemplo, cuando leemos este mismo blog, para diferenciar entre unos elementos y otros. Esta es una de las ventajas que proporciona un lenguaje de alto nivel como Python, como mencionábamos al comienzo de este manual, que muestra un comportamientos tan próximo a la forma que tenemos los humanos de entender y relacionarnos con la realidad). La herencia múltiple constituye una excelente opción para ahorrar código y simplificarlo, pero tiene el inconveniente de que, si heredamos o extendemos dentro de una única clase hija de un número elevado de clases padre, puede ocurrir que algunas de estas clases padre, realmente, no lo fueran; o que intentemos heredar o extender de una clase "hermana" una propiedad o método (una clase "hermana", por así decirlo, es una clase que comparte el mismo padre con nuestra clase hija) con lo que Python lanzará una excepción.
EL PROBLEMA DEL DIAMANTE
Se conoce así en programación al caso en que dos o más clases hijas heredan o extienden de una misma clase padre, y aún contamos con una tercera clase hija que hereda de estas últimas. Gráficamente, a la primera clase padre la llamamos A, mientras que a las clases nominadas como B y C son clases hijas que heredan de A. Por otra parte, la clase D es una clase hija de B y C en tanto que hereda o extiende de ellas. Vamos a imaginar ahora que instanciamos un objeto "alfa" a partir de la clase hija D, esto es, alfa = D(), e invocamos un método cualquiera que hallamos instanciado en la clase padre A. La pregunta que cabe hacerse es de dónde hereda el método? ¿ De la clase B que es hija directa de la clase padre A, o de la clase C que también es hija directa de la clase padre A? Python resuelve la cuestión tomando como referencia que todas las clases modelizadas son descendientes de la superclase de superclases object y, a partir de aquí, desarrolla una estructura de selección que se basa en generar una lista interna de clases (¡Ah!, las listas,... ¡El alma de Python!) donde se busca desde el descendente en dirección al ascendente, esto es, de derecha a izquierda y de abajo a arriba, eliminando las duplicidades si las hay mediante la aplicación de una función de filtrado, filter(), sobre la lista interna de clases excepto la última, instaurando un orden pertinente y preciso.
Otro aspecto a tener en cuenta de la sintaxis de herencia es que podemos sustituir el uso de la función integrada super() por la autorreferencia self., con la notación de punto correspondiente. El optar por super() o por self es una cuestión personal, pero la mayoría de los programadores prefieren recurrir a la autorreferencia self, siempre que la simplicidad del código lo permita.
Por supuesto tenemos también a nuestra disposición la posibilidad de apoyarnos en en una herencia con invocación directa de los métodos y/o propiedades desde la clase hija:
CAMPOS Y PALMERAS EN EL VALLE DE SANTIAGO DEL TEIDE, OESTE DE TENERIFE.
|
No hay comentarios:
Publicar un comentario