Ahora que hemos llegado hasta aquí, vamos a incluir un pequeño paréntesis para repasar un concepto que ya hemos abordado con anterioridad en este manual: los módulos de Python y la declaración import, (https://conocepython.blogspot.com/2016/12/t1-importaciones-haciendo-crecer-tu.html) y (https://conocepython.blogspot.com/2016/10/t1-modulos-de-python-archiva-y.html) para estudiar su comportamiento cuando trabajamos con la Programación Orientada a Objetos.
¿Y cuál es el motivo? Pues porque ya que nos hemos adentrado en la POO nos vendrá muy bien tenerlo en cuenta cuando codifiquemos programas grandes para localizar las clases, reutilizarlas en otros programas y editarlas si se diera el caso.
Comenzamos por lo básico. En síntesis, un módulo de Python no es otra cosa que un archivo de Python almacenado en algún directorio de nuestro disco duro con la extensión .py (o con otras extensiones asociadas, como .pym, .pypi, .pyc, etc.), de tal forma que un archivo de Python equivale a un módulo de Python, y quinientos archivos de Python, cada uno de ellos, convenientemente guardados en memoria con su extensión .py, equivale a otros tantos quinientos módulos de Python.
Esto es, n archivos Python = n módulos Python.
Ahora fijémonos en lo siguiente: si tenemos n módulos, 2, 4, 10,... los que sean, almacenados en una misma carpeta o directorio, tenemos la opción de llamar y cargar una clase (o las que fueran necesarias si precisáramos de más de una) que hayamos modelizado (construido) en cualquiera de ellos para utilizarlo en cualquier otro módulo que nos convenga mediante la declaración import que ya conocemos.
Apoyémonos en un ejemplo práctico: supongamos que creamos un directorio en nuestro disco duro al que ponemos por nombre "GESTIÓN DE EMPRESA", y en el que almacenamos cuatro módulos: base_de_datos.py, empleados.py, productividad.py y contrataciones.py.
Ya hemos mencionado que la declaración import nos permite importar módulos enteros, relacionado con el concepto de importación absoluta, dado que lo importamos todo, y/o clases y funciones, relacionado con el concepto de importación relativa dado que importamos sólo una parte de un módulo completo, entre distintos módulos. Pues bien, resulta que en el módulo base_de_datos.py tenemos creada una clase que hemos llamado CargosDeEmpresa, (class CargosDeEmpresa()). Y resulta que estamos editando/trabajando en otro módulo del mismo directorio "GESTIÓN DE EMPRESA", en concreto, el módulo empleados.py. Y mira por dónde, nos vendría de perlas aprovechar la clase CargosDeEmpresa modelizada en el módulo base_de_datos.py y aprovechar sus métodos instanciados aquí como Dios manda para reutilizarlos en nuestro módulo actual de trabajo, recordemos, empleados.py.
¿Cómo hacemos para no tener que repetirnos, para no recurrir a un "corta y pega", un "copy/paste", que nos engorde el código como un conejo enjaulado?
Pues echando mano de nuestra heroína: la declaración import.
Existen diferentes formas de de emplear la declaración import para cumplir con nuestro objetivo. Vamos a verlas.
TENGAMOS EN CUENTA PARA ENTENDER BIEN EL EJEMPLO QUE ESTAMOS CODIFICANDO DESDE EL MÓDULO empleados.py, QUE ES A DONDE QUEREMOS IMPORTAR LA CLASE CargosDeEmpresa QUE, ORIGINALMENTE, HEMOS MODELIZADO CON ANTERIORIDAD EN EL MÓDULO base_de_datos.py.
OPCIÓN A)
import base_de_datos
datbas = base_de_datos.CargosDeEmpresa()
A partir de este instante glorioso ya podemos consultar en el módulo base_de_datos.py desde nuestro módulo actual de trabajo (empleados.py) y acceder desde éste a todas las propiedades y métodos consignados en la clase CargosDeEmpresa.
Qué duda cabe que si quisiéramos importar otra clase diferente, por ejemplo, Organigrama, no tendríamos más que repetir la sintaxis modificando únicamente el nombre de la variable, procurando que el nombre que le demos nos proporcione una idea lo más aproximada posible al contenido que va a almacenar, más el nombre de la clase:
import base_de_datos
datbas = base_de_datos.CargosDeEmpresa()
organigrama = base_de_datos.Organigrama()
OPCIÓN B)
from base_de_datos import CargosDeEmpresa
datbas = CargosDeEmpresa()
Esta sintaxis nos resultará muy útil si tan sólo queremos importar la clase (importación relativa) y no el módulo completo (importación absoluta) como en el caso anterior.
OPCIÓN C)
from base_de_datos import CargosDeEmpresa as CDE
datbas = CDE()
Observamos que esta instrucción realiza una auténtica instanciación de objeto a la manera que hemos visto hasta ahora. En esta ocasión, modificamos el nombre de la clase (es una opción que tenemos para prevenir un posible conflicto en el caso de nuestro archivo de trabajo contáramos ya con una clase homónima, es decir, con el mismo nombre) en la misma instrucción recurriendo a una función alias mediante la cláusula as.
ISLOTE DE ARENISCA DESGAJADO DEL CANTIL DE UNA PLAYA DE ARENA FÓSIL, CON MILLONES DE AÑOS DE ANTIGÜEDAD, EN LA COSTA DE TAJAO, MUNICIPIO DE ARICO, SUR DE TENERIFE. |
Como ya sabemos de la entrada dedicada a los módulos en Python, también es posible importar varias clases desde un mismo módulo escribiendo sus nombres respectivos separados por comas.
En teoría, también es posible importar todas las clases y métodos desde el módulo apuntado, es decir, aquél al que asignamos la instrucción import, recurriendo para el caso al operador comodín asterisco, *, (from base_de_datos import *, por ejemplo) aunque en la práctica no se suele utilizar e, incluso, resulta, hasta cierto punto, desaconsejable. Esto es así por varias razones:
EN CONSONANCIA CON LO PROPUESTO POR EL PROFESOR DON JESÚS CONDE, PODEMOS SENTENCIAR QUE "TODO NOMBRE USADO EN UN MÓDULO DEBE LLEGAR DE UN LUGAR (EN REFERENCIA A SU RUTA, EL PATH) BIEN ESPECIFICADO". EL PROCEDER DE ESTA MANERA REDUNDARÁ EN UNA MEJOR IDENTIFICACIÓN DE LOS NOMBRES ALMACENADOS EN EL ESPACIO DE NOMBRES (NAMESPACE) DE NUESTRO MÓDULO DE TRABAJO, CONTRIBUYENDO ASÍ A CONSERVAR LA LEGIBILIDAD DEL CÓDIGO. ASÍ QUE, TRADUCIDO AL PERRUNO, VAMOS A OLVIDARNOS POR NUESTRO BIEN DE USAR EL OPERADOR GENÉRICO * Y A FIJARNOS BIEN EN LOS NOMBRES QUE PONEMOS A NUESTROS MÓDULOS, CLASES Y MÉTODOS.
En teoría, también es posible importar todas las clases y métodos desde el módulo apuntado, es decir, aquél al que asignamos la instrucción import, recurriendo para el caso al operador comodín asterisco, *, (from base_de_datos import *, por ejemplo) aunque en la práctica no se suele utilizar e, incluso, resulta, hasta cierto punto, desaconsejable. Esto es así por varias razones:
- Como ya hemos comentado en la opción C, para impedir un solapamiento de nombres entre los elementos que pudiéramos contener en el módulo en el que estemos trabajando. Incluso con nombres de objetos/instancias ajenos como variables, listas, funciones definidas, tuplas, etc. Así salvaguardamos el namespace (espacio de nombres) de nuestro módulo.
- Para programas grandes como días sin pan, con muchas líneas de código y, sobre todo, con muchas referencias e importaciones, resultaría particularmente dificultoso, oneroso, cargante, depurar y mantener el código.
- No podríamos hacer uso del comando help() cada vez que tuviéramos la necesidad de conocer las virtudes de una determinada clase y/o método, pues con el operador genérico *, Python, el pobre, abandonado a la melancolía, no sabría qué mostrarnos.
EN CONSONANCIA CON LO PROPUESTO POR EL PROFESOR DON JESÚS CONDE, PODEMOS SENTENCIAR QUE "TODO NOMBRE USADO EN UN MÓDULO DEBE LLEGAR DE UN LUGAR (EN REFERENCIA A SU RUTA, EL PATH) BIEN ESPECIFICADO". EL PROCEDER DE ESTA MANERA REDUNDARÁ EN UNA MEJOR IDENTIFICACIÓN DE LOS NOMBRES ALMACENADOS EN EL ESPACIO DE NOMBRES (NAMESPACE) DE NUESTRO MÓDULO DE TRABAJO, CONTRIBUYENDO ASÍ A CONSERVAR LA LEGIBILIDAD DEL CÓDIGO. ASÍ QUE, TRADUCIDO AL PERRUNO, VAMOS A OLVIDARNOS POR NUESTRO BIEN DE USAR EL OPERADOR GENÉRICO * Y A FIJARNOS BIEN EN LOS NOMBRES QUE PONEMOS A NUESTROS MÓDULOS, CLASES Y MÉTODOS.
ORGANIZACIÓN DE MÓDULOS (JERARQUÍA):
Dado que es imposible de toda imposibilidad que los módulos contengan a otros módulos (y recordémoslo una vez más: un módulo en Python no es otra cosa que un simple y humilde archivo con extensión .py o extensiones asociadas, como .pyc, .pyw, etc.), debemos almacenarlos de una manera ordenada dentro de paquetes (packages) que, a su vez, contienen o pueden contener uno o varios subdirectorios. Una razón más para no recurrir al operador genérico * salvo en contadas y muy claras ocasiones y hacer caso del consejo del profesor Jesús Conde: ¿Podemos imaginarnos lo intrincado de llegar a tal o cual clase o método en un paquete que contiene varios módulos que, a su vez, contienen varias clases donde cada una de ellas, a su vez también, pueden contener varios métodos si clarificar las rutas (paths) correctamente?
Sucintamente, tal y como ya sabemos, un paquete (package) o directorio es tan sólo una colección de módulos (archivos o ficheros) relacionados entre sí. En consecuencia, el nombre del paquete será el nombre del directorio.
Para conseguir ésto tenemos que hacer lo siguiente:
- Decirle a Python que un directorio es un paquete.
- Crear un archivo específico, ad hoc obligatorio que llamamos __init__.py y que, por conveniencia, se suele dejar vacío, sin contenido, el cual debemos incluir en el paquete, en sus respectivos directorios (y subdirectorios, si los hubiera) como módulo de cabecera, esto es, el primero en crearse, el primer módulo de la lista de módulos de cada directorio/subdirectorio (no es obligatorio hacerlo así, pero sí muy recomendable y ajustado a las especificaciones de Python)
IMPORTACIONES ABSOLUTAS Y RELATIVAS:
Existen dos tipos de importaciones: las importaciones absolutas, que obligan a especificar la ruta completa al módulo o a la clase que queremos importar; y las importaciones relativas, que no necesitan referenciar la ruta completa, siendo su uso más común en programas sencillos.
A) IMPORTACIONES ABSOLUTAS:
Cuenta con tres tipos de sintaxis para conseguir el mismo objetivo:
1.- import empresa.personal.nóminas.trabajador = empresa.personal.nóminas.Personal(), donde Personal() es el nombre de una clase que llamamos a partir de aquí. Fijémonos que no existe solapamiento entre el nombre del módulo personal y el nombre de la clase Personal gracias al case sensitive, que distingue minúsculas de mayúsculas, lo que implica que para Python son dos objetos distintos.
2.- from empresa.personal.nóminas import trabajador
trabajador = Personal() → una instanciación simple.
3.- from empresa.personal import nóminas
trabajador = nóminas.Personal()
Como acabamos de ver, debemos recurrir al al método del punto (dott method o dott notation)
SE PUEDE UTILIZAR INDISTINTAMENTE CUALQUIERA DE ELLAS. EL OPTAR POR UN MODELO DE SINTAXIS U OTRO DEPENDE DEL TIPO DE PROGRAMA QUE ESTEMOS DESARROLLANDO Y DE LOS GUSTOS PERSONALES DEL PROGRAMADOR. RESULTA MÁS COMÚN, EN CUALQUIER CASO, USAR EL TERCER TIPO CUANDO TRABAJAMOS CON MUCHAS CLASES DADO QUE REFERENCIA EL DIRECTORIO O SUBDIRECTORIO DE PROCEDENCIA. CUANDO SÓLO VAMOS A IMPORTAR UNAS POCAS CLASES DE UN MÓDULO DADO, QUIZÁS FUERA MEJOR RECURRIR AL SEGUNDO TIPO, YA QUE NOS PERMITE ESPECIFICAR LA CLASE CONCRETA QUE VAMOS A UTILIZAR.
B) IMPORTACIONES RELATIVAS:
Para las importaciones relativas contamos con dos tipos de sintaxis:
1.- Llama a la clase Seleccion() desde el módulo organigrama.py en el que estamos trabajando:
from cargosdeempresa import Seleccion
cargosdeempresa.py es el módulo de procedencia que se encuentra en el mismo paquete o directorio (empresa, en el ejemplo. ¡Ojo! que personal es un subdirectorio con sus propios módulos dentro de un directorio mayor: empresa, precisamente. No nos confundamos) que organigrama.py, que es aquél en el que estamos trabajando.
2.- Llama a la clase Seleccion() desde el módulo nóminas.py.
from .. cargosdeempresa import Seleccion
Dos puntos (..): un punto por cada subdirectorio de "distancia" (de acuerdo al ejemplo) en la jerarquía: la clase invocada se ubica en un subdirectorio que está por encima del actual.
ACCESO A OBJETOS YA INSTANCIADOS EN LA CLASE:
Muy sencillo. Hasta aquí hemos visto que para importar una clase de un módulo donde ha sido modelizada a otro, no tenemos más que utilizar, precisamente, esos dos elementos que acabamos de mencionar en la sintaxis de llamada: el nombre de la clase y el módulo en el que se encuentra, ¿verdad? Pues ahora vamos a aportar el tercero: el objeto (cualquier objeto y tantos como necesitemos) propiamente dicho que hayamos podido instanciar en el módulo de origen. Es algo tan sencillo como mostramos a continuación:CÓDIGOS EJECUTABLES Y CÓMO TRATARLOS:
Expliquemos brevemente qué es ésto de un código ejecutable: son scripts de distinto tamaño que la computadora ejecuta de manera automática, leyendo las instrucciones proporcionadas por el programador y los enlaces que se hayan habilitado a las librerías propias del lenguaje en que haya sido codificado. Son característicos y distintivos de cada plataforma/sistema operativo. Suelen llevar, entre otros, las extensiones o sufijos .exe y .com.
En este manual tenemos la página EJECUTABLES O DISTRIBUIBLES DE MÓDULOS PYTHON para aprender a crear ejecutables de nuestros programas.Pues bien: se trata, precisamente, de evitar que cuando importemos un módulo para acceder a alguna de sus clases u objetos, en el caso de que contengan código ejecutable, no se ejecute en nuestro módulo de trabajo de manera indeseada.
¿Y cómo hacemos esto? Para solventar este "inconveniente" Python nos proporciona una estructura sintáctica que pasa por colocar nuestro código de inicio dentro de una función definida que, por convención, recibe el nombre de "__main__".
Vemos un ejemplo a continuación:
Como podemos ver, hemos modelizado una clase, ClaseGenerica, sin más, sin contenido alguno, y luego hemos definido una función (¡ojo!, no un método instanciado dentro de la clase porque queda fuera de su cuerpo, de su ámbito, de su scope, al no estar indentado en ella) externa que hemos llamado main y que incorpora una instancia, generico, de la clase ClaseGenerica. Este método sólo se ejecutará cuando lo utilicemos como un script, es decir, como un programa normal y corriente.
El aspecto fundamental a tener en cuenta aquí es el condicional if, al que asociamos una variable especial __name__ (aquéllas que se declaran entre dobles guiones bajos y que tienen una significación específica en Python. No las confundamos con los métodos especiales de Python, como __new__() e __init__(), que ya conocemos, y que se distinguen porque, como todas las funciones/métodos de Python, terminan con un doble paréntesis, ()) que especifica el nombre de la función.
Al establecer un criterio de identidad entre los nombres mediante el operador de comparación ==, cada vez que se establezca la coincidencia, se ejecutará el script irremisiblemente. De esta manera, conseguiremos que el código ejecutable corra sólo cuando se invoque al módulo como script, y no cuando se haga lo propio para importar a nuestro módulo de trabajo.
La sintaxis if __name__ == "__main__", o simplemente, main() en Python 3 (v. LA FUNCIÓN MAIN()) deberíamos integrarla por defecto y a modo de control de seguridad, cada vez que construyamos un módulo para garantizar lo antedicho.
Lo más común en programación con Python es que las clases se modelicen dentro de módulos, tal y como hemos visto hasta ahora, como Dios manda y mandan los cánones, siguiendo una suerte de jerarquía.
Sin embargo, ¡oh, maravilla!, una clase puede ser modelizada dentro de una función:
El ejemplo que hemos puesto procede del videotutorial nº 5 del curso de Python 3 del profesor don Jesús Conde y es propiedad intelectual suya (jamás recomendaremos lo suficiente su canal: i-m-p-r-e-s-c-i-n-d-i-b-l-e).
Vamos ahora a destriparlo un poquito:
En 1. definimos una función de la manera habitual, tal y como hemos aprendido a hacerlo en este manual, y le asignamos dos parámetros en su zona de parámetros correspondiente:un objeto de tipo string, obligatorio; y un objeto que llamamos formatter al que le asignamos el valor None para inicializar el objeto mismo, dado que se trata de un parámetro opcional y en Python se recomienda por los programadores más expertos inicializar valores opcionales en una zona de parámetros con esta cláusula cuando no se parte de un valor determinado.
A continuación, en 2., modelizamos la clase con la sintaxis típica. El nombre que le proporcionamos es DefaultFormatter, que ya nos pone sobre la pista sobre su función: "Formateo por defecto, de manera predeterminada".
En 3. instanciamos un método con la sintaxis consabida. Llevará el nombre de format y contará, a parte del parámetro self de autorreferencia obligatorio, un único parámetro, también obligatorio, fijémonos en ello, y que corresponde al propio objeto string, la cadena de caracteres, vamos, sobre el que deseamos aplicar el método.
Ya en 4. cerramos como pie de la función con una sentencia return para que nos devuelva el resultado de la ejecución del método. ¿Cual? Pues un nuevo objeto de tipo string, para el que invocamos la función integrada, conversora, str(). ¿Y qué objeto será éste? Pues el resultado de aplicarle a la string que pasamos como argumento en la zona de parámetros del método format de la clase DefaultFormatter, el método title() de las strings, que ya estudiamos en la página que dedicamos a los métodos de las strings, y que convierte en mayúsculas el primer carácter de cada palabra, esto es, aquél que vaya justo a continuación de un carácter literal vacío (o más), tras convertir en mayúscula el carácter con índice 0 sí éste no es un carácter literal vacío (o más).
Instauramos el condicional if not en 5. para dar respuesta al caso de que de que no tengamos el objeto formatter (es importante tener en cuenta que el condicional está fuera del ámbito de la clase. De hecho, se encuentra al mismo nivel de indentación o sangrado. Consecuentemente, forma parte del ámbito de la función formato_string, y señala lo que debe ejecutarse cuando el parámetro opcional formatter=None no está presente en el paso de parámetros).
A continuación, ya en 6., instanciamos el objeto formatter a partir de la clase DefaultFormatter.
En 7., finalmente, decimos qué es lo que se tiene que hacer: recurrimos, una vez más, como debe ser a la sentencia return (ojo, de la función formato_string y no del método format) que le aplicará al objeto instanciado en la línea anterior, formatter, el método format(self, string) instanciado, a su vez, en el punto 3., mediante el operador punto (dott method, dott notation, o también, sintaxis del punto, en castellano).
ACCESO A DATOS EN PYTHON
Contrariamente a lo que sucede en otros lenguajes de programación, Python no impone un criterio estricto para categorizar si un determinado dato es de acceso público, protegido o privado. Antes bien, por su condición de lenguaje de acceso libre (open source), por su flexibilidad, y desde un punto de vista más pragmático, por la dificultad intrínseca para saber de antemano si un determinado objeto instanciado a partir de una clase nos resultará útil más adelante o no, tanto en un proyecto interno como en otros externos.
Python, más bien, propone una recomendación que va desde lo más "leve", por así decirlo, a los más "exigente", recurriendo en este caso a guiones a modo de prefijos simples y dobles. Aunque aquí introduciremos los conceptos, los desarrollaremos más ampliamente en las páginas que dedicaremos a VISIBILIDAD y ENCAPSULACIÓN.Realmente, en Python, todos los métodos y propiedades instanciados en una clase estarán siempre disponibles públicamente. Si queremos limitar este acceso, podemos empezar por incluir una nota en el docstring de la clase, indicando si un método o no debe utilizarse sólo a nivel interno.
Como segundo paso y en un modo de uso, digamos, convencional, podemos recurrir al prefijado de atributos/propiedades y métodos con el carácter de 'subrayado' (guión bajo, el mismo que empleamos en los métodos especiales que nos surte el objeto object de Python), para significar su condición de uso exclusivo interno (sólo se puede usar dentro del módulo en que se ha creado). En este caso, un subrayado simple.
Aún contamos con una tercera opción, más explícita y "contundente" de subrayar la privacidad de un objeto, y que pasa por prefijar con un subrayado doble. éste aplica al nombre del objeto en cuestión lo que se denomina en programación name mangling (más o menos, en su traducción al castellano, "mutilación del nombre"). En el caso propio de Python, además, desde un punto de vista ortográfico no puede extenderse el doble subrayado al final del nombre (entonces estaríamos hablando de "variables especiales"): pueden, o no llevar nada, o llevar un subrayado simple, pero no un subrayado doble.
La aplicación del name mangling a un método o a una propiedad/atributo no significa que no pueda ser llamado desde un objeto externo (la flexibilidad de Python que mencionábamos más arriba), pero a partir del subrayado doble se impone como requisito la lectura de una instrucción adicional en el código, puesto que de no hacerlo, tal método o tal propiedad o atributo no estarían disponibles.
Podemos ver un ejemplo sencillo a continuación.
Como destacamos en el ejemplo, ese doble subrayado delante de las variables es lo que llamamos name mangling. Sin embargo, el recurso al name mangling trae como consecuencia la modificación a nivel interno del nombre de la clase, a la que añadiremos a modo de prefijo un subrayado simple. Veámoslo en el siguiente ejemplo, con una estructura muy similar al ejemplo anterior:
Esto es así sólo para llamadas desde fuera de la clase (importar la clase desde otro módulo distinto al módulo donde se modelizó originalmente). A nivel interno, la clase conserva su nombre original, sin subrayados. El control estriba en la obligación que tiene el programador de escribir ese subrayado.
SI QUEREMOS EVITARNOS ESTOS "MENEOS" LO MEJOR ES RECURRIR AL NAME MANGLING SÓLO EN OCASIONES MUY PRECISAS, SIENDO LO MÁS HABITUAL Y RECOMENDABLE CUANDO QUERAMOS ESPECIFICAR DE MANERA EXPLÍCITA LA PRIVACIDAD DE UN MÉTODO DETERMINADO O DE UNA PROPIEDAD/ATRIBUTO, EL HACERLO A TRAVÉS DE UN SUBRAYADO SIMPLE:
MEJOR _welcome QUE __welcome, EN RESUMIDAS CUENTAS.
IMPORTACIONES DESDE PAQUETES (NO DESDE MÓDULOS)
Imaginémonos el siguiente árbol de módulos (tree of modules):
Supongamos que ▶️ DEPENDENCIAS es un paquete (package) de Python que contiene dos módulos: AREAS.py y DIRECCION_AREAS.py.
El módulo DIRECCION_AREAS.py contiene a su vez una variable que denominamos gerencia y a la que se accede desde distintas partes, por lo que sería recomendable para evitar posibles solapamientos entre los espacios de nombre (namespaces) y paths (rutas) demasiado enrevesados invocarla directamente desde el paquete ▶️ DEPENDENCIAS.
Podríamos hacerlo de dos maneras:
- Como módulo: import DEPENDENCIAS.DIRECCION_AREAS.gerencia
- Como paquete: import DEPENDENCIAS.gerencia
Tendríamos en estas circunstancias que hacer la siguiente modificación: incluimos dentro de nuestro archivo DEPENDENCIAS/__init__.py y efectuaríamos la llamada a nuestra variable gerencia del siguiente modo: from DIRECCION_AREAS import gerencia.
LA CULTURA DE LA VIÑA Y EL VINO, A LO LARGO Y A LO ANCHO DE TODA LA ISLA DE TENERIFE. CEPA A RAS DE TIERRA Y UVA VERDEJA PARA CREAR CALDOS BLANCOS DE GUSTO EXQUISITO. |
Muchas gracias , excelente manera de explicar, mu didactico, felicitaciones
ResponderEliminarMuchísimas gracias. Un placer. Saludos.
ResponderEliminar