From e81137474f102d71fe0b25307bcf88810c1b2d43 Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Wed, 4 Mar 2020 13:41:02 +0100 Subject: New arrangement for multilanguage and metadata support --- es/05_oop.md | 1093 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1093 insertions(+) create mode 100644 es/05_oop.md (limited to 'es/05_oop.md') diff --git a/es/05_oop.md b/es/05_oop.md new file mode 100644 index 0000000..6fdb9ce --- /dev/null +++ b/es/05_oop.md @@ -0,0 +1,1093 @@ +# Programación Orientada a Objetos + +La *programación orientada a objetos* u *object oriented programming* (OOP) es +un paradigma de programación que envuelve python de pies a cabeza. A pesar de +que python se define como un lenguaje de programación multiparadigma, la +programación orientada a objetos es el paradigma principal de éste. A pesar de +que varias de las características que tratamos en el apartado anterior se +corresponden más con un lenguaje de programación funcional, en python **todo** +(o casi todo) es una clase. + +Python usa una programación orientada a objetos basada en clases[^class], a +diferencia de otros lenguajes como JavaScript, donde la orientación a objetos +está basada en prototipos[^proto]. No es el objetivo de este documento el de +contarte cuales son las diferencias entre ambas, pero es interesante que sepas +de su existencia, ya que es una de las pocas diferencias que existen entre +estos dos lenguajes de amplio uso en la actualidad. + +[^class]: +[^proto]: + +## Programación basada en clases + +Tras haber hecho una afirmación tan categórica como que en python todo son +clases, es nuestra obligación entrar a definir lo que son y qué implica la +programación basada en clases. + +Los objetos, *objects*, son entidades que encapsulan un estado, un +comportamiento y una identidad capaz de separarlos de otras entidades. Una +clase, *class*, es la definición de estos objetos. + +Saliendo de la definición filosófica y trayéndola a un nivel de andar por casa, +puedes aclararte sabiendo que las clases son la definición enciclopédica de +algo, mientras que los objetos son el propio objeto, persona o animal descrito. + +Llevándolo al ejemplo de un perro, la clase es la definición de qué es un perro +y los objetos son los distintos perros que te puedes encontrar en el mundo. La +definición de perro indica qué características ha de tener un ente para ser un +perro, como ser un animal, concretamente doméstico, qué anatomía debe tener, +cómo debe comportarse, etc. Mientras que el propio perro es uno de los casos de +esa definición. + +Cada perro tiene una **identidad propia** y es independiente de los otros, +tiene un **comportamiento** concreto (corre, salta, ladra...) y tiene un +**estado** (está despierto o dormido, tiene una edad determinada...). + +La diferencia entre una clase y un objeto tiene lógica si lo piensas desde la +perspectiva de que python no tiene ni idea de lo que es un perro y tú tienes +que explicárselo. Una vez lo haces, declarando tu clase, puedes crear +diferentes perros y ponerlos a jugar. Lo bonito de programar es que tu programa +es tu mundo y tú decides lo que es para ti (o para tu programa) un perro. + +A nivel práctico, los objetos son grupos de datos (el *estado*) y funciones (la +*funcionalidad*). Estas funciones son capaces de alterar los datos del propio +objeto y no de otro (se intuye el concepto de *identidad*). Analizándolo desde +el conocimiento que ya tienes, es lógico pensar que un objeto es, por tanto, +una combinación de valores y funciones accesible a modo de elemento único. +Exactamente de eso se trata. + +Existe una terminología técnica, eso sí, para referirse a esos valores y a esas +funciones. Normalmente los valores se conocen como *propiedades* del objeto y +las funciones se conocen como *métodos*. Así que siempre que hagamos referencia +a cualquiera de estas dos palabras clave debes recordar que hacen referencia a +la programación orientada a objetos. + +### Fundamento teórico + +La programación basada en clases se basa en tres conceptos fundamentales que +repasaremos aquí de forma rápida para razonar el interés de la programación +orientada a objetos sobre otros paradigmas. + +La **encapsulación**[^encapsulation] trata de crear datos con sus métodos +propios para alterarlos de modo que restrinjan el acceso directo al contenido +de estos datos con el fin de asegurar una coherencia o robustez interna. Puedes +entender esto como una forma de esconder información o como mi profesor de +programación II en la universidad solía decir: «Las patatas se pelan en la +cocina del restaurante, no en el comedor». La utilidad de la encapsulación es +la de aislar secciones del programa para tener total control sobre su +contenido gracias a tener total control de la vía de acceso a estos datos. A +nivel práctico este concepto puede usarse para, por ejemplo, obligar a que un +objeto sólo pueda ser alterado en incrementos controlados en lugar de poder +pisarse con un valor arbitrario. + +La **herencia**[^inheritance] es un truco para reutilizar código de forma +agresiva que, casualmente, sirve como una buena forma de razonar. Aporta la +posibilidad de crear nuevas *clases* a partir de clases ya existentes. +Volviendo a la simplificación anterior, si una clase es una definición +enciclopédica de un concepto, como un perro, puede estar basada en otra +descripción para evitar contar todo lo relacionado con ella. En el caso del +perro, el perro es un animal. Animal podría ser otra clase definida previamente +de la que el perro heredara y recibiera gran parte de su descripción genérica +para sólo cubrir puntos que necesite especificar como el tamaño, la forma, el +tipo de animal, el comportamiento concreto, etc. Existe la posibilidad de hacer +herencias múltiples también ya que algunos conceptos pueden describirse en dos +superclases distintas: un perro es un animal (vive, muere, se alimenta, se +reproduce) y también es terrestre (camina sobre una superficie, etc). Ambos +conceptos son independientes: los coches también son terrestres pero no son +animales y los peces también son animales pero no terrestres. + +Y, finalmente, el **polimorfismo**[^polymorphism]. La propia etimología de la +palabra define con bastante precisión el concepto, pero aplicarlo a la +programación orientada a objetos no es tan evidente. Existen varios tipos de +polimorfismo pero el más sencillo es entender el *subtyping*[^subtyping]. Una +vez lo comprendas el resto será evidente. Si volvemos al ejemplo del perro, +para ciertos comportamientos, nos da igual que tratemos de perros, de peces o +de pájaros, todos son animales y todos los animales se comportan de la misma +forma. Es decir, todas las subclases señaladas comparten el comportamiento de +la superclase animal. Si esto es cierto, puede suponerse que en cualquier caso +en el que se espere un objeto de la clase animal es seguro usar una subclase de +ésta. + +Visto desde otra perspectiva, las subclases comparten comportamiento porque +reutilizan las funciones de la clase principal o las redefinen (*herencia*), +pero podemos asegurar que todas las subclases tienen un conjunto de funciones +con la misma estructura, independientemente de lo que hagan, que aseguran que +siempre van a ser compatibles. El nombre de esta cualidad viene a que un perro +puede tomar la forma de un animal. + +Los otros tipos de polimorfismo explotan el mismo comportamiento de diferentes +maneras, mientras que recuerdes que es posible programar de modo que el tipo de +los datos que trates sea indiferente o pueda variar es suficiente. Otro ejemplo +de esto son los operadores matemáticos, que son capaces de funcionar en +cualquier tipo de número (integer, float, complex, etc.) de la misma manera, ya +que todos son números, al fin y al cabo. + +Entender estos conceptos a nivel intuitivo, sin necesidad de entrar en los +detalles específicos de cada uno, es interesante para cualquier programador y +facilita de forma radical la comprensión de muchas de las decisiones de diseño +tomadas en python y en proyectos relacionados aunque también, por supuesto, de +otros lenguajes y herramientas. + +[^encapsulation]: https://en.wikipedia.org/wiki/Encapsulation_(computer_programming) +[^inheritance]: https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming) +[^polymorphism]: https://en.wikipedia.org/wiki/Polymorphism_(computer_science) +[^subtyping]: https://en.wikipedia.org/wiki/Subtyping + + +## Sintaxis + +En el siguiente ejemplo se muestra la sintaxis básica a la hora de crear una +clase y después instanciar dos nuevos objetos `bobby` y `beltza`. Los puntos +(`.`) se utilizan para indicar a quién pertenece el método o propiedad al que +se hace referencia (*identidad*). De este modo, no ocurrirá lo mismo cuando el +perro (`Dog`) `bobby` ladre (`bark`) que cuando lo haga el perro `beltza`. + +Los métodos describen la *funcionalidad* asociada a los perros en general, pero +además, la función `bark` los describe en particular, haciendo que cada perro +tome su nombre (`name`), una propiedad o dicho de otro modo, su *estado*. + +``` python +class Dog: + type = "canine" + def __init__(self, name): + self.name = name + def bark(self): + print("Woof! My name is " + self.name) + +bobby = Dog("Bobby") # New Dog called Bobby +beltza = Dog("Beltza") # New Dog called Beltza + +bobby.name # Bobby +beltza.name # Beltza + +bobby.type # canine +beltza.type # canine + +bobby.bark() # Prints "Woof! My name is Bobby" +beltza.bark() # Prints "Woof! My name is Beltza" +``` + +### Creación de objetos + +El ejemplo muestra cómo crear nuevos *objetos* de la clase `Dog`. Las llamadas +a `Dog("Bobby")` y `Dog("Beltza")` crean las diferentes instancias de la clase. + +Llamar a los nombres de clase como si de funciones se tratara crea una +instancia de éstas. Los argumentos de entrada de la llamada se envían como +argumentos de la función `__init__` declarada también en el propio ejemplo. +Entiende de momento que los argumentos posicionales se introducen a partir de +la segunda posición, dejando el argumento llamado `self` en el ejemplo para un +concepto que más adelante entenderás. + +En el ejemplo, por tanto, se introduce el nombre (`name`) de cada `Dog` en su +creación y la función `__init__` se encarga de asignárselo a la instancia +recién creada mediante una metodología que se explica más adelante en este +mismo capítulo. De momento no es necesario comentar en más profundidad estos +detalles, con lo que sabes es suficiente para entender el funcionamiento +general. + +Queda por aclarar, sin embargo, qué es la función `__init__` y por qué tiene un +nombre tan extraño y qué es `type = canine`, que lo trataremos en próximos +apartados de este capítulo. + +### Herencia + +Antes de entrar en los detalles propuestos en el apartado anterior, que tratan +conceptos algo más avanzados, es interesante ver cómo definir clases mediante +la herencia. Basta con introducir una lista de clases de las que heredar en la +definición de la clase, entre paréntesis, como si de argumentos de entrada de +una función se tratara, tal y como se muestra en la clase `Dog` del siguiente +ejemplo ejecutado en la REPL: + +``` python +>>> class Animal: +... def live(self): +... print("I'm living") +... +>>> class Terrestrial: +... def move(self): +... print("I'm moving on the surface") +... +>>> class Dog(Animal, Terrestrial): +... def bark(self): +... print("woof!") +... def move(self): +... print("I'm walking on the surface") +... +>>> bobby = Dog() +>>> bobby.bark() +woof! +>>> bobby.live() +"I'm living" +>>> bobby.move() +"I'm walking on the surface" +``` + +El ejemplo muestra un claro uso de la herencia. La clase `Dog` hereda +automáticamente las funciones asociadas a las superclases, pero es capaz de +definir las propias e incluso redefinir algunas. Independientemente de la +redefinición del método `move`, cualquier perro (`Dog`) va a ser capaz de +moverse por la superficie, porque la superclase `Terrestrial` ya le da los +métodos necesarios para hacerlo. Lo que ocurre es que cualquier subclase de +`Terrestrial` tiene la ocasión moverse (`move`) a su manera: en el caso del +perro, caminando. + +> NOTA: La herencia es interesante, pero tampoco debe caerse en la psicosis de +> añadir demasiadas superclases. En ocasiones las superclases son necesarias, +> sobre todo cuando aprovechar el polimorfismo facilita el trabajo, pero +> usarlas de forma agresiva genera código extremadamente complejo sin razón. + + +### Métodos de objeto o funciones de clase: `self` + +Los métodos reciben un parámetro de entrada llamado `self` que no se utiliza a +la hora de llamarlos: al hacer `bobby.bark()` no se introduce ningún argumento +de entrada a la función `bark`. + +Sin embargo, si no se añade el argumento de entrada a la definición del método +`bark` y se llama a `bobby.bark()` pasa lo siguiente: + +``` python +>>> class Dog: +... def bark(): +... pass +... +>>> bobby = Dog() +>>> bobby.bark() +Traceback (most recent call last): + File "", line 1, in +TypeError: bark() takes 0 positional arguments but 1 was given +``` + +Python dice que `bark` espera `0` argumentos posicionales pero se le ha +entregado `1`, que nosotros no hemos metido en la llamada, claro está. Así que +ha debido de ser él. + +Efectivamente, python introduce un argumento de entrada en los métodos, el +argumento de entrada que por convención se suele llamar `self`. Este parámetro +es el propio `bobby` en este caso. + +> NOTA: Por convención se le denomina `self`. Tú le puedes llamar como te +> apetezca pero, si pretendes que otros programadores te entiendan, mejor +> `self`. + +Para explicar por qué ocurre esto es necesario diferenciar bien entre clase y +objeto. Tal y como hemos hecho antes con las definiciones enciclopédicas +(*clase*) y los conceptos del mundo real que encajan en la definición +(*objeto*). Los objetos también se conocen como instancias, son piezas de +información independiente que han sido creadas a partir de la definición que la +clase aportaba. + +En python las clases tienen la posibilidad de tener funciones, que definen el +comportamiento de la clase y no el de los objetos que se crean desde ellas. +Ten en cuenta que las clases también deben procesarse y ocupan un espacio en la +memoria, igual que te ocurre a ti, puedes conocer un concepto y su +comportamiento y luego muchos casos que cumplan ese concepto y ambas cosas +son independientes. Esta posibilidad aporta mucha flexibilidad y permite +definir clases complejas. + +Ahora bien, para python las funciones de clase y los métodos (de los objetos, +si no no se llamarían métodos), se implementan de la misma manera. Para la +clase ambas cosas son lo mismo. Sin embargo, el comportamiento del operador +punto (`.`), que dice a quién pertenece la función o método, es diferente si el +valor de la izquierda es una clase o un objeto. Introduciendo en el segundo +caso el propio objeto como primer parámetro de entrada, el `self` del que +hablamos, para que la clase sepa qué objeto tiene que alterar. Este es el +mecanismo de la *identidad* del que antes hablamos y no llegamos a definir en +detalle. Cada objeto es único, y a través del `self` se accede a él. + +Es un truco interesante para no almacenar las funciones en cada uno de los +objetos como método. En lugar de eso, se mantienen en la definición de la clase +y cuando se llama al método, se busca de qué clase es el objeto y se llama a la +función de la clase con el objeto como argumento de entrada. + +Dicho de otra forma, `bobby.bark()` es equivalente a `Dog.bark( bobby )`. + +Ilustrado en un ejemplo más agresivo, puedes comprobar que en función de a +través de qué elemento se acceda a la función `bark` python la interpreta de +forma distinta. A veces como función (*function*) y otras veces como método +(*method*), en función de si se accede desde la clase o desde el objeto: + +``` python +>>> class Dog: +... def bark(self): +... pass +... +>>> type ( Dog.bark) + +>>> type ( bobby.bark ) + +``` + +> NOTA: También te habrás fijado, y si no lo has hecho es momento de hacerlo, +> que los nombres de las clases empiezan por mayúscula en los ejemplos (`Dog`) +> mientras que los objetos comienzan en minúscula (`bobby`). Se trata de otra +> convención ampliamente utilizada para saber diferenciar entre uno y otro de +> forma sencilla. Es evidente cuál es la clase y el objeto con los nombres que +> hemos tratado en los ejemplos, pero en otros casos puede no serlo y con este +> sencillo truco facilitas la lectura de tu código. Hay muchas ocasiones en las +> que esta convención se ignora, así que cuidado. +> Prueba a hacer `type(int)` en la terminal. + +### Variables de clase + +En el primer ejemplo del capítulo hemos postergado la explicación de `type = +canine` y ahora que ya manejas la mayor parte de la terminología y dominas la +diferencia entre una clase y una instancia de ésta (un *objeto*) es momento de +recogerla. A continuación se recupera la sección del ejemplo para facilitar la +consulta, fíjate en la línea 2. + +``` {.python .numberLines} +class Dog: + type = "canine" + def __init__(self, name): + self.name = name + def bark(self): + print("Woof! My name is " + self.name) +``` + +`type` es lo que se conoce como una *variable de clase* (*class variable*). + +> NOTA: En este documento se ha evitado de forma premeditada usar la palabra +> *variable* para referirse a los valores y sus referencias con la intención de +> marcar la diferencia entre ambos conceptos. En este apartado, sin embargo, a +> pesar de que se siga tratando de una referencia, se usa el nombre *class +> variable* porque es como se le llama en la documentación[^class_var] y así +> será más fácil que lo encuentres si en algún momento necesitas buscar +> información al respecto. De esto ya hemos discutido en el capítulo sobre +> datos, donde decimos que *todo es una referencia*. + +[^class_var]: + +Previamente hemos hablado de que los objetos pueden tener propiedades +asociadas, y cada objeto tendrá las suyas. Es decir, que cada instancia de la +clase puede tener sus propias propiedades independientes. El caso que tratamos +en este momento es el contrario, el `type` es un valor que comparten **todas** +las instancias de `Dog`. Cualquier cambio en esos valores los verán todos los +objetos de la clase, así que hay que ser cuidadoso. + +El acceso es idéntico al que ocurriría en un valor asociado al objeto, como en +el caso `name` del ejemplo, pero en este caso observas que en su declaración en +la clase no es necesario indicar `self`, ya no es necesario decir cuál es la +instancia concreta a la que se le asigna el valor: se le asigna a todas. + +A parte de poder acceder a través de los objetos de la clase, es posible +acceder directamente desde la clase a través de su nombre, como a la hora de +acceder a las funciones de clase: `Dog.type` resultaría en `"canine"`. + +> NOTA: Si en algún caso python viera que un objeto tiene propiedades y +> variables de clase definidas con el mismo nombre, cosa que no debería ocurrir +> a menudo, tendrán preferencia las propiedades. + +### Encapsulación explícita + +Es posible que te encuentres en alguna ocasión con métodos o propiedades, +*campos* en general, cuyo nombre comience por `_` o por `__`. Se trata de casos +en los que esas propiedades o métodos quieren ocultarse del exterior. + +El uso de `_` al inicio del nombre de un campo es una convención que avisa de +que este campo no debe accederse desde el exterior de la clase y su objetivo es +usarlo desde el interior de ésta. + +Esta convención se llevó al extremo en algún momento y se decidió crear un caso +en el que esta convención inicial tuviera cierta funcionalidad añadida para las +dobles barras bajas (`__`) que impidiera un acceso accidental a esos campos +conocido como *name mangling*. + +#### Campos privados: *name mangling* + +El *name mangling* es un truco que hace python para asegurarse de que no se +entra por accidente a las secciones que empiezan por `__`. Añade +`_nombredeclase` al inicio de los campos, transformando su nombre final y +dificultando el acceso por accidente. + +Ese acceso accidental no sólo es para que el programador no acceda, ya que, si +se esfuerza la suficiente, va a poder hacerlo de igual modo, si no para que el +propio python no acceda al campo que no corresponde. El hecho de añadir el +nombre de la clase al campo crea una brecha en la herencia, haciendo que los +campos no se hereden de la forma esperada. + +En una subclase en la que los campos de la clase madre han sido marcados con +`__`, la herencia hace que estos campos se hereden con el nombre cambiado que +contiene el nombre de la superclase. De este modo, es difícil para la subclase +pisar estos campos ya que tendría que definirlos manualmente con el nombre +cambiado. Crear nuevos campos con `__` no funcionaría, ya que, al haber +cambiado de clase, el nombre generado será distinto. + +Este mecanismo es un truco para crear *campos privados*, concepto bastante +común en otros lenguajes como Java o C++, que en python es inexistente. + +El concepto de los *campos privados* es interesante en la programación +orientada a objetos. Pensando en la *encapsulación*, es lógico que a veces las +clases definan métodos o propiedades que sólo los objetos creados a partir de +ellas conozcan y que los objetos creados de clases heredadas no. Este es el +método que python tiene para aportar esta funcionalidad. + +Es interesante añadir, por otro lado, que python es un lenguaje de programación +muy dinámico por lo que la propia definición de las clases, y muchas cosas más, +puede alterarse una vez creadas. Esto significa que el hecho de ocultar campos +no es más que un acuerdo tácito entre programadores porque, si quisieran, +podrían definir todo de nuevo. Trucos como este sirven para que el programador +sea consciente de que está haciendo cosas que se supone que no debería hacer. +Cuando programes en python, tómate esto como pistas que te indican cómo se +supone que deberías estar usando las clases. + +### Acceso a la superclase + +A pesar de la herencia, no siempre se desea eliminar por completo la +funcionalidad de un método o pisar una propiedad. A veces es interesante +simplemente añadir funcionalidad sobre un método o recordar algún valor +definido en la superclase. + +Python soporta la posibilidad de llamar a la superclase mediante la función +`super`, que permite el acceso a cualquier campo definido en la superclase. + +``` python +class Clase( SuperClase ): + def metodo(self, arg): + super().metodo(arg) # Llama a la definición de + # `metodo` de `SuperClase` +``` + +> NOTA: `super` busca la clase previa por preferencia, si usas herencias +> múltiples y pisas los campos puede complicarse. + + +## Interfaces estándar: Duck Typing + +Una de las razones principales para usar programación orientada a objetos es +que, si se eligen los métodos con precisión, pueden crearse estructuras de +datos que se comporten de similar forma pero que tengan cualidades diferentes. +Independientemente de cómo estén definidas sus clases, si dos objetos disponen +de los mismos métodos podrán ser sustituidos el uno por el otro en el programa +y seguirá funcionando aunque su funcionalidad cambie. + +Dicho de otra forma, dos objetos (o dos cosas, en general) podrán ser +intercambiados si disponen de la misma *interfaz*. *Interfaz*, de *inter*: +entre; y *faz*: cara, viene a significar algo así como «superficie de contacto» +y es la palabra que se usa principalmente para definir la frontera compartida +entre dos componentes o, centrándonos en el caso que nos ocupa, su conexión +funcional. + +Si recuerdas la *herencia* y la combinas con estos conceptos, puedes +interpretar que además de una metodología para reutilizar código es una forma +de crear nuevas definiciones que soporten la misma interfaz. + +En otros lenguajes de programación, Java, por ejemplo, existe el concepto +*interfaz* que serían una especie pequeñas clases que definen qué funciones +debe cumplir una clase para que cumpla la interfaz. A la hora de crear las +clases se les puede indicar qué interfaces implementan y el lenguaje se encarga +de asegurarse de que el programador ha hecho todo como debe. + +El dinamismo de python hace que esto sea mucho más flexible. Debido a que +python no hace casi comprobaciones antes de ejecutarse, necesita un método para +mucho más directo. Para python, *si anda como un pato, vuela como un pato y +nada como un pato: es un pato*. + +Python usa lo que en la terminología del lenguaje se conoce como +*protocolos*[^protocol] (*protocol*) para que los objetos creados por el +programador puedan comportarse como los que el propio sistema aporta. Por +ejemplo, que sea posible utilizarlos como iterable en un `for`, que el sistema +pueda cerrarlos de forma automática, buscar en ellos usando el operador `in`, +etc. Simplemente, el sistema define qué funciones se deben cumplir en cada uno +de esos casos y cuando se encuentre con ellos intentará llamarlas +automáticamente. Si el elemento no dispone de esas funciones lanzará una +excepción como la que lanza cuando intentamos acceder a un método que no existe +(que es básicamente lo que estamos haciendo en este caso). + +> TODO: En realidad no se llaman protocolos todos ellos. Se llama así sólo al +> *iterator protocol*. En realidad se llaman: [Special Method +> Names](https://docs.python.org/3/reference/datamodel.html#special-method-names) + +En general, python, con el fin de diferenciar claramente qué nombres elige el +programador y cuales han sido seleccionados por el lenguaje, suele utilizar una +convención para la nomenclatura: comienzan y terminan por: `__` + +A continuación se describen algunos de los protocolos más comunes, algunos ya +han aparecido a lo largo de los ejemplos del documento, otros las verás por +primera vez ahora. Existen muchos más, y todos están extremadamente bien +documentados. Si en algún momento necesitas crear algunos nuevos, la +documentación de python es una buena fuente donde empezar. + +Todos las protocolos se presentan con un nombre, en muchos casos inventado, +terminado en *-able*. Python utiliza también este tipo de nombres, como el ya +aparecido *llamable*, o *callable* en inglés, que se refiere a cualquier cosa +que puede ser llamada. Representar los nombres de esta manera sirve para +expresar el interés de los protocolos. Si en algún momento necesitas crear una +clase que defina un objeto en el que se puede buscar necesitas que sea un +*buscable*, es decir, que soporte el protocolo que define ese comportamiento. + +[^protocol]: **Protocolo**: 5. m. Inform. Conjunto de reglas que se establecen + en el proceso de comunicación entre dos sistemas. — RAE [Consultado + 01-12-2019]: + +### *Representable*: `__repr__` + +Este protocolo sirve para otorgar a python una forma de representar estos +objetos. Al ejecutar la función `print` o al exponer valores en la REPL +(recuerda que la P significa print), python trata de visualizarlos. + +La el método `__repr__` se ejecuta justo antes de imprimirse el objeto, de +forma automática. La función requiere que se devuelva un elemento de tipo +string, que será el que después se visualice. + +En el ejemplo a continuación se comienza con la clase `Dog` vacía y se +visualiza una de sus instancias. Posteriormente, se reasigna la función +`__repr__` de `Dog` con una función que devuelve un string. Al volver a mostrar +a `bobby` el resultado cambia. + +Como se ve en el ejemplo, es interesante tener una buena función de +representación si lo que se pretende es entender el contenido de los objetos. + +> NOTA: Python ya aporta una forma estándar de representar los objetos, si la +> función `__repr__` no se define simplemente se usará la forma estándar. + +``` python +>>> class Dog: +... pass +... +>>> bobby = Dog() +>>> bobby +<__main__.Dog object at 0x7fb7fba1b908> + +>>> Dog.__repr__ = lambda self: "Dog called: " + self.name +>>> bobby.name = "Bobby" +>>> bobby +Dog called: Bobby +>>> +``` + +### *Contable*: `__len__` + +En python se utiliza la función `len` para comprobar la longitud de cualquier +elemento contable. Por ejemplo: + +``` python +>>> len( (1,2,3) ) +3 +``` + +Las objetos que soporten esta función podrán contarse para conocer su longitud +mediante la función `len`. Python llamará al método `__len__` del objeto (que +se espera que devuelva un número entero) y ésta será su longitud. Siguiendo con +el ejemplo del protocolo anterior: + +``` python +>>> Dog.__len__ = lambda self: 12 # Siempre devuelve 12 +>>> len(bobby) +12 +``` + +Este protocolo permite crear elementos contables, en lugar de los típicos +diccionario, tupla y lista. Como por ejemplo los ya existentes `NamedTuple`, +`OrderedDict` y otros. Los protocolos para el *buscable* e *iterable* también +son muy interesantes para esta labor. + +### *Buscable*: `__contains__` + +El método `__contains__` debe devolver `True` o `False` y recibir un argumento +de entrada. Con esto el objeto será capaz de comprobarse con sentencias que +hagan uso del operador `in` (y `not in`). Las dos llamadas del ejemplo son +equivalentes. La segunda es lo que python realiza internamente al encontrarse +el operador `in` o el operador `not in`. + +``` python +>>> 1 in [1,2,3] +True +>>> [1,2,3].__contains__(1) +True +``` + +### *Iterable*: `__next__` e `__iter__` + +El protocolo iterable permite crear objetos con los que es posible iterar en +bucles `for` y otras estructuras. Por ejemplo, los archivos de texto en python +soportan este protocolo, por lo que pueden leerse línea a línea en un bucle +`for`. + +Igual que en el caso del protocolo `__len__`, que servía para habilitar la +llamada a la función `len`, `__iter__` y `__next__` sirven, respectivamente, +para habilitar las llamadas a `iter` y `next`. + +La función `iter` sirve para convertir el elemento a *iterable*, que es una +clase que soporte el funcionamiento de la función `next`. Y `next` sirve para +pasar al siguiente elemento de un iterable. Ejemplificado: + +``` python +>>> l = [1,2,3] +>>> next(l) +Traceback (most recent call last): + File "", line 1, in +TypeError: 'list' object is not an iterator +>>> it = iter(l) +>>> it + +>>> +>>> next(it) +1 +>>> next(it) +2 +>>> next(it) +3 +>>> next(it) +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` + +La función `__next__` tiene un comportamiento muy sencillo. Si hay un próximo +elemento, lo devuelve. Si no lo hay lanza la excepción `StopIteration`, para +que la capa superior la capture. + +Fíjate que la lista por defecto no es un iterable y que se debe construir un +elemento iterable desde ella con `iter` para poder hacer `next`. Esto se debe a +que la función `iter` está pensada para restaurar la posición del cursor en el +primer elemento y poder volver a iniciar la iteración. + +Sorprendentemente, este es el procedimiento de cualquier `for` en python. El +`for` es una estructura creada sobre un `while` que construye iterables e +itera sobre ellos automáticamente. + +Este bucle `for`: + +``` python +for el in secuencia: + # hace algo con `el` +``` + +Realmente se implementa de la siguiente manera: + +``` python +# Construye un iterable desde la secuencia +iter_obj = iter(secuencia) + +# Bucle infinito que se rompe cuando `next` lanza una +# excepción de tipo `StopIteration` +while True: + try: + el = next(iter_obj) + # hace algo con `el` + except StopIteration: + break +``` + +Así que, si necesitas una clase con capacidad para iterarse sobre ella, puedes +crear un pequeño iterable que soporte el método `__next__` y devolver una +instancia nueva de éste en el método `__iter__`. + +### *Creable*: `__init__` + +El método `__init__` es uno de los más usados e interesantes de esta lista, esa +es la razón por la que ha aparecido en más de una ocasión durante este +capítulo. + +El método `__init__` es a quién se llama al crear nuevas instancias de una +clase y sirve para *ini*cializar las propiedades del recién creado objeto. + +Cuando se crean nuevos objetos, python construye su estructura en memoria, +pidiéndole al sistema operativo el espacio necesario. Una vez la tiene, envía +esa estructura vacía a la función `__init__` como primer argumento para que sea +ésta la encargada de rellenarla. + +Como se ha visto en algún ejemplo previo, el método `__init__` (es un método, +porque el objeto, aunque vacío, ya está creado) puede recibir argumentos de +entrada adicionales, que serán los que la llamada al nombre de la clase reciba, +a la hora de crear los nuevos objetos. Es muy habitual que el inicializador +reciba argumentos de entrada, sobre todo argumentos con nombre, para que el +programador que crea las instancias tenga la opción de inicializar los campos +que le interesen. + +Volviendo a un ejemplo previo: + +``` python +class Dog: + type = "canine" + def __init__(self, name): + self.name = name + def bark(self): + print("Woof! My name is " + self.name) + +bobby = Dog("Bobby") # Aquí se llama a __init__ +``` + +El nombre del perro, `"Bobby"` será recibido por `__init__` en el argumento +`name` e insertado al `self` mediante `self.name = name`. De este modo, esa +instancia de `Dog`, `bobby`, tomará el nombre `Bobby`. + +> NOTA: En muchas ocasiones, el método `__init__` inicializa a valores vacíos +> todas las posibles propiedades del objeto con el fin de que quien lea el +> código de la clase sea capaz de ver cuáles son los campos que se utilizan en +> un primer vistazo. Es una buena práctica listar todos los campos posibles en +> `__init__`, a pesar de que no se necesite inicializarlos aún, con el fin de +> facilitar la lectura. + +> NOTA: Quien tenga experiencia con C++ puede equivocarse pensando que +> `__init__` es un constructor. Tal y como se ha explicado anteriormente, al +> método `__init__` ya llega un objeto construido. El objetivo de `__init__` es +> inicializar. En python el constructor, que se encarga de crear las instancias +> de la clase, es la función `__new__`. + +> NOTA: Si creas una clase a partir de la herencia y sobreescribes su método +> `__init__` es posible que tengas que llamar al método `__init__` de la +> superclase para inicializar los campos asociados a la superclase. Recuerda +> que puedes acceder a la superclase usando `super`. + +### *Abrible* y *cerrable*: `__enter__` y `__exit__` + +Este protocolo permite que los objetos puedan ser abiertos y cerrados de forma +segura y con una sintaxis eficiente. Aunque no se van a listar en profundidad, +el objetivo de este punto es mostrar la sentencia `with` que se habilita +gracias a estos protocolos y mostrar cómo facilitan la apertura y cierre. + +El PEP 343[^pep343] muestra en detalle la implementación de la sentencia +`with`. Simplificándolo y resumiéndolo, `with` sirve para abrir elementos y +cerrarlos de forma automática. + +> NOTA: Los PEP (*Python Enhancement Proposals*) son propuestas de mejora para +> el lenguaje. Puedes consultar todos en la web de python. Son una fuente +> interesante de información y conocimiento del lenguaje y de programación en +> general. +> + +Pensando en, por ejemplo, la lectura de un archivo, se requieren varias etapas +para tratar con él, por ejemplo: + +``` python +f = open("file.txt") # apertura del fichero +f.read() # lectura +f.close() # cierre +``` + +Este método es un poco arcaico y peligroso. Si durante la lectura del fichero +ocurriera alguna excepción el fichero no se cerraría, ya que la excepción +bloquearía la ejecución del programa. Para evitar estos problemas, lo lógico +sería hacer una estructura `try-except` y añadir el cierre del fichero en un +`finally`. + +La sentencia `with` se encarga básicamente de hacer eso y facilita la escritura +de todo el proceso quedándose así: + +``` python +with f as open("file.txt"): # apertura + f.read() # en este cuerpo `f` está abierto + +# Al terminar el cuerpo, de forma normal o forzada, +# `f` se cierra. +``` + +Ahora bien, para que el fichero pueda ser abierto y cerrado automáticamente, +deberá tener implementados los métodos `__enter__` y `__exit__`. En el PEP 343 +se muestra la equivalencia entre la sentencia `with` y el uso de `__enter__`, +`__close__` y el `try-except`. + +[^pep343]: Puedes leer el contenido completo del PEP en: + + +### *Callable*: `__call__` + +Queda pendiente desde el capítulo sobre funciones, responder a lo que es un +*callable* o *llamable*. Una vez llegados a este punto, tiene una respuesta +fácil: un *llamable* es un objeto que soporta el protocolo correspondiente, +definido por el método `__call__`. + +Aunque pueda parecer sorprendente, las funciones en python también se llaman de +este modo, así que realmente son objetos que se llaman porque soportan este +protocolo. Es lógico, porque las funciones, recuerda el capítulo previo, pueden +guardar valores, como el contexto en el que se crean (*closure*). Las funciones +son meros *llamables* y como tales se comportan. + +Llevado más allá, los tipos básicos de python están definidos en clases +también, lógicamente, pero pueden ser llamados para hacer conversiones tal y +como vimos en el capítulo sobre datos. Simplemente, soportan el protocolo +*llamable*. + +``` python +>>> class Dog: +... def __call__(self): +... print("Dog called") +... +>>> dog = Dog() +>>> dog() +Dog called +``` + +Ten en cuenta que el método `__call__` puede recibir cualquier cantidad de +argumentos como ya hemos visto en apartados anteriores, pero el primero será el +propio objeto que está siendo llamado, el `self` que ya conocemos. + +Resumiendo, el método `__call__` describe cómo se comporta el objeto cuando se +le aplican las paréntesis. + +### *Subscriptable*: `__getitem__`, `__setitem__` y `__delitem__` + +Tal y como el método anterior describía cómo se aplican las paréntesis a un +objeto, el protocolo que se muestra en este apartado describe el comportamiento +del objeto cuando se le aplican los corchetes. Recordando el capítulo sobre +datos, los corchetes sirven para acceder a valores de las listas, tuplas, +diccionarios y sets, que resultan ser también un tipo de objeto que describe +este comportamiento mediante el protocolo que tenemos entre manos. + +Cuando python encuentra que se está tratando de acceder a un campo de un objeto +mediante los corchetes llama automáticamente al método `__getitem__` y cuando +se intenta asociar un campo a un valor llama al método `__setitem__` del +objeto. Al pedir la eliminación de un campo del objeto con la sentencia `del`, +se llama al método `__delitem__`. + +Aunque en otros protocolos aquí descritos hemos inventado un nombre para este +documento, Python a este protocolo le denomina *subscriptable* así que cuando +intentes acceder usando corchetes a un objeto que no soporta el protocolo, el +error que saltará te utilizará la misma nomenclatura que nosotros. + +El siguiente ejemplo muestra el protocolo en funcionamiento en una clase sin +funcionamiento alguno. Lo lógico y funcional sería utilizar estos dos métodos +para facilitar el acceso a campos de estas clases o para crear clases que +pudiesen sustituir a listas, tuplas, diccionarios o sets de forma sencilla. + +``` python +>>> class Dog: +... def __getitem__(self, k): +... print(k) +... def __setitem__(self, k, v): +... print(k, v) +... +>>> bobby = Dog() +>>> bobby["field"] +field +>>> bobby["field"] = 10 +field 10 +``` + +Fíjate en que reciben diferente cantidad de argumentos de entrada cada uno de +los métodos. El método `__setitem__` necesita indicar no sólo qué *item* desea +alterarse, sino su también su valor. + +#### *Slice notation* + +Se trata de una forma avanzada de seleccionar las posiciones de un objeto, el +nombre viene de *slice*, rebanada, y significa que puede coger secciones del +objeto en lugar de valores únicos. Piénsalo como en una barra de pan cortada en +rebanadas de la que quieres seleccionar qué rebanadas te interesan en bloque. + +No todos los objetos soportan *slicing*, pero los que lo hacen permiten acceder +a grupos de valores en el orden en el que están indicando el inicio del grupo +(inclusive), el final (no inclusive) y el salto de un elemento al siguiente. + +Además, los valores del *slice* pueden ser negativos. Añadir un número negativo +al salto implica que el salto se hace hacia atrás. Añadirlo en cualquier de los +otros dos valores, inicio o final de grupo, implica que se cuenta el elemento +desde el final de la colección en dirección opuesta a la normal. + +La sintaxis de los *slice*s es la siguiente: `[inicio:fin:salto]`. +Cada uno de los valores es opcional y si no se añaden se comportan de la +siguiente manera: + +- Inicio: primer elemento +- Fin: último elemento inclusive +- Salto: un único elemento en orden de cabeza a cola + +> NOTA: El índice para representar el último elemento es -1, pero si se quiere +> indicar como final, usar -1 descartará el último elemento porque el final no +> es inclusivo. Para que sea inclusivo es necesario dejar el campo fin vacío. + +Dada una lista de los números naturales del 1 al 99, ambos incluidos, de +nombre `l` se muestran unos casos de *slicing*. + +``` python +>>> l[-5:] +[95, 96, 97, 98, 99] +>>> l[6:80:5] +[6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61, 66, 71, 76] +>>> l[60:0:-5] +[60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5] +``` + +La sintaxis de los *slice*s mostrada sólo tiene sentido a la hora de acceder a +los campos de un objeto, si se trata de escribir suelta lanza un error de +sintaxis. Para crear *slice*s de forma separada se construyen mediante la +clase `slice` de la siguiente manera: `slice(inicio, fin, salto)`. + +En los métodos del protocolo *subscriptable* (`__getitem__`, `__setitem__` y +`__delitem__`) a la hora de elegir un *slice* se recibe una instancia del tipo +*slice* en lugar de una selección única como en el ejemplo previo: + +``` python +>>> class Dog: +... def __getitem__(self, item): +... print(item) +... +>>> bobby = Dog() +>>> bobby[1:100] +slice(1, 100, None) +>>> bobby[1:100:9] +slice(1, 100, 9) +>>> bobby[1:100:-9] +slice(1, 100, -9) +``` + +Por complicarlo todavía más, los campos del *slice* creado desde la clase +`slice` pueden ser del tipo que se quiera. El formato de los `:` es únicamente +*sintactic sugar* para crear *slices* de tipo integer o string. Aunque después +es responsabilidad del quien implemente el protocolo soportar el tipo de +*slice* definido, es posible crear *slices* de lo que sea, incluso anidarlos. + +Como ejemplo de un caso que utiliza *slices* no integer, los tipos de datos +como los que te puedes encontrar en la librería `pandas` soportan *slicing* +basado en claves, como si de un diccionario se tratara. + +### Ejemplo de uso + +Para ejemplificar varios de estos protocolos, tomamos como ejemplo una pieza de +código fuente que quien escribe este documento ha usado en alguna ocasión en su +trabajo como desarrollador. + +Se trata de un iterable que es capaz de iterar en un sistema de ficheros +estructurado en carpetas *año-mes-día* con la estructura `AAAA/MM/DD`. Este +código se creó para analizar datos que se almacenaban de forma diaria en +carpetas con esta estructura. Diariamente se insertaban fichero a fichero por +un proceso previo y después se realizaban análisis semanales y mensuales de los +datos. Esta clase permitía buscar por las carpetas de forma sencilla y obtener +rápidamente un conjunto de carpetas que procesar. + +El ejemplo hace uso del módulo `datetime`, un módulo de la librería estándar +que sirve para procesar fechas y horas. Por ahora, puedes ver la forma de +importarlo como una receta y en el siguiente capítulo la entenderás a fondo. El +funcionamiento del módulo es sencillo y puedes usar la ayuda para comprobar las +funciones que no conozcas. + +Te animo a que analices el comportamiento del ejemplo, viendo en detalle cómo +se comporta. Como referencia, fuera de la estructura de la clase, en las +últimas líneas, tienes disponible un bucle que puedes probar a ejecutar para +ver cómo se comporta. + +``` {.python .numberLines} +from datetime import timedelta +from datetime import date + +class dateFileSystemIterator: + + """ + Iterate over YYYY/MM/DD filesystems or similar. + """ + def __init__( self, start = date.today(), end = date.today(), + days_step = 1, separator = '/'): + self.start = start + self.current = start + self.end = end + self.separator = separator + self.step = timedelta( days = days_step ) + + def __iter__( self ): + self.current = self.start + return self + + def __next__( self ): + if self.current >= self.end: + raise StopIteration + else: + self.current += self.step + datestring = self.current - self.step + datestring = datestring.strftime( "%Y" \ + + self.separator \ + + "%m"+self.separator \ + +"%d") + return datestring + + def __repr__( self ): + out = self.current - self.step + tostring = lambda x: x.strftime("%Y" \ + + self.separator \ + + "%m" \ + + self.separator + "%d") + return "" \ + + "," \ + + "," \ + + "," + + +it = dateFileSystemIterator(start = date.today() - timedelta(days=30)) +print(it) +for i in it: + print(i) +``` + +#### Ejercicio libre: `yield` y los generadores + +La parte de la iteración del ejemplo previo puede realizarse forma más breve +mediante el uso de la sentencia `yield`. Aunque no la trataremos, `yield` +habilita muchos conceptos interesantes, entre ellos los *generadores*. + +A continuación tienes un ejemplo de cómo resolver el problema anterior mediante +el uso de esta sentencia. Te propongo como ejercicio que investigues cómo +funciona buscando información sobre los *generadores* (*generator*) y la +propia sentencia `yield`. + +``` python +from datetime import datetime, timedelta + +def iterate_dates( date_start, date_end=datetime.today(), + separator='/', step=timedelta(days=1)): + date = date_start + while date < date_end: + yield date.strftime('%Y'+separator+'%m'+separator+'%d') + date += step +``` + +`yield` tiene mucha relación con las *corrutinas* (*coroutine*) que, aunque no +se tratarán en este documento, son un concepto muy interesante que te animo a +investigar. Si lo haces, verás que los generadores son un caso simple de una +corrutina. + +## Lo que has aprendido + +Este capítulo también ha sido intenso como el anterior, pero te prometo que no +volverá a pasar. El interés principal de este capítulo es el de hacerte conocer +la programación orientada a objetos y enseñarte que en python lo inunda todo. +Todo son objetos. + +Para entenderlo has comenzado aprendiendo lo que es la programación orientada a +objetos, concretamente la orientada a clases, donde has visto por primera vez +los conceptos de identidad propia, comportamiento y estado. + +Desde ahí has saltado al fundamento teórico de la programación orientada a +objetos y has visitado la encapsulación, la herencia y el polimorfismo para +luego, una vez comprendidos, comenzar a definir clases en python. + +Esto te ha llevado a necesitar conocer qué es el argumento que suele llamarse +`self`, una excusa perfecta para definir qué son las variables y funciones de +clase y en qué se diferencian de las propiedades y métodos. + +Como la encapsulación no se había tratado en detalle aún, lo próximo que has +hecho ha sido zambullirte en los campos privados viendo cómo python los crea +mediante un truco llamado *name mangling* y su impacto en la herencia. + +Aunque en este punto conocías el comportamiento general de la herencia hacia +abajo, necesitabas conocerlo hacia arriba. Por eso, ha tocado visitar la +función `super` en este punto, función que te permite acceder a la superclase +de la clase en la que te encuentras. En lugar de contártela en detalle, se te +ha dado una pincelada sobre ella para que tú investigues cuando lo veas +necesario, pero que sepas por dónde empezar. + +Para describir más en detalle lo calado que está python de programación +orientada a objetos necesitabas un ejemplo mucho más agresivo: los protocolos. +A través de ellos has visto cómo python recoge las funcionalidades estándar y +te permite crear objetos que las cumplan. Además, te ha servido para ver que +**todo** en python es un objeto (hasta las clases lo son[^objects]) y para ver +formas elegantes de resolver problemas comunes, como los iteradores, `with` y +otros. + +También, te recuerdo que, aunque sea de forma colateral y sin prestarle +demasiada atención, se te ha sugerido que cuando programamos no lo hacemos +únicamente para nosotros mismos y que la facilidad de lectura del código y la +preparación de éste para que otros lo usen es primordial. Los próximos +capítulos tratan en parte de ésto: de hacer uso del patrimonio tecnológico de +la humanidad, y de ser parte de él. + +[^objects]: Puedes preguntárselo a python: + ``` python + >>> class C: pass + ... + + >>> isinstance(C, object) + True + ``` -- cgit v1.2.3