Mapa mental
Python

Contenido del mapa
Python
Tipos de datos
- Cadenas
- Simples
- Las cadenas en Python o strings son un tipo inmutable que permite almacenar secuencias de caracteres. Para crear una, es necesario incluir el texto entre comillas dobles ". Puedes obtener más ayuda con el comando help(str).
- s = "Esto es una cadena" print(s) #Esto es una cadena print(type(s)) #
- También es valido declarar las cadenas con comillas simples '. s = 'Esto es otra cadena' print(s) #Esto es otra cadena print(type(s)) #<class 'str'>
- Se puede definir una cadena que ocupe varias líneas usando triple """ comilla. Puede ser muy útil si tenemos textos muy largo que no queremos tener en una sola línea.
- Existe también otra forma de declarar cadenas llamado raw strings. Usando como prefijo r, la cadena ignora todos las secuencias de escape, por lo que la salida es diferente a la anterior. print(r"\110\110") #\110\110 print("""La siguiente cadena ocupa varias lineas""")
- Estructuras
- Listas
- Listas en Python Las listas en Python son un tipo de dato que permite almacenar datos de cualquier tipo. Son mutables y dinámicas, lo cual es la principal diferencia con los sets y las tuplas. Crear listas Python Las listas en Python son uno de los tipos o estructuras de datos más versátiles del lenguaje, ya que permiten almacenar un conjunto arbitrario de datos. Es decir, podemos guardar en ellas prácticamente lo que sea. Si vienes de otros lenguajes de programación, se podría decir que son similares a los arrays.
- Ejemplo: lista = [1, 2, 3, 4] También se puede crear usando list y pasando un objeto iterable.lista = list("1234") Una lista sea crea con [] separando sus elementos con comas ,. Una gran ventaja es que pueden almacenar tipos de datos distintos.lista = [1, "Hola", 3.67, [1, 2, 3]]
- Algunas propiedades de las listas: Son ordenadas, mantienen el orden en el que han sido definidasPueden ser formadas por tipos arbitrariosPueden ser indexadas con [i].Se pueden anidar, es decir, meter una dentro de la otra.Son mutables, ya que sus elementos pueden ser modificados.Son dinámicas, ya que se pueden añadir o eliminar elementos.
- Sets
- Los set en Python son un tipo que permite almacenar varios elementos y acceder a ellos de una forma muy similar a las listas pero con ciertas diferencias: Los elementos de un set son único, lo que significa que no puede haber elementos duplicados.Los set son desordenados, lo que significa que no mantienen el orden de cuando son declarados.Sus elementos deben ser inmutables.
- Para entender mejor los sets, es necesario entender ciertos conceptos matemáticos como la teoría de conjuntos. Para crear un set en Python se puede hacer con set() y pasando como entrada cualquier tipo iterable, como puede ser una lista. Se puede ver como a pesar de pasar elementos duplicados como dos 8 y en un orden determinado, al imprimir el set no conserva ese orden y los duplicados se han eliminado. s = set([5, 4, 6, 8, 8, 1]) print(s) #{1, 4, 5, 6, 8} print(type(s)) #
- Tuplas
- Las tuplas en Python son un tipo o estructura de datos que permite almacenar datos de una manera muy parecida a las listas, con la salvedad de que son inmutables. Crear tupla Python Las tuplas en Python o tuples son muy similares a las listas, pero con dos diferencias. Son inmutables, lo que significa que no pueden ser modificadas una vez declaradas, y en vez de inicializarse con corchetes se hace con (). Dependiendo de lo que queramos hacer, las tuplas pueden ser más rápidas.
- Diccionarios
- Los diccionarios en Python son una estructura de datos que permite almacenar su contenido en forma de llave y valor. Un diccionario en Python es una colección de elementos, donde cada uno tiene una llave key y un valor value. Los diccionarios se pueden crear con paréntesis {} separando con una coma cada par key: value.
- En el siguiente ejemplo tenemos tres keys que son el nombre, la edad y el documento. d1 = { "Nombre": "Sara", "Edad": 27, "Documento": 1003882 } print(d1) #{'Nombre': 'Sara', 'Edad': 27, 'Documento': 1003882}
- Frozensets
- Los frozenset en Python son una estructura de datos muy similar a los set, con la salvedad de que son inmutables, es decir, no pueden ser modificados una vez declarados.
- Los frozenset pueden ser útiles cuando queremos usar un set pero se requiere que el tipo sea inmutable. Algo no muy común, pero que podría darse, es crear un set de sets. Es decir, un ser que contiene como elementos a otros sets. El siguiente código daría un TypeError ya que los elementos de un set deben ser por definición inmutables. s1 = {1, 2} s2 = {3, 4} #s3 = {s1, s2} # Error! TypeError Para resolver este problema, podemos crear un set de frozensets. Esto si es posible ya que el frozenset es un tipo inmutable. s1 = frozenset([1, 2]) s2 = frozenset([3, 4]) s3 = {s1, s2} print(s3) #{frozenset({3, 4}), frozenset({1, 2})}
- Listas
- Simples
- Numéricos
- Flotantes (float)
- Puedes usar el tipo float sin problemas para representar cualquier número real (siempre teniendo en cuenta que es una aproximación lo más precisa posible). Por tanto para longitudes, pesos, frecuencias, …, en los que prácticamente es lo mismo 3,3 que 3,3000000000000003 el tipo float es el más apropiado.
- Cuando un número float vaya a ser usado por una persona, en lugar de por el ordenador, puedes darle formato al número de la siguiente manera: >>> real = 1.1 + 2.2 # real es un float >>> print(real) 3.3000000000000003 # Representación aproximada de 3.3 >>> print(f'{real:.2f}') 3.30 # real mostrando únicamente 2 cifras decimales
- Y para terminar esta sección, te adelanto que, si por cualquier motivo sí que necesitas una mayor precisión a la hora de trabajar con los números reales, Python tiene otros tipos de datos, como Decimal. El tipo Decimal es ideal a la hora de trabajar, por ejemplo, con dinero o tipos de interés. Este tipo de dato trunca la parte decimal del número para ser más preciso, pero no es el objetivo de este tutorial hablar sobre el tipo de dato Decimal.
- Enteros (int)
- El tipo de los números enteros es int. Este tipo de dato comprende el conjunto de todos los números enteros, pero como dicho conjunto es infinito, en Python el conjunto está limitado realmente por la capacidad de la memoria disponible. No hay un límite de representación impuesto por el lenguaje. Un número de tipo int se crea a partir de un literal que represente un número entero o bien como resultado de una expresión o una llamada a una función.
- Ejemplo: >>> a = -1 # a es de tipo int y su valor es -1 >>> b = a + 2 # b es de tipo int y su valor es 1 >>> print(b) 1
- Booleano
- Este tipo de dato reconoce solamente dos valores: Verdadero (True) y Falso (False)
- Complejos
- En pocas palabras, los números complejos son aquellos que tienen dos partes: Una parte real, como por ejemplo 3 o 7.5. Y otra imaginaria, como 5j o -7j. Como puedes ver, la parte imaginaria viene acompañada de j, aunque es también común usar la i (de imaginario). Y por último los números complejos son la suma de un número real y otro imaginario, dando lugar a los ya vistos 5+5j.Un número complejo tiene la siguiente forma:parte_real + parte_imaginaria*j ¿Para qué sirven los números complejos? Los números complejos son muy utilizados en las telecomunicaciones y electrónica, ya que son muy útiles para describir las ondas electromagnéticas y la corriente eléctrica.También son usados en diferentes dominios de las matemáticas y en física, donde destaca su uso en la mecánica cuántica.Un número complejo puede representarse en un plano, donde el eje x representa la parte real y el eje y la imaginaria. Es decir, se puede ver a un número complejo 5+5i como un punto de coordenadas.
- Veamos unos ejemplos: a = 5 + 5j b = 1.3 - 7j
- También podemos tener un número complejo con parte real igual a cero. c = 10.3j
- Los números complejos en Python pueden ser creados sin tener que importar ninguna librería. Basta con hacer lo siguiente: c = 3 + 5j print(c) #(3+5j) print(type(c)) # Podemos ver como la clase que representa a los complejos en Python se llama complex. Una vez creado, es posible acceder a la parte real con real y a la imaginaria con imag. c = 3 + 5j print(c.real) #3.0 print(c.imag) #5.0 También se puede crear un número complejo haciendo uso de complex, pero sin usar la j. c = complex(3,5) print(c) #(3+5j)
- Operaciones con números complejos Usando variables del tipo complex, podemos realizar las operaciones más comunes típicas de los números complejos. Suma de complejos Para sumar números complejos, se suman las partes reales por un lado, y las imaginarias por otro. a = 1 + 3j b = 4 + 1j print(a+b) #(5+4j) Resta de complejos Para restar, se restan las partes reales por un lado y las imaginarias por otro. a = 1 + 3j b = 4 + 1j print(a-b) #(-3+2j) Multiplicación de complejos La multiplicación es algo más compleja. Si multiplicamos a+bj por c+dj, se puede demostrar fácilmente que el resultado es (ac-bd) para la parte real y (ad+bc) para la imaginaria. a = 1 + 3j b = 4 + 1j print(a*b) #(1+13j) División de complejos También podemos hacer la división. Si quieres saber cómo se dividen dos números complejos y su demostración matemática, te dejamos este enlace. a = 1 + 3j b = 4 + 1j print(a/b) #(0.41+0.64j)
- Conjugado de complejos Por último, podemos realizar el conjugado de un número complejo en Python con el método conjugate(). Calcular el conjugado consiste en negar la parte imaginaria, es decir, cambiar si signo de + a - y viceversa.a = 1 + 1j print(a.conjugate()) #(1-1j) Librería cmathSi quieres realizar más operaciones con número complejos, tal vez quieras echar un vistazo a la librería cmath, que es mucho más completa, y compleja.Algunas de las cosas que puedes hacer son las siguientes:Calcular la fase, que es el ángulo que forma el vector con el eje x, en radianes.Calcular la forma polar, es decir módulo y ángulo.import cmath print(cmath.phase(complex(5, 0))) # 0.0 print(cmath.polar(complex(5, 5))) # (7.0710678118654755, 0.7853981633974483)
- Flotantes (float)
- Cadenas
Objetos
- Python es un lenguaje de programación orientado a objetos, y como tal, trata a todos los tipos de datos como objetos. Un simple entero es un objeto tal como una función tambien es un objeto
- Sabida ya la clasificación, tal vez te preguntes porqué es esto relevante. Pues bien, Python trata de manera diferente a los tipos mutables e inmutables, y si no entiendes bien este concepto, puedes llegar a tener comportamientos inesperados en tus programas.
- Identidad
- Identidad: Nunca cambia e identifica de manera unívoca al objeto. El operador is nos permite saber si dos objetos son en realidad el mismo. Es decir, si dos variables hacen referencia al mismo.
- Tipo
- Tipo: Nos indica el tipo al que pertenece, como un float o una lista. La función type() nos indica el tipo de un determinado objeto. Es la clase a la que pertenece.
- Valor
- Valor: Todo objeto tiene unas características particulares. Si estas características pueden ser modificadas, diremos que es un tipo mutable. De lo contrario, que es inmutable.
- Mutables
- Mutables: Si permiten ser modificados una vez creados.
- Son mutables los siguientes tipos: ListasBytearrayMemoryviewDiccionariosSetsY clases definidas por el usuario
- Inmutables
- Inmutables: Si no permiten ser modificados una vez creados.
- Y son inmutables: BooleanosComplejosEnterosFloatFrozensetCadenasTuplasRangeBytes
- Funciones
- Cualquier función tendrá un nombre, unos argumentos de entrada, un código a ejecutar y unos parámetros de salida. Al igual que las funciones matemáticas, en programación nos permiten realizar diferentes operaciones con la entrada, para entregar una determinada salida que dependerá del código que escribamos dentro. Por lo tanto, es totalmente análogo al clásico y=f(x) de las matemáticas.
- Anteriormente hemos usado funciones nativas que vienen con Python como len() para calcular la longitud de una lista, pero al igual que en otros lenguajes de programación, también podemos definir nuestras propias funciones. Para ello hacemos uso de def.
- Nativas
- Se llaman con palabras reservadas del lenguaje como por ejemplo len() que sirve para calcular la longitud de una lista
- Return
- El uso de la sentencia return permite realizar dos cosas: Salir de la función y transferir la ejecución de vuelta a donde se realizó la llamada.Devolver uno o varios parámetros, fruto de la ejecución de la función. En lo relativo a lo primero, una vez se llama a return se para la ejecución de la función y se vuelve o retorna al punto donde fue llamada. Es por ello por lo que el código que va después del return no es ejecutado en el siguiente ejemplo. def mi_funcion(): print("Entra en mi_funcion") return print("No llega") mi_funcion() # Entra en mi_funcion Por ello, sólo llamamos a return una vez hemos acabado de hacer lo que teníamos que hacer en la función. Por otro lado, se pueden devolver parámetros. Normalmente las funciones son llamadas para realizar unos cálculos en base a una entrada, por lo que es interesante poder devolver ese resultado a quien llamó a la función. def di_hola(): return "Hola" di_hola() # 'Hola' También es posible devolver mas de una variable, separadas por ,. En el siguiente ejemplo tenemos una función que calcula la suma y media de tres números, y devuelve su resultado. def suma_y_media(a, b, c): suma = a+b+c media = suma/3 return suma, media suma, media = suma_y_media(9, 6, 3) print(suma) # 18 print(media) # 6.0
- Personalizadas
- podemos definir nuestras propias funciones. Para ello hacemos uso de def. def nombre_funcion(argumentos): código return retorno
- Argumentos de entrada
- Pasando argumentos de entrada Empecemos por la función más sencilla de todas. Una función sin parámetros de entrada ni parámetros de salida. def di_hola(): print("Hola") Hemos declarado o definido la función. El siguiente paso es llamarla con di_hola(). Si lo realizamos veremos que se imprime Hola. di_hola() # Hola Vamos a complicar un poco las cosas pasando un argumento de entrada. Ahora si pasamos como entrada un nombre, se imprimirá Hola y el nombre. def di_hola(nombre): print("Hola", nombre) di_hola("Juan") # Hola Juan
- Por posición
- Argumentos por posición Los argumentos por posición o posicionales son la forma más básica e intuitiva de pasar parámetros. Si tenemos una función resta() que acepta dos parámetros, se puede llamar como se muestra a continuación. def resta(a, b): return a-b resta(5, 3) # 2 Al tratarse de parámetros posicionales, se interpretará que el primer número es la a y el segundo la b.
- El número de parámetros es fijo, por lo que si intentamos llamar a la función con solo uno, dará error. #resta(1) # Error! TypeError Tampoco es posible usar mas argumentos de los tiene la función definidos, ya que no sabría que hacer con ellos. Por lo tanto si lo intentamos, Python nos dirá que toma 2 posicionales y estamos pasando 3, lo que no es posible.#TypeError: resta() takes 2 positional arguments but 3 were given #resta(5,4,3) # Error
- Por nombre
- Otra forma de llamar a una función, es usando el nombre del argumento con = y su valor. El siguiente código hace lo mismo que el código anterior, con la diferencia de que los argumentos no son posicionales. resta(a=3, b=5) # -2 Al indicar en la llamada a la función el nombre de la variable y el valor, el orden ya no importa, y se podría llamar de la siguiente forma. resta(b=5, a=3) # -2 Como es de esperar, si indicamos un argumento que no ha sido definido como parámetro de entrada, tendremos un error. #resta() got an unexpected keyword argument 'c' #resta(b=5, c=3) # Error!
- Por defecto
- Tal vez queramos tener una función con algún parámetro opcional, que pueda ser usado o no dependiendo de diferentes circunstancias. Para ello, lo que podemos hacer es asignar un valor por defecto a la función. En el siguiente caso c valdría cero salvo que se indique lo contrario. def suma(a, b, c=0): return a+b+c suma(5,5,3) # 13 Dado que el parámetro c tiene un valor por defecto, la función puede ser llamada sin ese valor. suma(4,3) # 7 Podemos incluso asignar un valor por defecto a todos los parámetros, por lo que se podría llamar a la función sin ningún argumento de entrada. def suma(a=3, b=5, c=0): return a+b+c suma() # 8 Las siguientes llamadas a la función también son válidas suma(1) # 6 suma(4,5) # 9 suma(5,3,2) # 10 O haciendo uso de lo que hemos visto antes y usando los nombres de los argumentos. suma(a=5, b=3) #8
- Por longitud de variable
- En el ejemplo con argumentos por defecto, hemos visto que la función puede ser llamada con diferente número de argumentos de entrada, pero esto no es realmente una función con argumentos de longitud variable, ya que existe un número máximo. Imaginemos que queremos una función suma() como la de antes, pero en este caso necesitamos que sume todos los números de entrada que se le pasen, sin importar si son 3 o 100. Una primera forma de hacerlo sería con una lista. def suma(numeros): total = 0 for n in numeros: total += n return total suma([1,3,5,4]) # 13 La forma es válida y cumple nuestro requisito, pero realmente no estamos trabajando con argumentos de longitud variable. En realidad tenemos un solo argumento que es una lista de números. Por suerte, Python tiene una herramienta muy potente. Si declaramos un argumento con *, esto hará que el argumento que se pase sea empaquetado en una tupla de manera automática. No confundir * con los punteros en otros lenguajes de programación, no tiene nada que ver. def suma(*numeros): print(type(numeros)) # total = 0 for n in numeros: total += n return total suma(1, 3, 5, 4) # 13 El resultado es igual que el anterior, y podemos ver como efectivamente numeros es de la clase tuple. También podemos hacer otras llamadas con diferente número de argumentos suma(6) # 6 suma(6, 4, 10) # 20 suma(6, 4, 10, 20, 4, 6, 7) # 57 Usando doble ** es posible también tener como parámetro de entrada una lista de elementos almacenados en forma de clave y valor. En este caso podemos iterar los valores haciendo uso de items(). def suma(**kwargs): suma = 0; for key, value in kwargs.items(): print(key, value) suma += value return suma suma(a=5, b=20, c=23) # 48 De igual manera, podemos pasar un diccionario como parámetro de entrada. def suma(**kwargs): suma = 0 for key, value in kwargs.items(): print(key, value) suma += value return suma di = {'a': 10, 'b':20} suma(**di) # 30 S
- Principio de reusabilidad
- El principio de reusabilidad, que nos dice que si por ejemplo tenemos un fragmento de código usado en muchos sitios, la mejor solución sería pasarlo a una función. Esto nos evitaría tener código repetido, y que modificarlo fuera más fácil, ya que bastaría con cambiar la función una vez.
- Principio de modularidad
- Y el principio de modularidad, que defiende que en vez de escribir largos trozos de código, es mejor crear módulos o funciones que agrupen ciertos fragmentos de código en funcionalidades específicas, haciendo que el código resultante sea más fácil de leer.
- Documentación
- Documentación Ahora que ya tenemos nuestras propias funciones creadas, tal vez alguien se interese en ellas y podamos compartírselas. Las funciones pueden ser muy complejas, y leer código ajeno no es tarea fácil. Es por ello por lo que es importante documentar las funciones. Es decir, añadir comentarios para indicar como deben ser usadas. def mi_funcion_suma(a, b): """ Descripción de la función. Como debe ser usada, que parámetros acepta y que devuelve """ return a+b Para ello debemos usar la triple comilla """ al principio de la función. Se trata de una especie de comentario que podemos usar para indicar como la función debe ser usada. No se trata de código, es un simple comentario un tanto especial, conocido como docstring. Ahora cualquier persona que tenga nuestra función, podrá llamar a la función help() y obtener la ayuda de como debe ser usada. help(mi_funcion_suma) Otra forma de acceder a la documentación es la siguiente. print(mi_funcion_suma.__doc__) Para saber más: Las descripciones de las funciones suelen ser un poco mas detalladas de lo que hemos mostrado. En la PEP257 se define en detalle como debería ser.
- Anotaciones en funciones
- Existe una funcionalidad relativamente reciente en Python llamada function annotation o anotaciones en funciones. Dicha funcionalidad nos permite añadir metadatos a las funciones, indicando los tipos esperados tanto de entrada como de salida. def multiplica_por_3(numero: int) -> int: return numero*3 multiplica_por_3(6) # 18 Las anotaciones son muy útiles de cara a la documentación del código, pero no imponen ninguna norma sobre los tipos. Esto significa que se puede llamar a la función con un parámetro que no sea int, y no obtendremos ningún error. multiplica_por_3("Cadena") # 'CadenaCadenaCadena'
- Paso por valor y referencia
- Paso por valor y referencia En muchos lenguajes de programación existen los conceptos de paso por valor y por referencia que aplican a la hora de como trata una función a los parámetros que se le pasan como entrada. Su comportamiento es el siguiente: Si usamos un parámetro pasado por valor, se creará una copia local de la variable, lo que implica que cualquier modificación sobre la misma no tendrá efecto sobre la original.Con una variable pasada como referencia, se actuará directamente sobre la variable pasada, por lo que las modificaciones afectarán a la variable original. En Python las cosas son un poco distintas, y el comportamiento estará definido por el tipo de variable con la que estamos tratando. Veamos un ejemplo de paso por valor. x = 10 def funcion(entrada): entrada = 0 funcion(x) print(x) # 10 Iniciamos la x a 10 y se la pasamos a funcion(). Dentro de la función hacemos que la variable valga 0. Dado que Python trata a los int como pasados por valor, dentro de la función se crea una copia local de x, por lo que la variable original no es modificada. No pasa lo mismo si por ejemplo x es una lista como en el siguiente ejemplo. En este caso Python lo trata como si estuviese pasada por referencia, lo que hace que se modifique la variable original. La variable original x ha sido modificada. x = [10, 20, 30] def funcion(entrada): entrada.append(40) funcion(x) print(x) # [10, 20, 30, 40] El ejemplo anterior nos podría llevar a pensar que si en vez de añadir un elemento a x, hacemos x=[], estaríamos destruyendo la lista original. Sin embargo esto no es cierto. x = [10, 20, 30] def funcion(entrada): entrada = [] funcion(x) print(x) # [10, 20, 30] Una forma muy útil de saber lo que pasa por debajo de Python, es haciendo uso de la función id(). Esta función nos devuelve un identificador único para cada objeto. Volviendo al primer ejemplo podemos ver como los objetos a los que “apuntan” x y entrada son distintos. x = 10 print(id(x)) # 4349704528 def funcion(entrada): entrada = 0 print(id(entrada)) # 4349704208 funcion(x) Sin embargo si hacemos lo mismo cuando la variable de entrada es una lista, podemos ver que en este caso el objeto con el que se trabaja dentro de la función es el mismo que tenemos fuera. x = [10, 20, 30] print(id(x)) # 4422423560 def funcion(entrada): entrada.append(40) print(id(entrada)) # 4422423560 funcion(x)
- Args y Kwargs
- Args y Kwargs en Python Si alguna vez has tenido que definir una función con un número variable de argumentos y no has sabido como hacerlo, a continuación te explicamos cómo gracias a los args y kwargs en Python. Vamos a suponer que queremos una función que sume un conjunto de números, pero no sabemos a priori la cantidad de números que se quieren sumar. Si por ejemplo tuviéramos tres, la función sería tan sencilla como la siguiente. def suma(a, b, c): return a+b+c suma(2, 4, 6) #Salida: 12 El problema surge si por ejemplo queremos sumar cuatro números. Como es evidente, la siguiente llamada a la función anterior daría un error ya que estamos usando cuatro argumentos mientras que la función sólo soporta tres. suma(2, 4, 6, 1) #TypeError: suma() takes 3 positional arguments but 4 were given Introducida ya la problemática, veamos como podemos resolver este problema con *args y **kwargs en Python. Uso de *args Gracias a los *args en Python, podemos definir funciones cuyo número de argumentos es variable. Es decir, podemos definir funciones genéricas que no aceptan un número determinado de parámetros, sino que se “adaptan” al número de argumentos con los que son llamados. De hecho, el args viene de arguments en Inglés, o argumentos. Haciendo uso de *args en la declaración de la función podemos hacer que el número de parámetros que acepte sea variable. def suma(*args): s = 0 for arg in args: s += arg return s suma(1, 3, 4, 2) #Salida 10 suma(1, 1) #Salida 2 Antes de nada, el uso del nombre args es totalmente arbitrario, por lo que podrías haberlo llamado como quisieras. Es una mera convención entre los usuarios de Python y resulta frecuente darle ese nombre. Lo que si es un requisito, es usar * junto al nombre. En el ejemplo anterior hemos visto como *args puede ser iterado, ya que en realidad es una tupla. Por lo tanto iterando la tupla podemos acceder a todos los argumentos de entrada, y en nuestro caso sumarlos y devolverlos. Nótese que es un mero ejemplo didáctico. En realidad podríamos hacer algo como lo siguiente, lo que sería mucho más sencillo. def suma(*args): return sum(args) suma(5, 5, 3) #Salida 13 Con esto resolvemos nuestro problema inicial, en el que necesitábamos un número variable de argumentos. Sin embargo, hay otra forma que nos proporciona además un nombre asociado al argumento, con el uso de **kwargs. La explicamos a continuación. Uso de **kwargs Al igual que en *args, en **kwargs el nombre es una mera convención entre los usuarios de Python. Puedes usar cualquier otro nombre siempre y cuando respetes el **. En este caso, en vez de tener una tupla tenemos un diccionario. Puedes verificarlo de la siguiente forma con type(). def suma(**kwargs): print(type(kwargs)) suma(x=3) # Pero veamos un ejemplo más completo. A diferencia de *args, los **kwargs nos permiten dar un nombre a cada argumento de entrada, pudiendo acceder a ellos dentro de la función a través de un diccionario. def suma(**kwargs): s = 0 for key, value in kwargs.items(): print(key, "=", value) s += value return s suma(a=3, b=10, c=3) #Salida #a = 3 #b = 10 #c = 3 #16 Como podemos ver, es posible iterar los argumentos de entrada con items(), y podemos acceder a la clave key (o nombre) y el valor o value de cada argumento. El uso de los **kwargs es muy útil si además de querer acceder al valor de las variables dentro de la función, quieres darles un nombre que de una información extra. Mezclando *args y **kwargs Una vez entendemos el uso de *args y **kwargs, podemos complicar las cosas un poco más. Es posible mezclar argumentos normales con *args y **kwargs dentro de la misma función. Lo único que necesitas saber es que debes definir la función en el siguiente orden: Primero argumentos normales.Después los *args.Y por último los **kwargs. Veamos un ejemplo. def funcion(a, b, *args, **kwargs): print("a =", a) print("b =", b) for arg in args: print("args =", arg) for key, value in kwargs.items(): print(key, "=", value) funcion(10, 20, 1, 2, 3, 4, x="Hola", y="Que", z="Tal") #Salida #a = 10 #b = 20 #args = 1 #args = 2 #args = 3 #args = 4 #x = Hola #y = Que #z = Tal Y por último un truco que no podemos dejar sin mencionar es lo que se conoce como tuple unpacking. Haciendo uso de *, podemos extraer los valores de una lista o tupla, y que sean pasados como argumentos a la función. def funcion(a, b, *args, **kwargs): print("a =", a) print("b =", b) for arg in args: print("args =", arg) for key, value in kwargs.items(): print(key, "=", value) args = [1, 2, 3, 4] kwargs = {'x':"Hola", 'y':"Que", 'z':"Tal"} funcion(10, 20, *args, **kwargs) #Salida #a = 10 #b = 20 #args = 1 #args = 2 #args = 3 #args = 4 #x = Hola #y = Que #z = Tal
- Lambda
- Funciones lambda Las funciones lambda o anónimas son un tipo de funciones en Python que típicamente se definen en una línea y cuyo código a ejecutar suele ser pequeño. Resulta complicado explicar las diferencias, y para que te hagas una idea de ello te dejamos con la siguiente cita sacada de la documentación oficial. “Python lambdas are only a shorthand notation if you’re too lazy to define a function.” Lo que significa algo así como, “las funciones lambda son simplemente una versión acortada, que puedes usar si te da pereza escribir una función” Lo que sería una función que suma dos números como la siguiente. def suma(a, b): return a+b Se podría expresar en forma de una función lambda de la siguiente manera. lambda a, b : a + b La primera diferencia es que una función lambda no tiene un nombre, y por lo tanto salvo que sea asignada a una variable, es totalmente inútil. Para ello debemos. suma = lambda a, b: a + b Una vez tenemos la función, es posible llamarla como si de una función normal se tratase. suma(2, 4) Si es una función que solo queremos usar una vez, tal vez no tenga sentido almacenarla en una variable. Es posible declarar la función y llamarla en la misma línea. (lambda a, b: a + b)(2, 4) ## Ejemplos Una función lambda puede ser la entrada a una función normal. def mi_funcion(lambda_func): return lambda_func(2,4) mi_funcion(lambda a, b: a + b) Y una función normal también puede ser la entrada de una función lambda. Nótese que son ejemplo didácticos y sin demasiada utilidad práctica per se. def mi_otra_funcion(a, b): return a + b (lambda a, b: mi_otra_funcion(a, b))(2, 4) A pesar de que las funciones lambda tienen muchas limitaciones frente a las funciones normales, comparten gran cantidad de funcionalidades. Es posible tener argumentos con valor asignado por defecto. (lambda a, b, c=3: a + b + c)(1, 2) # 6 También se pueden pasar los parámetros indicando su nombre. (lambda a, b, c: a + b + c)(a=1, b=2, c=3) # 6 Al igual que en las funciones se puede tener un número variable de argumentos haciendo uso de *, lo conocido como tuple unpacking. (lambda *args: sum(args))(1, 2, 3) # 6 Y si tenemos los parámetros de entrada almacenados en forma de key y value como si fuera un diccionario, también es posible llamar a la función. (lambda **kwargs: sum(kwargs.values()))(a=1, b=2, c=3) # 6 Por último, es posible devolver más de un valor. x = lambda a, b: (b, a) print(x(3, 9)) # Salida (9,3)
- Recursividad
- Otro ejemplo muy usado para ilustrar las posibilidades de la recursividad, es calcular la serie de fibonacci. Dicha serie calcula el elemento n sumando los dos anteriores n-1 + n-2. Se asume que los dos primeros elementos son 0 y 1. def fibonacci_recursivo(n): if n == 0: return 0 elif n == 1: return 1 else: return fibonacci_recursivo(n-1) + fibonacci_recursivo(n-2) Podemos ver que siempre que la n sea distinta de cero o uno, se llamará recursivamente a la función, buscando los dos elementos anteriores. Cuando la n valga cero o uno se dejará de llamar a la función y se devolverá un valor concreto. Podemos calcular el elemento 7, que será 0,1,1,2,3,5,8,13, es decir, 13. fibonacci_recursivo(7) # 13
- Decoradores
- Decoradores Los decoradores son funciones que modifican el comportamiento de otras funciones, ayudan a acortar nuestro código y hacen que sea más Pythonic. Si alguna vez has visto @, estás ante un decorador o decorator, bien sea uno que Python ofrece por defecto o uno que puede haber sido creado ex profeso. Si aún no controlas las funciones te recomendamos que empieces con este post. Veamos un ejemplo muy sencillo. Tenemos una función suma() que vamos a decorar usando mi_decorador(). Para ello, antes de la declaración de la función suma, hacemos uso de @mi_decorador. def mi_decorador(funcion): def nueva_funcion(a, b): print("Se va a llamar") c = funcion(a, b) print("Se ha llamado") return c return nueva_funcion @mi_decorador def suma(a, b): print("Entra en funcion suma") return a + b suma(5,8) # Se va a llamar # Entra en funcion suma # Se ha llamado Lo que realiza mi_decorador() es definir una nueva función que encapsula o envuelve la función que se pasa como entrada. Concretamente lo que hace es hace uso de dos print(), uno antes y otro después de la llamada la función. Por lo tanto, cualquier función que use @mi_decorador tendrá dos print, uno y al principio y otro al final, dando igual lo que realmente haga la función. Veamos otro ejemplo usando el decorador sobre otra función. @mi_decorador def resta(a ,b): print("Entra en funcion resta") return a - b resta(5, 3) # Se va a llamar # Entra en funcion resta # Se ha llamado Una vez entendido esto, vamos a entrar un poco más en detalle viendo como Python trata a las funciones, como se puede definir un decorador sin @, como se pueden pasar argumentos a los decoradores, y para finalizar, un ejemplo práctico. Definiendo decoradores Antes de nada hay que entender que todo en Python es un objeto, incluso una función. De hecho se puede asignar una función a una variable. Nótese la diferencia entre: di_hola() llama a la función.di_hola hace referencia a la función, no la llama. def di_hola(): print("Hola") f1 = di_hola() # Llama a la función f2 = di_hola # Asigna a f2 la función print(f1) # None, di_hola no devuelve nada print(f2) # #f1() # Error! No es válido f2() # Llama a f2, que es di_hola() del f2 # Borra el objeto que es la función #f2() # Error! Ya no existe di_hola() # Ok. Sigue existiendo Entendido esto, demos un paso más. En Python se pueden definir funciones dentro de otras funciones. La función operaciones define suma() y resta(), y dependiendo del parámetro de entrada op, se devolverá una u otra. def operaciones(op): def suma(a, b): return a + b def resta(a, b): return a - b if op == "suma": return suma elif op == "resta": return resta funcion_suma = operaciones("suma") print(funcion_suma(5, 7)) # 12 funcion_suma = operaciones("resta") print(funcion_suma(5, 7)) # -2 Si llamamos a la función devuelta con dos operandos, se realizará una operación distinta en función de si se uso suma o resta. Ahora ya podemos dar la última vuelta de tuerca y escribir nuestro propio decorador sin hacer uso de @. Por un lado tenemos el decorador, que recibe como entrada una función y devuelve otra función decorada. Por el otro la función suma() que queremos decorar. def decorador(func): def envoltorio_func(a, b): print("Decorador antes de llamar a la función") c = func(a, b) print("Decorador después de llamar a la función") return c return envoltorio_func def suma(a, b): print("Dentro de suma") return a + b # Nueva funcion decorada funcion_decorada = decorador(suma) funcion_decorada(5, 8) Entonces, haciendo uso de decorador y pasando como entrada suma, recibimos una nueva función decorada con una funcionalidad nueva, lista para ser usada. Sería el equivalente al uso de @. Decoradores con parámetros Tal vez quieras que tu decorador tenga algún parámetro de entrada para modificar su comportamiento. Se puede hacer envolviendo una vez más la función como se muestra a continuación. def mi_decorador(arg): def decorador_real(funcion): def nueva_funcion(a, b): print(arg) c = funcion(a, b) print(arg) return c return nueva_funcion return decorador_real @mi_decorador("Imprimer esto antes y después") def suma(a, b): print("Entra en funcion suma") return a + b suma(5,8) # Imprimer esto antes y después # Entra en funcion suma # Imprimer esto antes y después Es importante recalcar que los ejemplos mostrados hasta ahora son didácticos, y no tienen mucha utilidad práctica. Veamos un ejemplo más práctico. Ejemplo: logger Una de las utilidades más usadas de los decoradores son los logger. Su uso nos permite escribir en un fichero los resultados de ciertas operaciones, que funciones han sido llamadas, o cualquier información que en un futuro resulte útil para ver que ha pasado. En el siguiente ejemplo tenemos un uso más completo del decorador log() que escribe en un fichero los resultados de las funciones que han sido llamadas. def log(fichero_log): def decorador_log(func): def decorador_funcion(*args, **kwargs): with open(fichero_log, 'a') as opened_file: output = func(*args, **kwargs) opened_file.write(f"{output}\n") return decorador_funcion return decorador_log @log('ficherosalida.log') def suma(a, b): return a + b @log('ficherosalida.log') def resta(a, b): return a - b @log('ficherosalida.log') def multiplicadivide(a, b, c): return a*b/c suma(10, 30) resta(7, 23) multiplicadivide(5, 10, 2) Nótese que el decorador puede ser usado sobre funciones que tienen diferente número de parámetros de entrada, y su funcionalidad será siempre la misma. Escribir en el fichero pasado como parámetro los resultados de las operaciones. Ejemplo: Uso autorizado Otro caso de uso muy importante y ampliamente usado en Flask, que es un framework de desarrollo web, es el uso de decoradores para asegurarse de que una función es llamada cuando el usuario se ha autenticado. Realizando alguna simplificación, podríamos tener un decorador que requiriera que una variable autenticado fuera True. La función se ejecutará solo si dicha variable global es verdadera, y se dará un error de lo contrario. autenticado = True def requiere_autenticación(f): def funcion_decorada(*args, **kwargs): if not autenticado: print("Error. El usuario no se ha autenticado") else: return f(*args, **kwargs) return funcion_decorada @requiere_autenticación def di_hola(): print("Hola") di_hola() Prueba a cambiar la variable de True a False. Y esto es todo. En otros posts veremos el uso de functools y porque nos puede ser muy útil. Anterior
- Generadores
- i alguna vez te has encontrado con una función en Python que no sólo tiene una sentencia return, sino que además devuelve un valor haciendo uso de yield, ya has visto lo que es un generador o generator. A continuación te explicamos cómo se crean, para qué sirven y sus ventajas. Es muy importante también no confundir los generadores con las corrutinas, que también usan yield pero de otra manera, sin embargo estas las dejamos para otro post. Empecemos por lo básico. Seguramente ya sepas lo que es una función y cómo puede devolver un valor con return. def funcion(): return 5 Como hemos explicado, los generadores usan yield en vez de return. Vamos a ver que pasaría si cambiamos el return por el yield. def generador(): yield 5 Y ahora vamos a intentar llamar a las dos “funciones”. print(funcion()) print(generador()) # Salida: 5 # Salida: Podemos ver ya la primera diferencia al usar el yield. En el primer caso, se devuelve un 5, pero en el segundo lo que se devuelve es en realidad un objeto de la clase generator. En realidad el número 5 también puede ser accedido en el caso del generador, pero esto lo veremos más adelante. Entonces, si una función contiene al menos una sentencia yield, se convertirá en una función generadora. Una función generadora se diferencia de una función normal en que tras ejecutar el yield, la función devuelve el control a quién la llamó, pero la función es pausada y el estado (valor de las variables) es guardado. Esto permite que su ejecución pueda ser reanudada más adelante. Iterando los Generadores A continuación vamos a ver cómo acceder a los valores del generador. Para entenderlo mejor, te recomendamos que leas antes más acerca de iterables e iteradores. Otra de las características que hacen a los generators diferentes, es que pueden ser iterados, ya que codifican los métodos __iter__() y __next__(), por lo que podemos usar next() sobre ellos. Dado que son iterables, lanzan también un StopIteration cuando se ha llegado al final. Volviendo al ejemplo anterior, vamos a ver como podemos usar el next(). a = generador() print(next(a)) # Salida: 5 Como te prometimos antes, el 5 también se podía acceder ¿has visto?. Pero vamos a ver que pasa ahora si intentamos llamar otra vez a next(). Si recuerdas, sólo tenemos una llamada a yield. a = generador() print(next(a)) print(next(a)) # Salida: 5 # Salida: Error! StopIteration: Como era de esperar, tenemos una excepción del tipo StopIteration, ya que el generador no devuelve más valores. Esto se debe a que cada vez que usamos next() sobre el generador, se llama y se continúa su ejecución después del último yield. Y en este caso cómo no hay más código, no se generan más valores. Creando Generadores Vamos a ver otro ejemplo más completo donde tengamos un generador que genere varios valores. En la siguiente función podemos ver como tenemos una variable n que incrementada en 1, y después retorna con yield. Lo que pasará aquí, es que el generador generará un total de tres valores. def generador(): n = 1 yield n n += 1 yield n n += 1 yield n Y haciendo uso de next() al igual que hacíamos antes, podemos ver los valores que han sido generados. Lo que pasa por debajo, sería lo siguiente: Se entra en la función generadora, n=1 y se devuelve ese valor. Como ya hemos visto, el estado de la función se guarda (el valor de n es guardado para la siguiente llamada)La segunda vez que usamos next() se entra otra vez en la función, pero se continúa su ejecución donde se dejó anteriormente. Se suma 1 a la n y se devuelve el nuevo valor.La tercera llamada, realiza lo mismo.Una cuarta llamada daría un error, ya que no hay más código que ejecutar. g = generador() print(next(g)) print(next(g)) print(next(g)) # Salida: 1, 2, 3 Otra forma más cómoda de realizar lo mismo, sería usando un simple bucle for, ya que el generador es iterable. for i in generador(): print(i) # Salida: 1, 2, 3 Forma alternativa Los generadores también pueden ser creados de una forma mucho más sencilla y en una sola línea de código. Su sintaxis es similar a las list comprehension, pero cambiando el corchete [] por paréntesis (). El ejemplo con list comprehensions sería el siguiente. Simplemente se generan los valores de una lista elevados al cuadrado. lista = [2, 4, 6, 8, 10] al_cuadrado = [x**2 for x in lista] print(al_cuadrado) # [4, 16, 36, 64, 100] Y su equivalente con generadores sería lo siguiente. al_cuadrado_generador = (x**2 for x in lista) print(al_cuadrado_generador) # Salida: at 0x103e803b8> Y como hemos visto los valores pueden ser generados de la siguiente forma. for i in al_cuadrado_generador: print(i) # Salda: 4, 16, 36, 64, 100 La diferencia entre el ejemplo usando list compregensions y generators es que en el caso de los generadores, los valores no están almacenados en memoria, sino que se van generando al vuelo. Esta es una de las principales ventajas de los generadores, ya que los elementos sólo son generados cuando se piden, lo que hace que sean mucho más eficientes en lo relativo a la memoria. Ventajas y ejemplos Llegados a este punto tal vez te preguntes para qué sirven los generadores. Lo cierto es que aunque no existieran, podría realizarse lo mismo creando una clase que implemente los métodos __iter__() y __next__(). Veamos un ejemplo de una clase que genera los primeros 10 números (0,9) al cuadrado. class AlCuadrado: def __init__(self): self.i = 0 def __iter__(self): return self def __next__(self): if self.i > 9: raise StopIteration result = self.i ** 2 self.i += 1 return result Y de la misma forma que usábamos los generadores, podemos usar nuestra clase AlCuadrado. Creamos un objeto de ella, y la iteramos. En cada iteración generará un nuevo valor nuevo hasta que se llegue al final. eleva_al_cuadrado = AlCuadrado() for i in eleva_al_cuadrado: print(i) #0,1,4,9,16,25,36,49,64,81 Sin embargo esta forma es un tanto larga y tal vez confusa. Como hemos visto antes, podemos llegar a hacer lo mismo en una sola línea de código. ¿Para que complicarse la vida? Por otro lado, ya hemos mencionado que el uso de los generadores hace que no todos los valores estén almacenados en memoria sino que sean generados al vuelo. Vamos a ver un ejemplo donde se puede ver mejor. Supongamos que queremos sumar los primeros 100 números naturales (referencia). Una opción podría ser crear una lista de todos ellos y después sumarla. En este caso, todos los valores son almacenados en memoria, algo que podría ser un problema si por ejemplo intentamos sumar los primeros 1e10 números. def primerosn(n): nums = [] for i in range(n): nums.append(i) return nums print(sum(firstn(100))) # Salida: 4950 Sin embargo, podemos realizar lo mismo con un generador. En este caso los valores serán generados uno por uno según se vayan necesitando. def primerosn(n): num = 0 for i in range(n): yield num num += 1 print(sum(primerosn(100))) # Salida 4950 Nótese que es un ejemplo con fines didácticos, por lo que si quieres hacer esto, la mejor manera sería usando un simple range() asumiendo que usas Python 3. print(sum(range(100))) # Salida: 4950
- Caching
- El caché es un término muy usado en informática, y hace referencia al almacenamiento de resultados previos para su posterior reutilización, lo que permite reducir el tiempo de respuesta. Por ejemplo, si llamamos a una función con un determinado parámetro y acto seguido realizamos la misma llamada, sería interesante reutilizar el primer resultado para no tener que calcularlo otra vez. Existen por lo tanto dos posibilidades: Si ejecutamos la función y el resultado no ha sido calculado con anterioridad, se calcula y se almacena por si fuera útil en el futuro. Esto se conoce como cache miss.Si ejecutamos la función y el caché tiene almacenado el resultado para esa operación, en vez de calcular otra vez la salida la podemos reutilizar, lo que se conoce como cache hit. Dado que estamos reutilizando un valor ya calculado, generalmente el tiempo de respuesta será menor. Por suerte, Python nos permite añadir caching a nuestras funciones, pero antes de implementarlo es conveniente hacer un análisis sobre nuestro programa y determinar si merece la pena. Algunas cosas a tener en cuenta: El caching es especialmente útil cuando trabajamos con funciones muy intensivas en cálculo, lo que hace que reutilizar el valor del caché reduzca notablemente el tiempo de respuesta.Es necesario conocer (a nivel estadístico) la distribución de los argumentos con los que se llama la función. Si la función bajo estudio se llama con valores muy dispares y apenas repetidos, el caching poco ayudará, ya que apenas tendremos un cache hit.El uso de un caché puede mejorar el tiempo de respuesta, pero frecuentemente se paga en un incremento del uso de memoria. También es necesario decidir el número de valores a almacenar. A continuación veremos como implementar caching en Python, pudiendo hacerlo con diccionarios o utilizando la librería functools. Para ejemplificarlo, veremos como implementar un caché en nuestro código de números primos visto anteriormente, empleando ambas formas. def es_primo(num): for n in range(2, num): if num % n == 0: return False return True Caching con Diccionarios La primera forma de realizarlo es usando un diccionario como caché. Nótese que este es un ejemplo didáctico, y que obvia algunos factores. Como puedes ver tenemos claramente diferenciado el cache hit y el cache miss. Si el valor no está en el caché se calcula y se devuelve. def es_primo_concache(num, _cache={}): if num not in _cache: _cache[num] = True for n in range(2, num): if num % n == 0: _cache[num] = False break return _cache[num] Dado que es_primo es bastante intensivo en cálculo, cuando usamos números grandes el ahorro puede ser muy significativo. En el siguiente código podemos ver como la primera vez que ejecutamos la función, se tardan 3.5 segundos, ya que el resultado tiene que ser calculado. Sin embargo la segunda vez que la llamamos con la misma entrada, tenemos un cache hit, por lo que el valor ya no es calculado sino recuperado del caché, tardando microsegundos. import time tic = time.time() es_primo_concache(25565479) print(time.time() - tic) tic = time.time() es_primo_concache(25565479) print(time.time() - tic) # 3.5551438331604004 # 4.0531158447265625e-06 Caching con functools y lru_cache La segunda forma de realizarlo, y un poco más sofisticada es usando lru_cache, un decorador que viene con la librería estándar functools. La mayor ventaja es que no necesitamos modificar la función. Nótese que maxsize nos permite indicar el número máximo de valores que queremos almacenar en el caché. from functools import lru_cache @lru_cache(maxsize=32) def es_primo_concache(num): for n in range(2, num): if num % n == 0: return False return True Por lo tanto si ahora llamamos a nuestra función con los mismos valores, podemos ver como la primera vez tarda 3.9 segundos, pero la segunda apenas tarda unos microsegundos. import time tic = time.time() es_primo_concache(25565479) print(time.time() - tic) tic = time.time() es_primo_concache(25565479) print(time.time() - tic) # 3.9316678047180176 # 3.0994415283203125e-06 En el caso de que queramos limpiar el caché de nuestra función, podemos realizar lo siguiente. es_primo_concache.cache_clear()
Casting
- Hacer un cast o casting significa convertir un tipo de dato a otro. Anteriormente hemos visto tipos como los int, string o float. Pues bien, es posible convertir de un tipo a otro.
- Implícita
- Esta conversión de tipos es realizada automáticamente por Python, prácticamente sin que nos demos cuenta. Aún así, es importante saber lo que pasa por debajo para evitar problemas futuros. El ejemplo más sencillo donde podemos ver este comportamiento es el siguiente: a = 1 # b = 2.3 # a = a + b print(a) # 3.3 print(type(a)) # a es un intb es un float
- Explícita
- Conversión explicita Por otro lado, podemos hacer conversiones entre tipos o cast de manera explícita haciendo uso de diferentes funciones que nos proporciona Python.Las más usadas son las siguientes:float(), str(), int(), list(), set()Y algunas otras como hex(), oct() y bin()
- De float a int
- Para convertir de float a int debemos usar float(). Pero mucho cuidado, ya que el tipo entero no puede almacena decimales, por lo que perderemos lo que haya después de la coma.
- De float a string
- Podemos convertir un float a string con str(). Podemos ver en el siguiente código como cambia el tipo de a después de hacer el cast. a = 3.5 print(type(a)) # a = str(a) print(type(a)) #
- De string a float
- Convertir string a float Podemos convertir un string a float usando float(). Es importante notar que se usa el . como separador. a = "35.5" print(float(a)) # Salida: 35.5 El siguiente código daría un error, dado que , no se reconoce por defecto como separador decimal. a = "35,5" print(float(a)) # Salida: ValueError: could not convert string to float: '35,5' Y por último, resulta obvio pensar que el siguiente código dará un error también. a = "Python" print(float(a)) # Salida: ValueError: could not convert string to float: 'Python'
- De string a int
- Convertir string a int Al igual que la conversión a float del caso anterior, podemos convertir de string a int usando int(). a = "3" print(type(a)) # a = int(a) print(type(a)) # Cuidado ya que no es posible convertir a int cualquier valor. a = "Python" a = int(a) # ValueError: invalid literal for int() with base 10: 'Python'
- De int a string
- Convertir int a string La conversión de int a string se puede realizar con str(). A diferencia de otras conversiones, esta puede hacerse siempre, ya que cualquier valor entero que se nos ocurra poner en a, podrá ser convertido a string. a = 10 print(type(a)) # a = str(a) print(type(a)) #
- De set a lista
- Convertir a list Es también posible hacer un cast a lista, desde por ejemplo un set. Para ello podemos usar list(). a = {1, 2, 3} b = list(a) print(type(a)) # print(type(b)) #
- De lista a set
- Convertir a set Y de manera completamente análoga, podemos convertir de lista a set haciendo uso de set(). a = ["Python", "Mola"] b = set(a) print(type(a)) # print(type(b)) #
Operadores
- De asignación
- Operadores de asignación Anteriormente hemos visto los operadores aritméticos, que usaban dos números para calcular una operación aritmética (como suma o resta) y devolver su resultado. En este caso, los operadores de asignación o assignment operators nos permiten realizar una operación y almacenar su resultado en la variable inicial. Podemos ver como realmente el único operador nuevo es el =. El resto son abreviaciones de otros operadores que habíamos visto con anterioridad. Ponemos un ejemplo con x=7
- Aritmeticos
- Los operadores aritméticos o arithmetic operators son los más comunes que nos podemos encontrar, y nos permiten realizar operaciones aritméticas sencillas, como pueden ser la suma, resta o exponente. A continuación, condensamos en la siguiente tabla todos ellos con un ejemplo, donde x=10 y y=3.
- Orden de aplicación
- El orden de prioridad sería el siguiente para los operadores aritméticos, siendo el primero el de mayor prioridad: () Paréntesis** Exponente-x Negación* / // Multiplicación, División, Cociente, Módulo+ - Suma, Resta
- Relacionales
- Los operadores relacionales, o también llamados comparison operators nos permiten saber la relación existente entre dos variables. Se usan para saber si por ejemplo un número es mayor o menor que otro. Dado que estos operadores indican si se cumple o no una operación, el valor que devuelven es True o False. Veamos un ejemplo con x=2 e y=3
- x=2; y=3 print("Operadores Relacionales") print("x==y =", x==y) # False print("x!=y =", x!=y) # True print("x>y =", x>y) # False print("x<y =", x<y) # True print("x>=y =", x>=y) # False print("x<=y =", x<=y) # True
- Lógicos
- Operadores lógicos Los operadores lógicos o logical operators nos permiten trabajar con valores de tipo booleano. Un valor booleano o bool es un tipo que solo puede tomar valores True o False. Por lo tanto, estos operadores nos permiten realizar diferentes operaciones con estos tipos, y su resultado será otro booleano. Por ejemplo, True and True usa el operador and, y su resultado será True. A continuación lo explicaremos mas en detalle. OperadorNombreEjemploandDevuelve True si ambos elementos son TrueTrue and True = TrueorDevuelve True si al menos un elemento es TrueTrue or False = TruenotDevuelve el contrario, True si es Falso y viceversanot True = False
- De asignación
Programación Orientada a Objetos
- Antes de nada, empecemos con una introducción básica a la Programación Orientada a Objetos POO o OOP en inglés. Se trata de un paradigma de programación introducido en los años 1970s, pero que no se hizo popular hasta años más tarde. Este modo o paradigma de programación nos permite organizar el código de una manera que se asemeja bastante a como pensamos en la vida real, utilizando las famosas clases. Estas nos permiten agrupar un conjunto de variables y funciones que veremos a continuación. Cosas de lo más cotidianas como un perro o un coche pueden ser representadas con clases. Estas clases tienen diferentes características, que en el caso del perro podrían ser la edad, el nombre o la raza. Llamaremos a estas características, atributos. Por otro lado, las clases tienen un conjunto de funcionalidades o cosas que pueden hacer. En el caso del perro podría ser andar o ladrar. Llamaremos a estas funcionalidades métodos. Por último, pueden existir diferentes tipos de perro. Podemos tener uno que se llama Toby o el del vecino que se llama Laika. Llamaremos a estos diferentes tipos de perro objetos. Es decir, el concepto abstracto de perro es la clase, pero Toby o cualquier otro perro particular será el objeto. La programación orientada a objetos está basada en 6 principios o pilares básicos: HerenciaCohesiónAbstracciónPolimorfismoAcoplamientoEncapsulamientoMotivación Una vez explicada la programación orientada a objetos puede parecer bastante lógica, pero no es algo que haya existido siempre, y de hecho hay muchos lenguajes de programación que no la soportan. En parte surgió debido a la creciente complejidad a la que los programadores se iban enfrentando conforme pasaban los años. En el mundo de la programación hay gran cantidad de aplicaciones que realizan tareas muy similares y es importante identificar los patrones que nos permiten no reinventar la rueda. La programación orientada a objetos intentaba resolver esto. Uno de los primeros mecanismos que se crearon fueron las funciones, que permiten agrupar bloques de código que hacen una tarea específica bajo un nombre. Algo muy útil ya que permite también reusar esos módulos o funciones sin tener que copiar todo el código, tan solo la llamada. Las funciones resultaron muy útiles, pero no eran capaces de satisfacer todas las necesidades de los programadores. Uno de los problemas de las funciones es que sólo realizan unas operaciones con unos datos de entrada para entregar una salida, pero no les importa demasiado conservar esos datos o mantener algún tipo de estado. Salvo que se devuelva un valor en la llamada a la función o se usen variables globales, todo lo que pasa dentro de la función queda olvidado, y esto en muchos casos es un problema. Imaginemos que tenemos un juego con naves espaciales moviéndose por la pantalla. Cada nave tendrá una posición (x,y) y otros parámetros como el tipo de nave, su color o tamaño. Sin hacer uso de clases y POO, tendremos que tener una variable para cada dato que queremos almacenar: coordenadas, color, tamaño, tipo. El problema viene si tenemos 10 naves, ya que nos podríamos juntar con un número muy elevado de variables. Todo un desastre. En el mundo de la programación existen tareas muy similares al ejemplo con las naves, y en respuesta a ello surgió la programación orientada a objetos. Una herramienta perfecta que permite resolver cierto tipo de problemas de una forma mucho más sencilla, con menos código y más organizado. Agrupa bajo una clase un conjunto de variables y funciones, que pueden ser reutilizadas con características particulares creando objetos.
- Clases
- Definiendo clases Vista ya la parte teórica, vamos a ver como podemos hacer uso de la programación orientada a objetos en Python. Lo primero es crear una clase, para ello usaremos el ejemplo de perro. # Creando una clase vacía class Perro: pass Se trata de una clase vacía y sin mucha utilidad práctica, pero es la mínima clase que podemos crear. Nótese el uso del pass que no hace realmente nada, pero daría un error si después de los : no tenemos contenido. Ahora que tenemos la clase, podemos crear un objeto de la misma. Podemos hacerlo como si de una variable normal se tratase. Nombre de la variable igual a la clase con (). Dentro de los paréntesis irían los parámetros de entrada si los hubiera. # Creamos un objeto de la clase perro mi_perro = Perro()
- Atributos
- A continuación vamos a añadir algunos atributos a nuestra clase. Antes de nada es importante distinguir que existen dos tipos de atributos: Atributos de instancia: Pertenecen a la instancia de la clase o al objeto. Son atributos particulares de cada instancia, en nuestro caso de cada perro.Atributos de clase: Se trata de atributos que pertenecen a la clase, por lo tanto serán comunes para todos los objetos. Empecemos creando un par de atributos de instancia para nuestro perro, el nombre y la raza. Para ello creamos un método __init__ que será llamado automáticamente cuando creemos un objeto. Se trata del constructor. class Perro: # El método __init__ es llamado al crear el objeto def __init__(self, nombre, raza): print(f"Creando perro {nombre}, {raza}") # Atributos de instancia self.nombre = nombre self.raza = raza Ahora que hemos definido el método init con dos parámetros de entrada, podemos crear el objeto pasando el valor de los atributos. Usando type() podemos ver como efectivamente el objeto es de la clase Perro. mi_perro = Perro("Toby", "Bulldog") print(type(mi_perro)) # Creando perro Toby, Bulldog # Seguramente te hayas fijado en el self que se pasa como parámetro de entrada del método. Es una variable que representa la instancia de la clase, y deberá estar siempre ahí. El uso de __init__ y el doble __ no es una coincidencia. Cuando veas un método con esa forma, significa que está reservado para un uso especial del lenguaje. En este caso sería lo que se conoce como constructor. Hay gente que llama a estos métodos mágicos. Por último, podemos acceder a los atributos usando el objeto y .. Por lo tanto. print(mi_perro.nombre) # Toby print(mi_perro.raza) # Bulldog Hasta ahora hemos definido atributos de instancia, ya que son atributos que pertenecen a cada perro concreto. Ahora vamos a definir un atributo de clase, que será común para todos los perros. Por ejemplo, la especie de los perros es algo común para todos los objetos Perro. class Perro: # Atributo de clase especie = 'mamífero' # El método __init__ es llamado al crear el objeto def __init__(self, nombre, raza): print(f"Creando perro {nombre}, {raza}") # Atributos de instancia self.nombre = nombre self.raza = raza Dado que es un atributo de clase, no es necesario crear un objeto para acceder al atributos. Podemos hacer lo siguiente. print(Perro.especie) # mamífero Se puede acceder también al atributo de clase desde el objeto. mi_perro = Perro("Toby", "Bulldog") mi_perro.especie # 'mamífero' De esta manera, todos los objetos que se creen de la clase perro compartirán ese atributo de clase, ya que pertenecen a la misma.
- Métodos
- Definiendo métodos En realidad cuando usamos __init__ anteriormente ya estábamos definiendo un método, solo que uno especial. A continuación vamos a ver como definir métodos que le den alguna funcionalidad interesante a nuestra clase, siguiendo con el ejemplo de perro. Vamos a codificar dos métodos, ladrar y caminar. El primero no recibirá ningún parámetro y el segundo recibirá el número de pasos que queremos andar. Como hemos indicado anteriormente self hace referencia a la instancia de la clase. Se puede definir un método con def y el nombre, y entre () los parámetros de entrada que recibe, donde siempre tendrá que estar self el primero. class Perro: # Atributo de clase especie = 'mamífero' # El método __init__ es llamado al crear el objeto def __init__(self, nombre, raza): print(f"Creando perro {nombre}, {raza}") # Atributos de instancia self.nombre = nombre self.raza = raza def ladra(self): print("Guau") def camina(self, pasos): print(f"Caminando {pasos} pasos") Por lo tanto si creamos un objeto mi_perro, podremos hacer uso de sus métodos llamándolos con . y el nombre del método. Como si de una función se tratase, pueden recibir y devolver argumentos. mi_perro = Perro("Toby", "Bulldog") mi_perro.ladra() mi_perro.camina(10) # Creando perro Toby, Bulldog # Guau # Caminando 10 pasos
- Tipos
- Instancia
- Métodos de instancia Los métodos de instancia son los métodos normales, de toda la vida, que hemos visto anteriormente. Reciben como parámetro de entrada self que hace referencia a la instancia que llama al método. También pueden recibir otros argumentos como entrada. Para saber más: El uso de "self" es totalmente arbitrario. Se trata de una convención acordada por los usuarios de Python, usada para referirse a la instancia que llama al método, pero podría ser cualquier otro nombre. Lo mismo ocurre con "cls", que veremos a continuación. class Clase: def metodo(self, arg1, arg2): return 'Método normal', self Y como ya sabemos, una vez creado un objeto pueden ser llamados. mi_clase = Clase() mi_clase.metodo("a", "b") # ('Método normal', <__main__.Clase at 0x10b9daa90>) En vista a esto, los métodos de instancia: Pueden acceder y modificar los atributos del objeto.Pueden acceder a otros métodos.Dado que desde el objeto self se puede acceder a la clase con ` self.class`, también pueden modificar el estado de la clase
- De Clase
- Métodos de clase (classmethod) A diferencia de los métodos de instancia, los métodos de clase reciben como argumento cls, que hace referencia a la clase. Por lo tanto, pueden acceder a la clase pero no a la instancia. class Clase: @classmethod def metododeclase(cls): return 'Método de clase', cls Se pueden llamar sobre la clase. Clase.metododeclase() # ('Método de clase', __main__.Clase) Pero también se pueden llamar sobre el objeto. mi_clase.metododeclase() # ('Método de clase', __main__.Clase) Por lo tanto, los métodos de clase: No pueden acceder a los atributos de la instancia.Pero si pueden modificar los atributos de la clase.
- Estáticos
- Por último, los métodos estáticos se pueden definir con el decorador @staticmethod y no aceptan como parámetro ni la instancia ni la clase. Es por ello por lo que no pueden modificar el estado ni de la clase ni de la instancia. Pero por supuesto pueden aceptar parámetros de entrada. class Clase: @staticmethod def metodoestatico(): return "Método estático" mi_clase = Clase() Clase.metodoestatico() mi_clase.metodoestatico() # 'Método estático' # 'Método estático' Por lo tanto el uso de los métodos estáticos pueden resultar útil para indicar que un método no modificará el estado de la instancia ni de la clase. Es cierto que se podría hacer lo mismo con un método de instancia por ejemplo, pero a veces resulta importante indicar de alguna manera estas peculiaridades, evitando así futuros problemas y malentendidos. En otras palabras, los métodos estáticos se podrían ver como funciones normales, con la salvedad de que van ligadas a una clase concreta.
- Instancia
- Principios básicos
- Herencia
- Para entender la herencia, es fundamental entender la programación orientada a objetos, por lo que te recomendamos empezar por ahí antes. La herencia es un proceso mediante el cual se puede crear una clase hija que hereda de una clase padre, compartiendo sus métodos y atributos. Además de ello, una clase hija puede sobreescribir los métodos o atributos, o incluso definir unos nuevos. Se puede crear una clase hija con tan solo pasar como parámetro la clase de la que queremos heredar. En el siguiente ejemplo vemos como se puede usar la herencia en Python, con la clase Perro que hereda de Animal. Así de fácil. # Definimos una clase padre class Animal: pass # Creamos una clase hija que hereda de la padre class Perro(Animal): pass De hecho podemos ver como efectivamente la clase Perro es la hija de Animal usando __bases__ print(Perro.__bases__) # (,) De manera similar podemos ver que clases descienden de una en concreto con __subclasses__. print(Animal.__subclasses__()) # [] ¿Y para que queremos la herencia? Dado que una clase hija hereda los atributos y métodos de la padre, nos puede ser muy útil cuando tengamos clases que se parecen entre sí pero tienen ciertas particularidades. En este caso en vez de definir un montón de clases para cada animal, podemos tomar los elementos comunes y crear una clase Animal de la que hereden el resto, respetando por tanto la filosofía DRY. Realizar estas abstracciones y buscar el denominador común para definir una clase de la que hereden las demás, es una tarea de lo más compleja en el mundo de la programación. Para saber más: El principio DRY (Don't Repeat Yourself) es muy aplicado en el mundo de la programación y consiste en no repetir código de manera innecesaria. Cuanto más código duplicado exista, más difícil será de modificar y más fácil será crear inconsistencias. Las clases y la herencia a no repetir código. Extendiendo y modificando métodos Continuemos con nuestro ejemplo de perros y animales. Vamos a definir una clase padre Animal que tendrá todos los atributos y métodos genéricos que los animales pueden tener. Esta tarea de buscar el denominador común es muy importante en programación. Veamos los atributos: Tenemos la especie ya que todos los animales pertenecen a una.Y la edad, ya que todo ser vivo nace, crece, se reproduce y muere. Y los métodos o funcionalidades: Tendremos el método hablar, que cada animal implementará de una forma. Los perros ladran, las abejas zumban y los caballos relinchan.Un método moverse. Unos animales lo harán caminando, otros volando.Y por último un método descríbeme que será común. Definimos la clase padre, con una serie de atributos comunes para todos los animales como hemos indicado. class Animal: def __init__(self, especie, edad): self.especie = especie self.edad = edad # Método genérico pero con implementación particular def hablar(self): # Método vacío pass # Método genérico pero con implementación particular def moverse(self): # Método vacío pass # Método genérico con la misma implementación def describeme(self): print("Soy un Animal del tipo", type(self).__name__) Tenemos ya por lo tanto una clase genérica Animal, que generaliza las características y funcionalidades que todo animal puede tener. Ahora creamos una clase Perro que hereda del Animal. Como primer ejemplo vamos a crear una clase vacía, para ver como los métodos y atributos son heredados por defecto. # Perro hereda de Animal class Perro(Animal): pass mi_perro = Perro('mamífero', 10) mi_perro.describeme() # Soy un Animal del tipo Perro Con tan solo un par de líneas de código, hemos creado una clase nueva que tiene todo el contenido que la clase padre tiene, pero aquí viene lo que es de verdad interesante. Vamos a crear varios animales concretos y sobreescrbir algunos de los métodos que habían sido definidos en la clase Animal, como el hablar o el moverse, ya que cada animal se comporta de una manera distinta. Podemos incluso crear nuevos métodos que se añadirán a los ya heredados, como en el caso de la Abeja con picar(). class Perro(Animal): def hablar(self): print("Guau!") def moverse(self): print("Caminando con 4 patas") class Vaca(Animal): def hablar(self): print("Muuu!") def moverse(self): print("Caminando con 4 patas") class Abeja(Animal): def hablar(self): print("Bzzzz!") def moverse(self): print("Volando") # Nuevo método def picar(self): print("Picar!") Por lo tanto ya podemos crear nuestros objetos de esos animales y hacer uso de sus métodos que podrían clasificarse en tres: Heredados directamente de la clase padre: describeme()Heredados de la clase padre pero modificados: hablar() y moverse()Creados en la clase hija por lo tanto no existentes en la clase padre: picar() mi_perro = Perro('mamífero', 10) mi_vaca = Vaca('mamífero', 23) mi_abeja = Abeja('insecto', 1) mi_perro.hablar() mi_vaca.hablar() # Guau! # Muuu! mi_vaca.describeme() mi_abeja.describeme() # Soy un Animal del tipo Vaca # Soy un Animal del tipo Abeja mi_abeja.picar() # Picar! Uso de super() En pocas palabras, la función super() nos permite acceder a los métodos de la clase padre desde una de sus hijas. Volvamos al ejemplo de Animal y Perro. class Animal: def __init__(self, especie, edad): self.especie = especie self.edad = edad def hablar(self): pass def moverse(self): pass def describeme(self): print("Soy un Animal del tipo", type(self).__name__) Tal vez queramos que nuestro Perro tenga un parámetro extra en el constructor, como podría ser el dueño. Para realizar esto tenemos dos alternativas: Podemos crear un nuevo __init__ y guardar todas las variables una a una.O podemos usar super() para llamar al __init__ de la clase padre que ya aceptaba la especie y edad, y sólo asignar la variable nueva manualmente. class Perro(Animal): def __init__(self, especie, edad, dueño): # Alternativa 1 # self.especie = especie # self.edad = edad # self.dueño = dueño # Alternativa 2 super().__init__(especie, edad) self.dueño = dueño mi_perro = Perro('mamífero', 7, 'Luis') mi_perro.especie mi_perro.edad mi_perro.dueño Herencia múltiple En Python es posible realizar herencia múltiple. En otros posts hemos visto como se podía crear una clase padre que heredaba de una clase hija, pudiendo hacer uso de sus métodos y atributos. La herencia múltiple es similar, pero una clase hereda de varias clases padre en vez de una sola. Veamos un ejemplo. Por un lado tenemos dos clases Clase1 y Clase2, y por otro tenemos la Clase3 que hereda de las dos anteriores. Por lo tanto, heredará todos los métodos y atributos de ambas. class Clase1: pass class Clase2: pass class Clase3(Clase1, Clase2): pass Es posible también que una clase herede de otra clase y a su vez otra clase herede de la anterior. class Clase1: pass class Clase2(Clase1): pass class Clase3(Clase2): pass Llegados a este punto nos podemos plantear lo siguiente. Vale, como sabemos de otros posts las clases hijas heredan los métodos de las clases padre, pero también pueden reimplementarlos de manera distinta. Entonces, si llamo a un método que todas las clases tienen en común ¿a cuál se llama?. Pues bien, existe una forma de saberlo. La forma de saber a que método se llama es consultar el MRO o Method Order Resolution. Esta función nos devuelve una tupla con el orden de búsqueda de los métodos. Como era de esperar se empieza en la propia clase y se va subiendo hasta la clase padre, de izquierda a derecha. class Clase1: pass class Clase2: pass class Clase3(Clase1, Clase2): pass print(Clase3.__mro__) # (, , , ) Una curiosidad es que al final del todo vemos la clase object. Aunque pueda parecer raro, es correcto ya que en realidad todas las clases en Python heredan de una clase genérica object, aunque no lo especifiquemos explícitamente. Y como último ejemplo,…el cielo es el límite. Podemos tener una clase heredando de otras tres. Fíjate en que el MRO depende del orden en el que las clases son pasadas: 1, 3, 2. class Clase1: pass class Clase2: pass class Clase3: pass class Clase4(Clase1, Clase3, Clase2): pass print(Clase4.__mro__) # (, , , , ) Junto con la herencia, la cohesión, abstracción, polimorfismo, acoplamiento y encapsulamiento son otros de los conceptos claves para entender la programación orientada a objetos.
- Cohesión
- La cohesión hace referencia al grado de relación entre los elementos de un módulo. En el diseño de una función, es importante pensar bien la tarea que va a realizar, intentando que sea única y bien definida. Cuantas más cosas diferentes haga una función sin relación entre sí, más complicado será el código de entender. Existen por lo tanto dos tipos de cohesión: Por un lado tenemos la cohesión débil que indica que la relación entre los elementos es baja. Es decir, no pertenecen a una única funcionalidad.Por otro la cohesión fuerte, que debe ser nuestro objetivo al diseñar programas. La cohesión fuerte indica que existe una alta relación entre los elementos existentes dentro del módulo. Existe también otro concepto llamado acoplamiento que explicamos en otro post. Normalmente acoplamiento débil se relaciona con cohesión fuerte o alta. Veámoslo con un ejemplo. Tenemos una función suma() que suma dos números. El problema es que además de sumar dos números, los convierte a float() y además pide al usuario que introduzca por pantalla el número. Podría parecer que esas otras dos funcionalidades no son para tanto, pero si por ejemplo una persona quiere usar nuestra función suma() pero ya tiene los números y no quiere pedirlos por pantalla, no le serviría nuestra función. # Mal. Cohesión débil def suma(): num1 = float(input("Dame primer número")) num2 = float(input("Dame segundo número")) suma = num1 + num2 print(suma) suma() Para que la función tuviese una cohesión fuerte, sería conveniente que la suma realizara una única tarea bien definida, que es sumar. # Bien. Cohesión fuerte def suma(numeros): total = 0 for i in numeros: total = total + i return total Evidentemente un ejemplo tan sencillo como el explicado no tiene implicaciones demasiado graves, pero es importante buscar que las funciones realicen una única tarea (o conjunto) pero relacionadas entre sí. Diseñar código con cohesión fuerte nos permite: Reducir la complejidad del módulo, ya que tendrá un menor número de operaciones.Se podrá reutilizar los módulos más fácilmenteEl sistema será más fácilmente mantenible. Existen otros conceptos muy importantes y relacionados con la programación orientada a objetos. Aquí te los dejamos: HerenciaCohesiónAbstracciónPolimorfismoAcoplamientoEncapsulamiento
- Abstracción
- La abstracción es un termino que hace referencia a la ocultación de la complejidad intrínseca de una aplicación al exterior, centrándose sólo en como puede ser usada, lo que se conoce como interfaz. Si has oído hablar del enfoque caja negra, es un concepto muy relacionado. Dicho en otras palabras, la abstracción consiste en ocultar toda la complejidad que algo puede tener por dentro, ofreciéndonos unas funciones de alto nivel, por lo general sencillas de usar, que pueden ser usadas para interactuar con la aplicación sin tener conocimiento de lo que hay dentro. Una analogía del mundo real podría ser la televisión. Se trata de un dispositivo muy complejo donde han trabajado gran cantidad de ingenieros de diversas disciplinas como telecomunicaciones o electrónica. ¿Os imagináis que para cambiar de canal tuviéramos que saber todos los entresijos del aparato?. Pues bien, se nos ofrece una abstracción de la televisión, un mando a distancia. El mando nos abstrae por completo de la complejidad de los circuitos y señales, y nos da una interfaz sencilla que con unos pocos botones podemos usar. Algo muy parecido sucede en la programación orientada a objetos. Si tuviéramos una clase Televisor, en su interior podría haber líneas y líneas de código super complejas, pero una buena abstracción sería la que simplemente ofreciera los métodos encender(), apagar() y cambiar_canal() al exterior. Un concepto relacionado con la abstracción, serían las clases abstractas o más bien los métodos abstractos. Se define como clase abstracta la que contiene métodos abstractos, y se define como método abstracto a un método que ha sido declarado pero no implementado. Es decir, que no tiene código. Es posible crear métodos abstractos en Python con decoradores como @absttractmethod, pero esto lo dejamos para otro post.
- Polimorfismo
- El polimorfismo es uno de los pilares básicos en la programación orientada a objetos, por lo que para entenderlo es importante tener las bases de la POO y la herencia bien asentadas. El término polimorfismo tiene origen en las palabras poly (muchos) y morfo (formas), y aplicado a la programación hace referencia a que los objetos pueden tomar diferentes formas. ¿Pero qué significa esto? Pues bien, significa que objetos de diferentes clases pueden ser accedidos utilizando el mismo interfaz, mostrando un comportamiento distinto (tomando diferentes formas) según cómo sean accedidos. En lenguajes de programación como Python, que tiene tipado dinámico, el polimorfismo va muy relacionado con el duck typing. Sin embargo, para entender bien este concepto, es conveniente explicarlo desde el punto de vista de un lenguaje de programación con tipado estático como Java. Vamos a por ello. Polimorfismo en Java Vamos a comenzar definiendo una clase Animal. // Código Java class Animal{ public Animal() {} } Y dos clases Perro y Gato que heredan de la anterior. // Código Java class Perro extends Animal { public Perro() {} } class Gato extends Animal { public Gato() {} } El polimorfismo es precisamente lo que nos permite hacer lo siguiente: // Código Java Animal a = new Perro(); Recuerda que Java es un lenguaje con tipado estático, lo que significa que el tipo tiene que ser definido al crear la variable. Sin embargo estamos asignando a una variable Animal un objeto de la clase Perro. ¿Cómo es esto posible? Pues ahí lo tienes, el polimorfismo es lo que nos permite usar ambas clases de forma indistinta, ya que soportan el mismo “interfaz” (no confundir con el interface de Java). El siguiente código es también correcto. Tenemos un array de Animal donde cada elemento toma la forma de Perro o de Gato. // Código Java Animal[] animales = new Animal[10]; animales[0] = new Perro(); animales[1] = new Gato(); Sin embargo, no es posible realizar lo siguiente, ya que OtraClase no comparte interfaz con Animal. Tendremos un error error: incompatible types. // Código Java class OtraClase { public OtraClase() {} } Animal a = new OtraClase(); animales[0] = new OtraClase(); Polimorfismo en Python El término polimorfismo visto desde el punto de vista de Python es complicado de explicar sin hablar del duck typing, por lo que te recomendamos la lectura. Al ser un lenguaje con tipado dinámico y permitir duck typing, en Python no es necesario que los objetos compartan un interfaz, simplemente basta con que tengan los métodos que se quieren llamar. Podemos recrear el ejemplo de Java de la siguiente manera. Supongamos que tenemos un clase Animal con un método hablar(). class Animal: def hablar(self): pass Por otro lado tenemos otras dos clases, Perro, Gato que heredan de la anterior. Además, implementan el método hablar() de una forma distinta. class Perro(Animal): def hablar(self): print("Guau!") class Gato(Animal): def hablar(self): print("Miau!") A continuación creamos un objeto de cada clase y llamamos al método hablar(). Podemos observar que cada animal se comporta de manera distinta al usar hablar(). for animal in Perro(), Gato(): animal.hablar() # Guau! # Miau! En el caso anterior, la variable animal ha ido “tomando las formas” de Perro y Gato. Sin embargo, nótese que al tener tipado dinámico este ejemplo hubiera funcionado igual sin que existiera herencia entre Perro y Gato, pero esta explicación la dejamos para el capítulo de duck typing
- Acoplamiento
- El acoplamiento en programación (denominado coupling en Inglés) es un concepto que mide la dependencia entre dos módulos distintos de software, como pueden ser por ejemplo las clases. El acoplamiento puede ser de dos tipos: Acoplamiento débil, que indica que no existe dependencia de un módulo con otros. Esto debería ser la meta de nuestro software.Acoplamiento fuerte, que por lo contrario indica que un módulo tiene dependencias internas con otros. El término acoplamiento está muy relacionado con la cohesión, ya que acoplamiento débil suele ir ligado a cohesión fuerte. En general lo que buscamos en nuestro código es que tenga acoplamiento débil y cohesión fuerte, es decir, que no tenga dependencias con otros módulos y que las tareas que realiza estén relacionadas entre sí. Un código así es fácil de leer, de reusar, mantener y tiene que ser nuestra meta. Nótese que se suele emplear alta y baja para designar fuerza y débil respectivamente. Si aún no te hemos convencido de porque buscamos código débilmente acoplado, veamos lo que pasaría con un código fuertemente acoplado: Debido a las dependencias con otros módulo, un cambio en un modulo ajeno al nuestro podría tener un “efecto mariposa” en nuestro código, aún sin haber modificado directamente nuestro módulo.Si un módulo tiene dependencias con otros, reduce la reusabilidad, ya que para reusarlo deberíamos copiar también las dependencias. Veamos un ejemplo usando clases y objetos en Python. Tenemos una Clase1 que define un atributo de clase x. Por otro lado la Clase2 basa el comportamiento del método mi_metodo() en el valor de x de la Clase1. En este ejemplo existe acoplamiento fuerte, ya que existe una dependencia con una variable de otro módulo. class Clase1: x = True pass class Clase2: def mi_metodo(self, valor): if Clase1.x: self.valor = valor mi_clase = Clase2() mi_clase.mi_metodo("Hola") mi_clase.valor Puede parecer un ejemplo trivial, pero cuando el software se va complicando, no es nada raro acabar haciendo cosas de este tipo casi sin darnos cuenta. Hay veces que dependencias externas pueden estar justificadas, pero hay que estar muy seguro de lo que se hace. Este tipo de dependencias también puede hacer el código muy difícil de depurar. Imaginemos que nuestro código de la Clase2 funciona perfectamente, pero de repente alguien hace un cambio en la Clase1. Un cambio tan sencillo como el siguiente. Clase1.x = False Este cambio estaría modificando el comportamiento de nuestra clase y nos preguntaríamos ¿porqué ha dejado de funcionar mi código si no he tocado nada? A veces atribuimos estos comportamientos a la magia o radiación cósmica, pero simplemente tenemos código con acoplamiento fuerte.
- Encapsulamiento
- El encapsulamiento o encapsulación en programación es un concepto relacionado con la programación orientada a objetos, y hace referencia al ocultamiento de los estado internos de una clase al exterior. Dicho de otra manera, encapsular consiste en hacer que los atributos o métodos internos a una clase no se puedan acceder ni modificar desde fuera, sino que tan solo el propio objeto pueda acceder a ellos. Para la gente que conozca Java, le resultará un termino muy familiar, pero en Python es algo distinto. Digamos que Python por defecto no oculta los atributos y métodos de una clase al exterior. Veamos un ejemplo con el lenguaje Python. class Clase: atributo_clase = "Hola" def __init__(self, atributo_instancia): self.atributo_instancia = atributo_instancia mi_clase = Clase("Que tal") mi_clase.atributo_clase mi_clase.atributo_instancia # 'Hola' # 'Que tal' Ambos atributos son perfectamente accesibles desde el exterior. Sin embargo esto es algo que tal vez no queramos. Hay ciertos métodos o atributos que queremos que pertenezcan sólo a la clase o al objeto, y que sólo puedan ser accedidos por los mismos. Para ello podemos usar la doble __ para nombrar a un atributo o método. Esto hará que Python los interprete como “privados”, de manera que no podrán ser accedidos desde el exterior. class Clase: atributo_clase = "Hola" # Accesible desde el exterior __atributo_clase = "Hola" # No accesible # No accesible desde el exterior def __mi_metodo(self): print("Haz algo") self.__variable = 0 # Accesible desde el exterior def metodo_normal(self): # El método si es accesible desde el interior self.__mi_metodo() mi_clase = Clase() #mi_clase.__atributo_clase # Error! El atributo no es accesible #mi_clase.__mi_metodo() # Error! El método no es accesible mi_clase.atributo_clase # Ok! mi_clase.metodo_normal() # Ok! Y como curiosidad, podemos hacer uso de dir para ver el listado de métodos y atributos de nuestra clase. Podemos ver claramente como tenemos el metodo_normal y el atributo de clase, pero no podemos encontrar __mi_metodo ni __atributo_clase. print(dir(mi_clase)) #['_Clase__atributo_clase', '_Clase__mi_metodo', '_Clase__variable', #'__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', #'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', #'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', #'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', #'__str__', '__subclasshook__', '__weakref__', 'atributo_clase', 'metodo_normal'] Pues bien, en realidad si que podríamos acceder a __atributo_clase y a __mi_metodo haciendo un poco de trampa. Aunque no se vea a simple vista, si que están pero con un nombre distinto, para de alguna manera ocultarlos y evitar su uso. Pero podemos llamarlos de la siguiente manera, pero por lo general no es una buena idea. mi_clase._Clase__atributo_clase # 'Hola' mi_clase._Clase__mi_metodo() # 'Haz algo'
- Herencia
Excepciones
- Las excepciones en Python son una herramienta muy potente que la gran mayoría de lenguajes de programación modernos tienen. Se trata de una forma de controlar el comportamiento de un programa cuando se produce un error. Esto es muy importante ya que salvo que tratemos este error, el programa se parará, y esto es algo que en determinadas aplicaciones no es una opción válida.
- Uso de raise
- Uso de raise También podemos ser nosotros los que levantemos o lancemos una excepción. Volviendo a los ejemplos usados en el apartado anterior, podemos ser nosotros los que levantemos ZeroDivisionError o NameError usando raise. La sintaxis es muy fácil. raise ZeroDivisionError("Información de la excepción") O podemos lanzar otra de tipo NameError. raise NameError("Información de la excepción") Se puede ver como el string que hemos pasado se imprime a continuación de la excepción. Se puede llamar también sin ningún parámetro como se hace a continuación. raise ZeroDivisionError Visto esto, ya sabemos como una excepción puede ser lanzada. Existen dos maneras principalmente: Hacemos una operación que no puede ser realizada (como dividir por cero). En este caso Python se encarga de lanzar automáticamente la excepción.O también podemos lanzar nosotros una excepción manualmente, usando raise.Habría un tercer caso que sería lanzar una excepción que no pertenece a las definidas por defecto en Python. Pero eso te lo explicamos aquí. A continuación veremos que podemos hacer para controlar estas excepciones, y que hacer cuando se lanza una para que no se interrumpa la ejecución del programa.
- Uso de try y except
- La buena noticia es que las excepciones que hemos visto antes, pueden ser capturadas y manejadas adecuadamente, sin que el programa se detenga. Veamos un ejemplo con la división entre cero. a = 5; b = 0 try: c = a/b except ZeroDivisionError: print("No se ha podido realizar la división") En este caso no verificamos que b!=0. Directamente intentamos realizar la división y en el caso de que se lance la excepción ZeroDivisionError, la capturamos y la tratamos adecuadamente. La diferencia con el ejemplo anterior es que ahora no se para el programa y se puede seguir ejecutando. Prueba a ejecutar el código y ver que pasa. Verás como el programa ya no se para. Entonces, lo que hay dentro del try es la sección del código que podría lanzar la excepción que se está capturando en el except. Por lo tanto cuando ocurra una excepción, se entra en el except pero el programa no se para. También se puede capturar diferentes excepciones como se ve en el siguiente ejemplo. try: #c = 5/0 # Si comentas esto entra en TypeError d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError except ZeroDivisionError: print("No se puede dividir entre cero!") except TypeError: print("Problema de tipos!") Puedes también hacer que un determinado número de excepciones se traten de la misma manera con el mismo bloque de código. Sin embargo suele ser más interesante tratar a diferentes excepciones de diference manera. try: #c = 5/0 # Si comentas esto entra en TypeError d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError except (ZeroDivisionError, TypeError): print("Excepcion ZeroDivisionError/TypeError") Otra forma si no sabes que excepción puede saltar, puedes usar la clase genérica Exception. En este caso se controla cualquier tipo de excepción. De hecho todas las excepciones heredan de Exception. Ver documentación. try: #c = 5/0 # Si comentas esto entra en TypeError d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError except Exception: print("Ha habido una excepción") No obstante hay una forma de saber que excepción ha sido la que ha ocurrido. try: d = 2 + "Hola" # Si comentas esto entra en ZeroDivisionError except Exception as ex: print("Ha habido una excepción", type(ex)) # Ha habido una excepción
- Uso de else
- Al ya explicado try y except le podemos añadir un bloque más, el else. Dicho bloque se ejecutará si no ha ocurrido ninguna excepción. Fíjate en la diferencia entre los siguientes códigos. try: # Forzamos una excepción al dividir entre 0 x = 2/0 except: print("Entra en except, ha ocurrido una excepción") else: print("Entra en else, no ha ocurrido ninguna excepción") #Entra en except, ha ocurrido una excepción Sin embargo en el siguiente código la división se puede realizar sin problema, por lo que el bloque except no se ejecuta pero el else si es ejecutado. try: # La división puede realizarse sin problema x = 2/2 except: print("Entra en except, ha ocurrido una excepción") else: print("Entra en else, no ha ocurrido ninguna excepción") #Entra en else, no ha ocurrido ninguna excepción
- Uso de finally
- A los ya vistos bloques try, except y else podemos añadir un bloque más, el finally. Dicho bloque se ejecuta siempre, haya o no haya habido excepción. Este bloque se suele usar si queremos ejecutar algún tipo de acción de limpieza. Si por ejemplo estamos escribiendo datos en un fichero pero ocurre una excepción, tal vez queramos borrar el contenido que hemos escrito con anterioridad, para no dejar datos inconsistenes en el fichero. En el siguiente código vemos un ejemplo. Haya o no haya excepción el código que haya dentro de finally será ejecutado. try: # Forzamos excepción x = 2/0 except: # Se entra ya que ha habido una excepción print("Entra en except, ha ocurrido una excepción") finally: # También entra porque finally es ejecutado siempre print("Entra en finally, se ejecuta el bloque finally") #Entra en except, ha ocurrido una excepción #Entra en finally, se ejecuta el bloque finally
Imágenes y archivos del mapa
Información
- Descripción
- Mapa Mental sobre Python, creado por Francisco Hernandez el 24/05/2020.
- Creado por
- Francisco Hernandez
- Visitas
- 9
- Valoración



























