Volver

3. Nivel básico-intermedio de Python

Hoy veremos

  • 1. Input - Output (I/O)

    Input-output
  • 2. Lambdas

    Lambda syntax
  • 3. Programación orientada a objetos

    OOP

I/O

Sabemos cómo imprimir datos por consola:


x = 4
print(f"2+2 = {x}")

print("Damn")
print("LaTeX no soportado por defecto: " + "x^2 = 2$")
print("123", "456", "789", sep="-")

print("Hola", end=" ")
print("mundo!")

                    

Veamos el proceso inverso: la introducción de datos por consola.

I/O

Podemos pedir al usuario que introduzca datos usando input("mensaje")


    username = input("Dime tu nombre: ")
    print(username)
    
                        

Si el prompt es largo, es recomendable escribirlo por separado de antemano:


    prompt = "Buenos días! Este es el servicio de atención al usuario.\n"
    prompt += "Por favor, escribe tu nombre: "

    nombre = input(prompt)
    print(f"Hola, {nombre}.")

                        

I/O

Por defecto, input() devuelve strings.


    edad = input("¿Cuántos años tienes? ")
    if edad >= 18:       # TypeError: '>=' not supported between instances of 'str' and 'int'
        print("Adulto")
    else:
        print("Menor")

                    

Si queremos usar el resultado como otro tipo de dato, podemos convertirlo explícitamente.


    edad = input("¿Cuántos años tienes? ")
    edad = int(edad)
    print(edad >= 18)     # True o False

    precio = input("¿Cuánto cuesta una barra de pan?")
    precio = float(precio)
    if precio > 1.85:
        print("Caro, si me preguntan")

                    

I/O

La idea de pedir input por consola no es solo una curiosidad!
Se utiliza en la práctica, por ejemplo para construir menús


    menu = "Introduce una opción:\n"
    menu += "\t a. Buscar en la base de datos.\n"
    menu += "\t b. Añadir entrada a la base de datos.\n"
    menu += "\t c. Salir.\n"

    msg = ""
    while (msg != "c"):
        msg = input(menu).lower()

        if msg == "a": buscar()
        elif msg == "b": añadir()
        elif msg == "c": print("¡Adiós!")
        else: print("Opción incorrecta.")

                    

Excepciones

Excepciones

Si durante la ejecución ocurre un error, el programa finalizará abruptamente.

El manejo de excepciones nos permite indicar al programa cómo afrontar un error de manera controlada para que pueda continuar ejecutándose.

Los bloques de manejo de excepciones utilizan 4 keywords:

  • try: intenta hacer esto, a ver si no falla $\dots$
  • except: si falla algo dentro del try, haz esto $\dots$
  • else: si todo va bien dentro del try, haz esto $\dots$
  • finally: pase lo que que pase, haz esto $\dots$

Ni except, ni else ni finally son obligatorios.


    try:
        # Código que puede producir un error
        pass
    except:
        # Se ejecuta si se produce un error
        # El resto de secuencias de try no se ejecutan
        pass
    else:
        # Se ejecuta después de todo el código de try si no se produce un error
        pass
    finally:
        # Se ejecuta en último lugar, independientemente de si se ha producido un error o no
        pass
                    

Nota: el orden es importante. try-else-except no es válido. Quitar los pass y preguntar por qué el código daría un error? else es útil para lo que dependa de que el bloque de try tenga éxito else y finally son anecdóticos Puede haber else sin except, finally sin ... pero todos dependen de try

Algunos ejemplos


    def dividir(a, b):
        try:
            res = a / b
        except ZeroDivisionError as e:
            print("Error. b no puede ser 0:", e)
        else:
            print(res)
        finally:
            print("Terminado!")

    dividir(3, 0)

    dividir(3, 1)

                    

    def dividir(a, b):
        try:
            res = a / b
            print(res)
        except:
            print("Error. b no puede ser 0")

    dividir(3, 0)

    dividir(3, 1)

                    

Excepciones

except permite recoger varios tipos de errores.

También podemos generar nuestras propias excepciones.

Volveremos a esto (necesitamos el concepto de clase).

I/O por archivo

Lectura de archivos (input)

Función básica: open(path, modo)


    # Ruta relativa (i.e. desde el mismo directorio que el script)
    archivo = open("archivo.txt", "r")  

    # Ruta absoluta (i.e. desde el directorio raíz)
    archivo2 = open("C:\\Users\\Xiana\\Desktop\\archivo.txt", "r")

    arch_bin = open("archivo_binario.txt", "rb")

                

Lectura de archivos (input)

Hay diferentes formas de leer el contenido:



    archivo = open("archivo.txt", "r")
    contenido = archivo.read()     # Todo el contenido como una única cadena
    print(contenido)
    archivo.close()






    archivo2 = open("archivo.txt", "r")
    lineas = archivo2.readlines()
    for linea in lineas:
        print(linea.rstrip())   
        # Sin rstrip(), se introducirían 2 saltos de línea
    archivo2.close()





    archivo3 = open("archivo.txt", "r")
    for linea in archivo3:
        print(linea.rstrip()) 
    archivo3.close()





    archivo4 = open("archivo.txt", "r")
    lineas = archivo4.read().splitlines()
    for linea in archivo4:
        print(linea)
        # No hace falta rstrip()
    archivo4.close()




    archivo5 = open("archivo.txt", "r")
    linea = archivo5.readline()
    while linea:
        print(linea.rstrip())
        linea = archivo5.readline()
    archivo5.close()




                    

Importante: cerrar los ficheros!

  • Evita la corrupción de los datos.
  • Asegura que los últimos cambios se guarden correctamente.
  • Libera memoria RAM que se estaba usando como buffer.

Lectura de archivos (input)

Es preferible abrir archivos usando with:

  • Cierra los recursos automáticamente.
  • Maneja los errores
  • Código más limpio y compacto

    # El archivo se cierra automáticamente,
    # no es necesario incluir archivo.close()
    with open("archivo.txt", "r") as archivo:
        for linea in archivo:
            print(linea.rstrip())

                        

La sintaxis de with es equivalente al siguiente bloque de código:


    try:
        archivo = open("archivo.txt", "r")
        try:
            datos = archivo.read()
            print(datos)
        finally:
            archivo.close()

                        
TODO: with para input y output a la vez Nota: no es necesario hacer close en el bloque externo porque el archivo no se ha llegado a abrir (de hecho, daría un error)

Lectura de archivos (input)

Por supuesto, pueden seguir occuriendo varios errores al abrir archivos $\dots$


    try:
        with open("archivo.txt", "r") as archivo:      # archivo = open("archivo.txt", "r")
            for linea in archivo:
                print(linea.rstrip())
    except FileNotFoundError:
        print("El archivo no existe")
    except IOError as e:
        print(f"Error al acceder al archivo: {e}")
    except:
        print("Error desconocido")

                    

Escritura de archivos (output)

La función open("archivo.txt", modo) tiene varios modos para abrir ficheros:

  • r $\rightarrow$ Read mode. Lee el archivo desde el inicio. Si el archivo no existe, se genera un error.
  • w $\rightarrow$ Write mode. Borra el contenido del archivo y permite escribir nuevo texto sobre él. Si el archivo no existe, lo crea.
  • a $\rightarrow$ Append mode. Añade nuevo texto desde el final del archivo. Si el archivo no existe, lo crea.
  • x $\rightarrow$ Create mode. Crea el archivo y permite escribir en él. Si ya existe, genera un error.

r es la opción por defecto: open("fichero.txt", "r") y open("fichero.txt") hacen lo mismo.

A cada uno de estos modos se le puede añadir b, para manejar los archivos en binario, o t, para manejarlos como archivos de texto (por defecto).

Si queremos leer y escribir a la vez, podemos añadir +.

Escritura de archivos (output)

Modo Uso Posición del cursor ¿Crea el archivo si no existe? ¿Sobreescribe el contenido?
r Lectura Inicio No No
r+ Lectura y escritura Inicio No No
w Escritura Inicio
w+ Lectura y escritura Inicio
a Escritura (append) Final No
a+ Lectura y escritura (append) Final No

Escritura de archivos (output)

Diagrama de modos de la funcióno open()
Más info

Funciones lambda

Lambdas meme

Funciones lambda

  • Son funciones anónimas (i.e. sin nombre) útiles para tareas pequeñas y sencillas.
  • Su origen está en la investigación sobre el concepto de función que dio origen al cálculo lambda de Alonzo Church (1930s).
  • El estilo funcional del cálculo lambda y las funciones lambda busca actuar sin tener en cuenta el estado actual del sistema y evitar los efectos secundarios.
  • Python no es un lenguaje funcional, pero adoptó algunos conceptos de lenguajes funcionales.
  • Las funciones lambda solo pueden tener expresiones puras. No puede haber expresiones con asignaciones $=$, ni bloques while, try, etc.
Unlike lambda forms in other languages, where they add functionality, Python lambdas are only a shorthand notation if you're too lazy to define a function.
Documentación de Python

Sintaxis

lambda argumentos: expresión
  • lambda indica que se trata de una función lambda
  • argumentos: Los parámetros (0, 1 o varios). Estas variables solo pueden ser usadas dentro de la lambda en la que se declaran.
  • expresión: El cuerpo de la función, que indica qué devuelve. Debe ser una única expresión.

    # Algunos ejemplos de sintaxis
    lambda x: x + 1
    lambda: True

    # Podemos darle un nombre y ejecutarla luego
    cuadrado = lambda x: x * x
    print(cuadrado(3))     # 9

                        

Más ejemplos

Función normal de división


    def dividir(x, y):
        if y != 0:
            return x / y
        else:
            return "undefined"
                            

$\Longrightarrow$

Lambda de división


    lambda x, y: x / y if y != 0 else "undefined"



    
                            

Función de formato de nombre


def nombre_completo(nombre, apellido):
    return f"{nombre.title()} {apellidos.title()}"

                            

$\Longrightarrow$

Lambda de formato de nombre


lambda nombre, apellido: f"{nombre.title()} {apellidos.title()}"    
                    

                            

Casos de uso columnes

Las funciones lambda son especialmente útiles cuando se usan junto a iterables y funciones como filter, map, sorted, etc.
map(func, iter) aplica una función a cada elemento de un iterable.

Función + map


    def doblar(x):
        return 2 * x
    
    lista = [1, 2, 3, 4]
    dobles = map(dobles, lista)
    print(dobles)      # [2, 4, 6, 8]
                                

$\Longrightarrow$

Lambda + map (más conciso)


    lista = [1, 2, 3, 4]
    dobles = list(map(lambda x: 2 * x, lista))
    print(dobles)      # [2, 4, 6, 8]


    
                                
filter(func, iter) mantiene solo aquellos elementos del iterable que cumplen func(elem)=True.

Función + filter


    def es_par(x):
        return x % 2 == 0

    lista = range(2, 7)
    res = list(filter(es_par, lista))
    print(res)      # [2, 4, 6]
                            

$\Longrightarrow$

Lambda + filter (más conciso)


    lista = range(2, 7)
    res = list(filter(lambda x: x % 2 == 0, lista))
    print(res)      # [2, 4, 6]



                            
sorted(iter, key, reverse) devuelve una lista ordenada a partir de un iterable.

    lista = list('vwerfa')
    ordenada = sorted(lista)
    print(ordenada) 
    # >>> ['a', 'e', 'f', 'r', 'v', 'w']

    # Ordenar números con una función especial (key)
    nums = [15, 11, 4, -5, 0, -1, 9]
    def dist_10(n):
        return abs(10-n)
    print(sorted(nums, key=dist_10))
    # >>> [11, 9, 15, 4, 0, -1, -5]
                                

$\Longrightarrow$


    # Ordenamos los números igual, pero ahora la key es un lambda
    nums = [15, 11, 4, -5, 0, -1, 9]
    print(sorted(nums, key=lambda n: abs(10-n), reverse=True)) 
    # >>> [-5, -1, 0, 4, 15, 11, 9]






                            

(Aún) más ejemplos

  • Filtrar elementos $> 20$

    datos = range(5, 50, 5)
    mayores_20 = filter(lambda x: x > 20, datos)
    print(list(mayores_20)) # [25, 30, 35, 40, 45]

                        
  • Ordenar una lista de tuplas por el segundo elemento

    datos = [(4, 20), (1, 30), (2, 10)]
    datos_ordenados = sorted(data, key=lambda x: x[1])
    print(datos_ordenados)  # [(2, 10), (3, 20), (1, 30)]

                        

    Lorem codium

                        

    Lorem codium

                        

Quizás mejor dejar esto como ejercicios... Also: TODO ver reduce (hay que importarla)

Funciones lambda

✅ Ventajas

  • Concisas, compactas
  • Útiles en casos sencillos
  • Pueden ser combinadas con conceptos de lenguaje funcional (map, filter, etc.)
  • No tienen nombre ⟹ menos variables, código más limpio

❌ Desventajas

  • No pueden ser reutilizadas (a menos que se asignen a una variable...)
  • Pueden reducir la legibilidad y ser sobreusadas
  • En caso de error, es más difícil seguir el stack trace
  • Peor documentación, no se pueden manejar excepciones con try

Python Object Oriented Programming
aka
Python OOP
aka
POOP 💩

Programación Orientada a Objetos: una breve introducción

Es un paradigma de programación basado en el concepto de objetos.
  • Un objeto tiene un estado y un comportamiento.
  • Estado: atributos = propiedades -> variables que almacenan datos de un objeto.
  • Comportamiento: métodos = funciones -> definen qué puede hacer el objeto.
  • Los objetos se abstraen en forma de clases. Un objeto es una instancia de una clase.
Python OOP

Veamos cómo implementar el ejemplo de la imagen

  1. Primero necesitamos crear la clase con class Person y añadir su constructor, esto es, el método que se encarga de crear los objetos: __init__(self)
  2. Ahora añadimos los atributos de cada persona, su nombre, edad, género y ocupación. Pasamos estos datos al constructor, que los guardará usando self.<atributo>
  3. Los métodos son funciones dentro de la clase que modifican al objeto. Su primer argumento tiene que ser self, que representa el propio objeto.
  4. Para este ejemplo, añadimos también un atributo adicional de salud que tiene cada persona, health. Lo inicializamos a $100$ en el constructor y modificamos en cada llamada a los métodos.
                            
    class Person:
        def __init__(self):
            pass
    












    


    









                            
                                
    class Person:
        def __init__(self, name, age, gender, occupation):
            self.name = name
            self.age = age
            self.gender = gender
            self.occupation = occupation
    
    # Ya podemos crear objetos de la clase Persona
    marta = Person("Marta", 20, "F", "estudiante")
    artai = Person("Artai", 23, "NB", "dibujante")












    






                                
                                    
    class Person:
        def __init__(self, name, age, gender, occupation):
            self.name = name
            self.age = age
            self.gender = gender
            self.occupation = occupation

        def walk(self):
            print(f"{self.name} está caminando")
        
        def eat(self, comida):
            print(f"{self.name} está comiendo {comida}")
            
        def sleep(self):
            print("No hace nada...")
        
        def work():
            print("Qué currante")

    marta = Person("Marta", 20, "F", "estudiante")
    artai = Person("Artai", 23, "NB", "dibujante")





            

    
                                    
                                        
    class Person:
        def __init__(self, name, age, gender, occupation):
            self.name = name
            self.age = age
            self.gender = gender
            self.occupation = occupation
            self.health = 100 # Salud inicial
    
        def walk(self):
            print(f"{self.name} está caminando")
            self.health += 5 # Caminar es bueno para la salud
        
        def eat(self, comida):
            print(f"{self.name} está comiendo {comida}")
            self.health += 10 # Recuperamos fuerzas
            
        def sleep(self):
            print("No hace nada...")
            self.health += 20 # A mimir zzzzz
        
        def work():
            print("Qué currante")
            self.health -= 10 # El trabajo es duro
    
    marta = Person("Marta", 20, "F", "estudiante")
    artai = Person("Artai", 23, "NB", "dibujante")
    


                                        

Métodos

Ahora podemos llamar a los métodos de cada objeto creados con la sintaxis objeto.metodo(args...)

Con esto, ejecutamos una acción sobre el objeto, posiblemente alterando su estado.

Importante: Cuando llamamos a un método de un objeto, no hace falta pasar self


    marta = Person("Marta", 20, "F", "estudiante")
    artai = Person("Artai", 23, "NB", "dibujante")

    marta.walk()         # Marta está caminando	
    marta.eat("pizza")   # Marta está comiendo pizza

    # Podemos también acceder e imprimir los valores del objeto
    print(marta.name)   # Marta
    print(marta.health) # 115
    marta.work()        # Qué currante
    print(marta.health) # 105

                

Herencia

  • La herencia es un mecanismo que contienen todos los lenguajes orientados a objetos en los que una clase, la subclase, "expande" a otra, la superclase.
  • Las subclases se caracterizan por ser un tipo particular de la superclase (''is a'').
  • Por ejemplo, un doctor es un tipo particular de persona. Por tanto, podríamos tener una clase Doctor que expandiera a Person
  • Podemos llamar a los elementos de la superclase con super().<metodo>()
  • En el constructor de la subclase, podemos llamar o no al constructor de la superclase (pero es buena práctica hacerlo).

    class Doctor(Person):
        def __init__(self, name, age, gender, specialty):
            # Buena práctica: El constructor original de Person cubre campos comúnes
            super().__init__(name, age, "M", "doctor")
            # Ahora añadimos el resto del estado inicial
            self.specialty = specialty

                    

Los objetos de subclase pueden acceder y llamar a los métodos de su clase base.
Por ejemplo, un nuevo Doctor podría llamar a walk()


    doc = Doctor("House", 45, "M", "médico de cabecera")
    doc.walk() # House está caminando

                    
Nota obligatoria: en la mayoría de los casos, suele ser mejor idea usar composición en vez de herencia!

Herencia: overriding

Una subclase puede reimplementar un método que ya está en la superclase. Esto se conoce como overriding (o sobreescritura)

Por ejemplo, si queremos que un Doctor camine de forma diferente a una Persona normal, podemos hacerlo así:


    class Doctor(Person):
        def walk(self):
            print(f"{self.name} está caminando... pero con bata")
                    

Más avanzado: Esto lleva al concepto de polimorfismo. Las clases que sobreescriben métodos se pueden comportar como instancias de su superclase, hasta que llaman a uno de los métodos sobreescritos. Entonces, se ejecuta la versión sobreescrita en vez de la original $\dots$

Un ejemplo práctico: clases de excepciones personalizadas

  • Python permite crear excepciones personalizadas heredando de la clase Exception o sus subclases
  • Las excepciones personalizadas mejoran la legibilidad y mantenibilidad del código
  • Son útiles para manejar errores específicos de nuestra aplicación

    class UXAError(Exception):
        def __init__(self, message = "Tu matrícula es incorrecta"):
            super().__init__(message)

                    

Uso de la excepción personalizada en código:


    def matricularse_usc(str_matricula):
        if str_matricula == "No quiero estudiar":
            raise UXAError()
        return str_matricula + " aceptada"
    
    try:
        resultado = matricularse_usc("No quiero estudiar")
    except UXAErrror as error:
        print(f"Error al matricularse en la USC: {error}")
    
                    

Más sobre Python OOP

Podemos implementar el método __str__(self) para modificar como se imprime un objeto de una clase


    class Person:
        def __str__(self):
            return f"{self.name} es un {self.occupation} de {self.age}"
                        f"años y género {self.gender}"
    
    # Ahora podemos imprimir más fácilmente un objeto Person
    marta = Person("Marta", 20, "F", "estudiante")
    print(marta) # Marta es un estudiante de 20 años y género F
    
                        

Hay muchos más métodos mágicos de este estilo, por ejemplo __em__ para reimplementar la igualdad entre dos objetos de la misma clase, __add__ para la suma, $\dots$


    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
        def __add__(self, other):
            return Vector(self.x + other.x, self.y + other.y)
        def __str__(self):
            return f"({self.x}, {self.y})"
    v1 = Vector(1, 2)
    v2 = Vector(3, 4)
    print(v1 + v2) # (4, 6)
                        

La clase object es la clase base de todas las demás, aunque no se especifique explícitamente. Los tipos y contenedores primitivos (int, str, list, dict, $\dots$) también son subclases de object

Más avanzado: Gracias a que todos los objetos sean subclases de object, funcionan los métodos mágicos! Todos estos métodos están definidos en object, y no hacemos más que overridearlos. El lenguaje interpreta cada uno de ellos de forma especial, y los asigna a los operadores. Por ejemplo v1 + v2 es lo mismo que v1.__add__(v2)