Programación con Python

Python ha logrado convertirse en uno de los lenguajes de programación más utilizados. Ello es debido principalmente a su enfoque en la simplicidad, legibilidad, versatilidad y su creciente ecosistema de bibliotecas y herramientas. Estas características lo hacen ideal para una amplia gama de aplicaciones y hacen que sea una elección popular entre desarrolladores de todos los niveles de experiencia.

Historia

La historia de Python se remonta a finales de los 80 y principios de los 90, cuando Guido van Rossum, un programador holandés, comenzó a desarrollar el lenguaje. La primera versión pública, Python 0.9.0, fue lanzada en febrero de 1991. El nombre «Python» fue inspirado por el programa humorístico de la televisión británica llamado «Monty Python’s Flying Circus» del cual Guido era un gran fan.

Con el paso de los años, Python ganó popularidad debido a su diseño simple, legibilidad y su enfoque en la facilidad de uso. A medida que evolucionó, se convirtió en un lenguaje de programación muy versátil, adoptado por una amplia comunidad de desarrolladores. La filosofía detrás de Python, conocida como «The Zen of Python» destaca la importancia de la legibilidad del código y la simplicidad.

¿Por qué es considerado uno de los mejores lenguajes de programación que existen?

  1. Legibilidad y sintaxis clara: Python tiene una sintaxis simple y limpia que lo hace fácil de leer y escribir. La falta de llaves y el uso de indentación significativa para delimitar bloques de código hacen que los programas escritos en Python sean más legibles y menos propensos a errores.
  2. Versatilidad: Python es un lenguaje multipropósito, lo que significa que se puede utilizar para una amplia variedad de aplicaciones, como desarrollo web, análisis de datos, inteligencia artificial, automatización, scripting, entre otros.
  3. Gran comunidad y soporte: Python cuenta con una comunidad activa y dedicada de desarrolladores en todo el mundo. Esto resulta en una abundancia de bibliotecas y módulos de terceros que facilitan el desarrollo de proyectos, permitiendo a los programadores reutilizar código y acelerar el proceso de desarrollo.
  4. Portabilidad: Python es un lenguaje interpretado, lo que significa que un programa escrito en Python puede ejecutarse en diferentes plataformas sin necesidad de hacer modificaciones en el código fuente.
  5. Fácil de aprender para principiantes: Su sintaxis simple y clara lo convierte en una excelente opción para aquellos que recién están empezando a programar.
  6. Enfoque en la productividad: Python prioriza la productividad del desarrollador, permitiendo a los programadores hacer más con menos líneas de código.
  7. Comunidad y apoyo de la industria: Python es ampliamente adoptado y utilizado por empresas líderes en tecnología como Google, Facebook, Netflix, Dropbox, y muchas otras, lo que ha contribuido a su crecimiento y desarrollo continuo.

Principales características de Python

  1. Tipado dinámico: Python es un lenguaje de tipado dinámico, lo que significa que no es necesario declarar el tipo de variable antes de usarla. Las variables pueden cambiar de tipo durante la ejecución del programa.
  2. Interpretado: Python es un lenguaje interpretado, lo que significa que no necesita ser compilado antes de ejecutarse. Esto permite una mayor flexibilidad y facilidad en el desarrollo y prueba de código.
  3. Orientado a objetos: Python es un lenguaje de programación orientado a objetos, lo que permite la encapsulación, herencia y polimorfismo, lo que facilita el desarrollo de software modular y estructurado.
  4. Bibliotecas incluidas: Python incluye una amplia biblioteca estándar que proporciona una gran cantidad de módulos y funciones para realizar diversas tareas sin la necesidad de escribir mucho código adicional.
  5. Gestión automática de memoria: Python cuenta con un recolector de basura que se encarga de liberar automáticamente la memoria utilizada por objetos que ya no se necesitan, lo que simplifica la administración de la memoria para el programador.

Aprende a programar con Python

  • Variables. En Python, una variable es un espacio de memoria reservado para almacenar un valor. No es necesario declarar el tipo de variable, ya que Python es de tipado dinámico. Simplemente se asigna un valor a una variable y Python inferirá su tipo automáticamente.
  • Operadores. Los operadores en Python son símbolos que permiten realizar diferentes operaciones en variables y valores. Por ejemplo, los operadores aritméticos (+, -, *, /) se utilizan para realizar operaciones matemáticas.
  • Cadenas de texto. En Python, las cadenas de texto son secuencias de caracteres que se utilizan para representar texto. Se definen entre comillas simples o dobles. Python ofrece una variedad de métodos para manipular y trabajar con cadenas, como concatenación, extracción de subcadenas, conversión entre mayúsculas y minúsculas, entre otros.
  • Conversiones entre datos básicos. Python permite convertir datos entre diferentes tipos utilizando funciones incorporadas como int(), float(), str(), bool(), etc. Esto es útil para realizar operaciones entre diferentes tipos de datos.
  • Control de flujo:
    • Condicionales. Permiten tomar decisiones en función de ciertas condiciones. Se utilizan principalmente las estructuras if, elif (else if), y else para ejecutar diferentes bloques de código según el resultado de una expresión condicional. Los operadores de comparación (==, >, <, >=, <=, !=) se utilizan para comparar valores y los operadores lógicos (and, or, not) se utilizan para combinar condiciones.
    • Bucles. Los bucles permiten repetir una acción o un bloque de código varias veces. Python ofrece dos tipos de bucles principales: bucle for, que se utiliza para iterar sobre elementos de una secuencia (como una lista o una cadena), y bucle while, que se repite mientras se cumple una condición dada.
  • Estructuras de datos:
    • Listas. Son colecciones ordenadas y modificables de elementos. Se definen utilizando corchetes [] y pueden contener diferentes tipos de datos. Las listas permiten agregar, eliminar y modificar elementos, y se accede a sus elementos mediante índices.
    • Tuplas. Son colecciones ordenadas e inmutables de elementos. Se definen utilizando paréntesis () y, a diferencia de las listas, no pueden modificarse después de su creación. Las tuplas son útiles para proteger datos que no deben cambiar.
    • Diccionarios. Son colecciones de elementos que se almacenan como pares clave-valor. Se definen utilizando llaves {} y permiten acceder a sus elementos a través de sus claves en lugar de índices. Son ideales para buscar y almacenar datos de manera eficiente.
  • Funciones. Las funciones son bloques de código reutilizables que se definen una vez y se pueden llamar múltiples veces en el programa. Permiten modularizar el código y facilitan su mantenimiento y comprensión.
  • Ficheros. Python permite leer y escribir en archivos utilizando funciones incorporadas como open(), read(), write(), entre otras. Esto es útil para trabajar con datos almacenados en archivos externos.
  • Clases y objetos. Python es un lenguaje de programación orientado a objetos. Las clases son plantillas para crear objetos, que son instancias de una clase. Las clases encapsulan datos y funciones relacionadas, lo que permite organizar y reutilizar código de manera más efectiva.
  • SQLite. SQLite es una base de datos ligera y de código abierto que se integra de manera nativa en Python. Permite crear, modificar y consultar bases de datos relacionales desde aplicaciones Python sin necesidad de un servidor de base de datos externo. Es ampliamente utilizado para aplicaciones que requieren almacenamiento de datos local y sencillo.

Programación con Python: SQLite

SQLite es una librería que proporciona un sistema de gestión de bases de datos (SGBD) relacional y se incluye con Python de forma predeterminada. Es una base de datos extremadamente liviana, eficiente y fácil de usar, lo que la hace ideal para aplicaciones pequeñas o medianas que requieren almacenamiento local de datos. A continuación mostraremos cómo utilizar SQLite en Python, con explicaciones y ejemplos prácticos.

Instalación

Python incluye SQLite como parte de la biblioteca estándar, por lo que no es necesario instalar nada adicional para comenzar a trabajar con SQLite en Python.

Conexión a la base de datos

Antes de interactuar con la base de datos, necesitamos establecer una conexión. Para ello, utilizamos el módulo sqlite3 que viene integrado con Python:

import sqlite3

# Crear una conexión o conectarse a una base de datos existente (si no existe, se creará)
conexion = sqlite3.connect("mi_base_de_datos.db")

Crear una tabla

Después de establecer la conexión, ya podemos crear una tabla en la base de datos. Para ello, necesitamos un objeto «cursor» que nos permitirá ejecutar comandos SQL:

# Obtener un objeto cursor
cursor = conexion.cursor()

# Crear una tabla
cursor.execute("CREATE TABLE IF NOT EXISTS usuarios (id INTEGER PRIMARY KEY, nombre TEXT, edad INTEGER)")

Insertar datos

Podemos insertar datos en una tabla utilizando el método execute() para ejecutar un comando SQL de inserción:

# Insertar datos en la tabla
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('Juan', 30)")
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('María', 25)")

# Guardar los cambios (commit) en la base de datos
conexion.commit()

Consultar datos

Para obtener datos de una tabla, podemos utilizar el método execute() para ejecutar una consulta SQL de selección y luego utilizar los métodos fetchone() o fetchall() para obtener los resultados:

# Consultar datos de la tabla
cursor.execute("SELECT * FROM usuarios")

# Obtener un solo resultado
primer_usuario = cursor.fetchone()
print(primer_usuario)  # Salida: (1, 'Juan', 30)

# Obtener todos los resultados
todos_los_usuarios = cursor.fetchall()
print(todos_los_usuarios)  # Salida: [(1, 'Juan', 30), (2, 'María', 25)]

Actualizar datos

Para actualizar los datos de una tabla también usaremos el método execute(), utilizando en este caso como parámetro un comando SQL de actualización:

# Actualizar datos en la tabla
cursor.execute("UPDATE usuarios SET edad = 31 WHERE nombre = 'Juan'")

# Guardar los cambios (commit) en la base de datos
conexion.commit()

Eliminar datos

Para eliminar datos de una tabla, también haremos uso del método execute() utilizando un comando SQL de eliminación:

# Eliminar datos de la tabla
cursor.execute("DELETE FROM usuarios WHERE nombre = 'María'")

# Guardar los cambios (commit) en la base de datos
conexion.commit()

Cerrar la conexión

Es importante cerrar la conexión una vez que hayamos terminado de trabajar con la base de datos:

# Cerrar la conexión
conexion.close()

Transacciones

Las transacciones son bloques de operaciones que se ejecutan como una sola unidad. SQLite permite trabajar con transacciones para asegurar la integridad de los datos. Las transacciones se inician con BEGIN y se confirman con COMMIT. Si algo sale mal, se pueden revertir con ROLLBACK.

Veamos un ejemplo a continuación. Si todas las operaciones dentro de la transacción se realizan correctamente, se confirman las inserciones de los nuevos registros y los cambios se guardan en la base de datos. Si alguna operación falla, los inserciones se revierten y no se realizan cambios en la base de datos:

import sqlite3

conexion = sqlite3.connect("mi_base_de_datos.db")
cursor = conexion.cursor()

# Iniciar transacción
cursor.execute("BEGIN")

try:
    # Realizar operaciones en la base de datos
    cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('Ana', 28)")
    cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('Pedro', 35)")
    # ... más operaciones SQL ...

    # Confirmar transacción
    cursor.execute("COMMIT")
    print("Transacción exitosa")
except Exception as e:
    # Revertir transacción en caso de error
    cursor.execute("ROLLBACK")
    print("Error en la transacción:", e)

conexion.close()

Consultas parametrizadas

Es recomendable utilizar consultas parametrizadas para evitar la inyección de SQL y mejorar la seguridad de nuestras aplicaciones. Las consultas parametrizadas se crean mediante el método execute() y los valores que se utilizan se pueden pasar como una tupla o un diccionario:

# Consulta parametrizada con tupla
nombre = "Luis"
edad = 40
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES (?, ?)", (nombre, edad))

# Consulta parametrizada con diccionario
usuario = {"nombre": "Sofía", "edad": 27}
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES (:nombre, :edad)", usuario)

Ejemplo completo con una tabla

A continuación mostramos un ejemplo completo que resume todas las operaciones que hemos visto:

import sqlite3

# Conexión a la base de datos
conexion = sqlite3.connect("mi_base_de_datos.db")
cursor = conexion.cursor()

# Crear tabla si no existe
cursor.execute("CREATE TABLE IF NOT EXISTS usuarios (id INTEGER PRIMARY KEY, nombre TEXT, edad INTEGER)")

# Insertar datos
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('Juan', 30)")
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('María', 25)")

# Consultar datos
cursor.execute("SELECT * FROM usuarios")
todos_los_usuarios = cursor.fetchall()
print(todos_los_usuarios)  # Salida: [(1, 'Juan', 30), (2, 'María', 25)]

# Actualizar datos
cursor.execute("UPDATE usuarios SET edad = 31 WHERE nombre = 'Juan'")
cursor.execute("SELECT * FROM usuarios")
todos_los_usuarios = cursor.fetchall()
print(todos_los_usuarios)  # Salida: [(1, 'Juan', 31), (2, 'María', 25)]

# Eliminar datos
cursor.execute("DELETE FROM usuarios WHERE nombre = 'María'")
cursor.execute("SELECT * FROM usuarios")
todos_los_usuarios = cursor.fetchall()
print(todos_los_usuarios)  # Salida: [(1, 'Juan', 31)]

# Cerrar conexión
conexion.close()

Creación de múltiples tablas

SQLite ofrece muchas más funcionalidades, como la capacidad de trabajar con múltiples tablas, realizar consultas complejas, utilizar funciones agregadas y trabajar con claves ajenas para establecer relaciones entre tablas.

Para crear múltiples tablas en una base de datos SQLite, simplemente debemos ejecutar múltiples comandos CREATE TABLE. Cada tabla se crea con su propia estructura y columnas:

import sqlite3

conexion = sqlite3.connect("mi_base_de_datos.db")
cursor = conexion.cursor()

# Crear una tabla de usuarios
cursor.execute("CREATE TABLE IF NOT EXISTS usuarios (id INTEGER PRIMARY KEY, nombre TEXT, edad INTEGER)")

# Crear una tabla de productos
cursor.execute("CREATE TABLE IF NOT EXISTS productos (id INTEGER PRIMARY KEY, nombre TEXT, precio REAL)")

conexion.close()

Relaciones entre tablas y claves ajenas

SQLite permite definir claves ajenas para establecer relaciones entre los registros de dos o más tablas. En el siguiente ejemplo crearemos una tabla de «pedidos» que tiene una clave ajena que nos permite llegar a la tabla de «usuarios»:

import sqlite3

conexion = sqlite3.connect("mi_base_de_datos.db")
cursor = conexion.cursor()

# Crear tabla de usuarios
cursor.execute("CREATE TABLE IF NOT EXISTS usuarios (id INTEGER PRIMARY KEY, nombre TEXT, edad INTEGER)")

# Crear tabla de pedidos con clave ajena
cursor.execute("CREATE TABLE IF NOT EXISTS pedidos (id INTEGER PRIMARY KEY, usuario_id INTEGER, fecha TEXT, FOREIGN KEY (usuario_id) REFERENCES usuarios(id))")

conexion.close()

Consultas más complejas

SQLite permite realizar consultas SQL más complejas utilizando cláusulas JOIN, GROUP BY, HAVING, ORDER BY, y otras. Por ejemplo, supongamos que tenemos una tabla «ventas» que contiene información sobre las ventas realizadas en una empresa, relacionando los productos que se han vendido con los usuarios que los han comprado. En este caso, podemos llegar a obtener información sobre el total de ventas por producto utilizando la cláusula GROUP BY y la función agregada SUM:

import sqlite3

conexion = sqlite3.connect("mi_base_de_datos.db")
cursor = conexion.cursor()

# Crear tabla de usuarios
cursor.execute("CREATE TABLE IF NOT EXISTS usuarios (id INTEGER PRIMARY KEY, nombre TEXT, edad INTEGER)")

# Crear tabla de productos
cursor.execute("CREATE TABLE IF NOT EXISTS productos (id INTEGER PRIMARY KEY, nombre TEXT, precio REAL)")

# Crear tabla de ventas
cursor.execute("CREATE TABLE IF NOT EXISTS ventas (id INTEGER PRIMARY KEY, usuario_id INTEGER, producto_id INTEGER, cantidad INTEGER)")

# Insertar datos de ejemplo
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('Juan', 30)")
cursor.execute("INSERT INTO usuarios (nombre, edad) VALUES ('María', 25)")

cursor.execute("INSERT INTO productos (nombre, precio) VALUES ('Producto A', 10.5)")
cursor.execute("INSERT INTO productos (nombre, precio) VALUES ('Producto B', 20.0)")

cursor.execute("INSERT INTO ventas (usuario_id, producto_id, cantidad) VALUES (1, 1, 3)")
cursor.execute("INSERT INTO ventas (usuario_id, producto_id, cantidad) VALUES (2, 2, 2)")
cursor.execute("INSERT INTO ventas (usuario_id, producto_id, cantidad) VALUES (1, 2, 1)")

# Consulta para obtener el total de ventas por producto
cursor.execute("SELECT productos.nombre, SUM(ventas.cantidad) FROM ventas JOIN productos ON ventas.producto_id = productos.id GROUP BY productos.nombre")

resultado = cursor.fetchall()
print(resultado) # [('Producto A', 3), ('Producto B', 3)]

conexion.close()

Programación con Python: Clases y objetos

La Programación Orientada a Objetos (POO) es un paradigma de programación que se centra en la organización del código en objetos, los cuales encapsulan datos y comportamientos relacionados. Python es un lenguaje de programación que soporta completamente la POO y permite crear clases y objetos de manera sencilla. A continuación mostraremos la sintaxis relacionada con la POO en Python, incluyendo la creación de clases, la definición de atributos y métodos, la herencia y el encapsulamiento, utilizando explicaciones y ejemplos prácticos.

Clases y Objetos

En POO, una clase es una plantilla que define la estructura y el comportamiento de un objeto. Un objeto es una instancia particular de una clase, es decir, una entidad real con sus propios datos y métodos. Para definir una clase en Python, utilizamos la palabra clave class, seguida del nombre de la clase (por convención, los nombres de clase suelen empezar con mayúscula):

# Definición de una clase simple
class MiClase:
    pass

# Creación de un objeto (instancia) de la clase MiClase
objeto1 = MiClase()

Atributos y métodos

Los atributos son variables que pertenecen a una clase y representan las características de los objetos. Los métodos son funciones definidas dentro de una clase y describen el comportamiento de los objetos. Los métodos siempre tienen como primer parámetro self, que se refiere al objeto en sí mismo:

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad

    def saludar(self):
        print(f"Hola, mi nombre es {self.nombre} y tengo {self.edad} años.")

# Crear un objeto de la clase Persona
persona1 = Persona("Juan", 30)

# Acceder a los atributos
print(persona1.nombre)  # Salida: "Juan"
print(persona1.edad)    # Salida: 30

# Llamar a un método del objeto
persona1.saludar()  # Salida: "Hola, mi nombre es Juan y tengo 30 años."

En este ejemplo, definimos la clase Persona con los atributos nombre y edad, y el método saludar() que muestra un mensaje con los valores de los atributos.

Constructor __init__()

El método __init__() es especial en Python y se llama automáticamente cuando se crea un nuevo objeto de la clase. Se utiliza habitualmente para inicializar los atributos del objeto:

class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad

    def saludar(self):
        print(f"Hola, mi nombre es {self.nombre} y tengo {self.edad} años.")

# Crear un objeto de la clase Persona
persona1 = Persona("María", 25)
persona1.saludar()  # Salida: "Hola, mi nombre es María y tengo 25 años."

Atributos de clase y atributos de instancia

En Python, los atributos de clase son comunes a todas las instancias de la clase, mientras que los atributos de instancia son específicos para cada objeto:

class Circulo:
    # Atributo de clase (común a todos los círculos)
    pi = 3.14159

    def __init__(self, radio):
        # Atributo de instancia (especifico para cada círculo)
        self.radio = radio

    def calcular_area(self):
        return self.pi * self.radio ** 2

# Crear objetos de la clase Circulo
circulo1 = Circulo(5)
circulo2 = Circulo(10)

# Acceder a los atributos de clase (usando el nombre de la clase)
print(Circulo.pi)  # Salida: 3.14159

# Acceder a los atributos de instancia (usando el objeto)
print(circulo1.radio)  # Salida: 5
print(circulo2.radio)  # Salida: 10

# Llamar a un método del objeto
print(circulo1.calcular_area())  # Salida: 78.53975
print(circulo2.calcular_area())  # Salida: 314.159

Herencia

La herencia es un concepto clave en la POO y permite crear una nueva clase basada en una clase existente. La clase nueva hereda atributos y métodos de la clase existente y puede añadir sus propios atributos y métodos:

class Animal:
    def __init__(self, especie):
        self.especie = especie

    def hacer_sonido(self):
        pass

class Perro(Animal):
    def __init__(self, raza):
        super().__init__("Perro")
        self.raza = raza

    def hacer_sonido(self):
        return "Guau Guau!"

class Gato(Animal):
    def __init__(self, color):
        super().__init__("Gato")
        self.color = color

    def hacer_sonido(self):
        return "Miau Miau!"

# Crear objetos de las clases Perro y Gato
perro1 = Perro("Labrador")
gato1 = Gato("Negro")

# Acceder a atributos y llamar a métodos de las clases y de la clase base (Animal)
print(perro1.especie)         # Salida: "Perro"
print(perro1.raza)            # Salida: "Labrador"
print(perro1.hacer_sonido())  # Salida: "Guau Guau!"

print(gato1.especie)          # Salida: "Gato"
print(gato1.color)            # Salida: "Negro"
print(gato1.hacer_sonido())   # Salida: "Miau Miau!"

Métodos de clase y métodos estáticos

Los métodos de clase (@classmethod) son métodos que se definen en una clase y operan en la clase en sí misma, en lugar de en instancias individuales de la clase. Estos métodos pueden ser accedidos directamente desde la clase:

class MiClase:
    contador = 0

    def __init__(self):
        MiClase.contador += 1

    @classmethod
    def obtener_contador(cls):
        return cls.contador

objeto1 = MiClase()
objeto2 = MiClase()

# Llamar al método de clase desde la clase
print(MiClase.obtener_contador())  # Salida: 2

Los métodos estáticos (@staticmethod) son similares a los métodos de clase, pero no tienen acceso a la instancia (self) ni a la clase (cls). Se utilizan para realizar tareas que están relacionadas con la clase, pero no dependen de los atributos o métodos de la clase:

class Utilidades:
    @staticmethod
    def sumar(a, b):
        return a + b

    @staticmethod
    def restar(a, b):
        return a - b

resultado_suma = Utilidades.sumar(5, 3)
resultado_resta = Utilidades.restar(10, 4)

print(resultado_suma)  # Salida: 8
print(resultado_resta)  # Salida: 6

Encapsulamiento

El encapsulamiento es un concepto que se refiere a ocultar detalles internos de una clase y proteger sus atributos y métodos. En Python no disponemos de modificadores de acceso como en otros lenguajes (p. ej., public, private). Sin embargo, podemos indicar que un atributo o método es «privado» mediante una convención de nomenclatura agregando dos guiones bajos al inicio del nombre (p. ej., __atributo):

class CuentaBancaria:
    def __init__(self, titular, saldo):
        self.__titular = titular
        self.__saldo = saldo

    def depositar(self, cantidad):
        self.__saldo += cantidad

    def retirar(self, cantidad):
        if cantidad <= self.__saldo:
            self.__saldo -= cantidad
        else:
            print("Saldo insuficiente.")

    def obtener_titular(self):
        return self.__titular

    def obtener_saldo(self):
        return self.__saldo

# Crear un objeto de la clase CuentaBancaria
cuenta = CuentaBancaria("Juan", 1000)

# Usar los métodos para interactuar con los atributos de la clase
cuenta.depositar(500)
print("Depositados 500 euros")

cuenta.retirar(200)
print("Retirados 200 euros")

# Usar los métodos para obtener el titular y el saldo de la cuenta
print(f"{cuenta.obtener_titular()} tiene {cuenta.obtener_saldo()} euros")  # Salida: Juan tiene 1300 euros

Propiedades

Las propiedades son una forma de controlar el acceso a los atributos de una clase, y permiten además la ejecución de código adicional al consultar o asignar valores, pudiendo realizar validaciones de los datos. Se utilizan para definir métodos especiales (getter y setter) que se comportan como atributos:

class Persona:
    def __init__(self, nombre, edad):
        self.__nombre = nombre
        self.__edad = edad

    # Getter para obtener el nombre
    @property
    def nombre(self):
        return self.__nombre

    # Setter para asignar el nombre y validar la longitud
    @nombre.setter
    def nombre(self, valor):
        if len(valor) > 3:
            self.__nombre = valor
        else:
            print("El nombre debe tener más de 3 caracteres.")

    # Getter para obtener la edad
    @property
    def edad(self):
        return self.__edad

    # Setter para asignar la edad y validar que sea mayor que 0
    @edad.setter
    def edad(self, valor):
        if valor > 0:
            self.__edad = valor
        else:
            print("La edad debe ser mayor que 0.")

# Crear un objeto de la clase Persona
persona = Persona("Juan", 30)

# Acceder al atributo usando la propiedad (getter)
print(persona.nombre)  # Salida: "Juan"

# Asignar un nuevo valor al atributo usando la propiedad (setter)
persona.nombre = "Ana"  # Salida: "El nombre debe tener más de 3 caracteres."

# Asignar un nuevo valor válido al atributo usando la propiedad (setter)
persona.nombre = "María"
print(persona.nombre)  # Salida: "María"

# Intentar asignar un valor no válido a la edad
persona.edad = -5  # Salida: "La edad debe ser mayor que 0."

# Asignar un valor válido a la edad
persona.edad = 25
print(persona.edad)  # Salida: 25

Métodos Especiales

En Python, los métodos especiales (también conocidos como «métodos mágicos») son métodos que tienen doble guion bajo al inicio y al final del nombre. Son utilizados para sobrecargar operadores y permitir la personalización del comportamiento de una clase.

En el siguiente ejemplo vamos a sobrecargar los métodos __str__, __add__, __sub__, __eq__ y __lt__ para personalizar la representación de un objeto Punto como una cadena y también para permitir la suma, la resta y la comparación utilizando las coordenadas x e y de cada punto. Estos métodos especiales nos permiten definir el comportamiento de los operadores en la clase Punto, lo que proporciona una mayor flexibilidad y facilidad de uso cuando interactuamos con objetos de esta clase. A continuación mostramos cómo podemos especificar dicha funcionalidad y sobrecargar los operadores ==, <, + y - en la clase Punto:

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Punto({self.x}, {self.y})"

    def __add__(self, otro_punto):
        return Punto(self.x + otro_punto.x, self.y + otro_punto.y)

    def __sub__(self, otro_punto):
        return Punto(self.x - otro_punto.x, self.y - otro_punto.y)

    def __eq__(self, otro_punto):
        return self.x == otro_punto.x and self.y == otro_punto.y

    def __lt__(self, otro_punto):
        distancia_origen_self = (self.x ** 2 + self.y ** 2) ** 0.5
        distancia_origen_otro = (otro_punto.x ** 2 + otro_punto.y ** 2) ** 0.5
        return distancia_origen_self < distancia_origen_otro

# Crear objetos de la clase Punto
punto1 = Punto(1, 2)
punto2 = Punto(3, 4)
punto3 = Punto(1, 2)

# Uso del método especial __str__() al imprimir el objeto
print(punto1)  # Salida: Punto(1, 2)
print(punto2)  # Salida: Punto(3, 4)
print(punto3)  # Salida: Punto(1, 2)

# Sobrecarga del operador ==
print(punto1 == punto2)  # Salida: False
print(punto1 == punto3)  # Salida: True

# Sobrecarga del operador <
punto4 = Punto(5, 5)
punto5 = Punto(3, 4)
print(punto4 < punto5)  # Salida: False (distancia al origen de punto4 es mayor)

# Sobrecarga del operador +
resultado_suma = punto1 + punto2
print(resultado_suma)  # Salida: Punto(4, 6)

# Sobrecarga del operador -
resultado_resta = punto1 - punto2
print(resultado_resta)  # Salida: Punto(-2, -2)

En este ejemplo, la clase Punto tiene el método especial __str__(), que devuelve una cadena que representa el objeto en un formato personalizado. Cuando imprimimos los objetos punto1, punto2 y punto3 Python utiliza automáticamente el método __str__() para obtener la representación de cadena del objeto, lo que nos permite ver las coordenadas del punto de una forma legible.

Además, hemos sobrecargado los métodos __add__() y __sub__() para definir la suma y resta de objetos Punto. Esto nos permite realizar operaciones aritméticas directamente entre objetos Punto y obtener nuevos objetos Punto con las coordenadas sumadas o restadas.

Y por último hemos sobrecargado los métodos __eq__() y __lt__() para utilizar los operadores == y <, respectivamente.

El método __eq__() permite comparar si dos objetos Punto son iguales. En este caso, estamos comparando las coordenadas x e y de ambos puntos. Si ambas coordenadas son iguales, entonces los objetos Punto son considerados iguales.

El método __lt__() permite comparar si un objeto Punto es menor que otro, en función de su distancia al origen (punto en coordenadas [0, 0]). Calculamos la distancia al origen de ambos puntos y devolvemos True si la distancia del primer punto es menor que la del segundo, de lo contrario, devolvemos False.

Herencia múltiple y «mixins»

En Python, una clase puede heredar de varias clases, lo que se conoce como herencia múltiple. Esto permite que una clase obtenga atributos y métodos de varias clases base:

class A:
    def metodo_a(self):
        print("Método A")

class B:
    def metodo_b(self):
        print("Método B")

class C(A, B):
    def metodo_c(self):
        print("Método C")

objeto_c = C()
objeto_c.metodo_a()  # Salida: "Método A"
objeto_c.metodo_b()  # Salida: "Método B"
objeto_c.metodo_c()  # Salida: "Método C"

También podemos utilizar «mixins» con este propósito. Los «mixins» son pequeñas clases que se utilizan para agregar funcionalidades específicas a otras clases. La idea es que contengan piezas de código que puedan mezclarse o combinarse con otras clases para agregar ciertas características. Los «mixins» no están diseñados para ser instanciados por sí mismos, sino para ser usados como componentes adicionales. Permiten que las clases compartan comportamientos comunes sin la necesidad de heredar de una clase base que puede tener otros métodos y atributos que no son necesarios para todas las subclases.

Veamos un ejemplo de cómo podemos utilizar un «mixin» para agregar funcionalidad a una clase Vehiculo que representa un vehículo genérico:

class Vehiculo:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo

    def conducir(self):
        print(f"Conduciendo el {self.marca} {self.modelo}")

Ahora queremos agregar funcionalidad adicional a la clase Vehiculo para que algunos vehículos puedan navegar por el agua. Para ello creamos un «mixin » llamado NavegableMixin:

class NavegableMixin:
    def navegar(self):
        print(f"Navegando con el {self.marca} {self.modelo}")

Finalmente, combinamos el «mixin» NavegableMixin con la clase Vehiculo para crear una nueva clase Barco que puede navegar:

class Barco(Vehiculo, NavegableMixin):
    pass

barco = Barco("Transatlántico", "Titanic")
barco.conducir()  # Salida: "Conduciendo el Transatlántico Titanic"
barco.navegar()   # Salida: "Navegando con el Transatlántico Titanic"

En este ejemplo, hemos creado una clase NavegableMixin que tiene un método navegar(). Luego, creamos la clase Barco que hereda de la clase Vehiculo y mezcla el mixin NavegableMixin. De esta manera, la clase Barco hereda la funcionalidad de la clase Vehiculo y también obtiene la capacidad de navegar del «mixin» NavegableMixin.

La utilización de «mixins» permite una mayor modularidad y reutilización de código, ya que podemos crear «mixins» con funcionalidades específicas y combinarlos con diferentes clases según sea necesario.

Es importante tener en cuenta que el orden en que se heredan las clases y mixins puede afectar el comportamiento de la clase final. En el ejemplo anterior, heredamos primero de Vehiculo y luego del mixin NavegableMixin, lo que asegura que los métodos de la clase base (Vehiculo) tengan prioridad en caso de existir métodos con el mismo nombre en el mixin. Si el orden fuera invertido, los métodos del «mixin» tendrían prioridad. Por lo tanto, es recomendable tener cuidado con el orden de herencia y mezcla de clases y mixins para evitar posibles conflictos.

Programación con Python: Ficheros

Trabajar con ficheros es esencial para leer y escribir datos en el sistema de archivos de nuestro ordenador. A continuación mostraremos la sintaxis básica para utilizar ficheros en Python, incluyendo cómo abrir, leer y escribir en ellos. Utilizaremos explicaciones acompañadas de ejercicios prácticos.

Abrir y cerrar ficheros

Para trabajar con un archivo en Python, primero debemos abrirlo. Podemos usar la función open() para abrir un archivo en diferentes modos, como lectura ('r') o escritura ('w') y también para añadir ('a'). Después de terminar de trabajar con el fichero, debemos cerrarlo utilizando el método close() para liberar recursos y también para evitar la pérdida de datos:

# Abrir un archivo en modo de lectura
archivo = open("archivo.txt", "r")

# Trabajar con el archivo (leer o escribir)

# Cerrar el archivo después de terminar
archivo.close()

Leer contenido de un fichero

Para leer el contenido de un archivo, podemos usar los métodos read(), readline() o readlines():

  • read(). Lee todo el contenido del archivo como una cadena de texto:
archivo = open("archivo.txt", "r")
contenido = archivo.read()
print(contenido)
archivo.close()
  • readline(). Lee una línea del archivo:
archivo = open("archivo.txt", "r")
linea = archivo.readline()
print(linea)
archivo.close()
  • readlines(). Lee todas las líneas del archivo y las devuelve como una lista:
archivo = open("archivo.txt", "r")
lineas = archivo.readlines()
print(lineas)
archivo.close()

Escribir en un archivo

Para escribir en un archivo, debemos abrirlo en modo de escritura, con 'w' o para añadir, con 'a', según lo que necesitemos en cada momento:

  • Modo escritura ('w'). Sobrescribe el contenido del archivo existente o crea un nuevo archivo si no existe:
archivo = open("archivo.txt", "w")
archivo.write("Este es un nuevo contenido.\n")
archivo.write("¡Hola, Mundo!\n")
archivo.close()
  • Modo añadir ('a'). Agrega contenido al final del archivo existente o crea un nuevo archivo si no existe:
archivo = open("archivo.txt", "a")
archivo.write("Este es un contenido adicional.\n")
archivo.write("¡Hola de nuevo!\n")
archivo.close()

Gestores de contexto

El uso de un gestor de contexto con la declaración with es una forma más segura y eficiente de trabajar con ficheros. Utilizando esta funcionalidad, el archivo se cierra automáticamente cuando el bloque with termina, incluso si se produce una excepción:

# Leer contenido del archivo con un Context Manager
with open("archivo.txt", "r") as archivo:
    contenido = archivo.read()
    print(contenido)

# Escribir en el archivo con un Context Manager
with open("archivo.txt", "a") as archivo:
    archivo.write("Esto es un nuevo contenido con Context Manager.\n")

Verificar si un archivo existe

Podemos usar el módulo os para verificar si un archivo existe antes de abrirlo.

import os

nombre_archivo = "archivo.txt"

if os.path.exists(nombre_archivo):
    with open(nombre_archivo, "r") as archivo:
        contenido = archivo.read()
        print(contenido)
else:
    print(f"El archivo '{nombre_archivo}' no existe.")

Programación con Python: Funciones

Las funciones son bloques de código reutilizables en Python que pueden tener argumentos de entrada, realizar un conjunto de acciones y devolver un resultado. Son una parte fundamental de la programación, ya que nos permiten dividir tareas complejas en piezas más pequeñas y organizadas. A continuación analizaremos la sintaxis básica de las funciones en Python, con explicaciones y ejemplos prácticos.

Definición de funciones

En Python podemos definir una función utilizando la palabra clave def, seguida del nombre de la función y los paréntesis de apertura y cierre, que pueden contener los argumentos de la función. A continuación debemos añadir dos puntos : y el bloque de código que forma el cuerpo de la función:

def saludar():
    print("¡Hola! Bienvenido.")

Llamadas a funciones

Para ejecutar el código de la función simplemente debemos realizar una llamada utilizando su nombre seguido de paréntesis:

def saludar():
    print("¡Hola!")

saludar()  # Salida: ¡Hola!

Argumentos de funciones

Las funciones pueden aceptar argumentos de entrada, que son valores que se pasan a la función cuando se llama. Los parámetros que recibirán esos argumentos se escriben entre paréntesis en la definición de la función:

def saludar(nombre):
    print(f"¡Hola, {nombre}!")

saludar("Juan")   # Salida: ¡Hola, Juan!
saludar("María")  # Salida: ¡Hola, María!

Parámetros con valores predeterminados

Podemos asignar valores predeterminados a los parámetros de una función. Si un valor no se proporciona al llamar a la función, se utilizará el valor predeterminado:

def saludar(nombre="Invitado"):
    print(f"¡Hola, {nombre}!")

saludar()          # Salida: ¡Hola, Invitado!
saludar("Carlos")  # Salida: ¡Hola, Carlos!

Parámetros posicionales

Los parámetros posicionales son la forma más básica y común de pasar argumentos a una función en Python. Estos parámetros reciben sus valores en el orden en que son pasados cuando se llama a la función:

def nombre_de_funcion(parametro1, parametro2, parametro3):
    # Cuerpo de la función
    # Podemos usar los parámetros dentro de la función
    print(parametro1, parametro2, parametro3)

nombre_de_funcion("a", "b", "c") # Salida: a b c

Por ejemplo, si queremos realizar una división entre dos números, podemos definir los parámetros posicionales a y b de la función dividir(). Cuando llamamos a la función dividir(10, 5), los valores 10 y 5 se asignan a los parámetros a y b, respectivamente:

def dividir(a, b):
    resultado = a / b
    return resultado

resultado = dividir(10, 5)
print(resultado)  # Salida: 2.0

Orden de los argumentos en una función

El orden de los argumentos en una función es esencial cuando se utilizan parámetros posicionales. Si cambiamos el orden, los valores se asignarán a los parámetros de forma diferente y la función puede no producir los resultados esperados:

def restar(a, b):
    resultado = a - b
    return resultado

resultado_resta1 = restar(10, 3)
print(resultado_resta1)  # Salida: 7

resultado_resta2 = restar(3, 10)
print(resultado_resta2)  # Salida: -7

En el segundo caso, se asignaron los valores 3 y 10 a a y b, respectivamente. Como la operación en la función es a - b, obtuvimos -7 en lugar de 7.

Orden de los parámetros en la definición de la función

El orden en que definamos los parámetros posicionales en la declaración de la función también es importante. En el siguiente ejemplo definimos la función potencia , recibiendo los parámetros (a, b) en el primer caso, y (b, a) en el segundo:

def potencia(a, b):
    resultado = a ** b
    return resultado

resultado1 = potencia(2, 3)
print(resultado1)  # Salida: 8

# Cambio en el orden de los parámetros
def potencia(b, a):
    resultado = a ** b
    return resultado

resultado2 = potencia(2, 3)
print(resultado2)  # Salida: 9

En el segundo caso, se asignaron los valores 3 y 2 a a y b, respectivamente. Como la operación en la función es a ** b, obtuvimos 9 en lugar de 8.

Parámetros nominales

Los parámetros nominales son una característica relativamente nueva en Python que se introdujo en la versión 3.8. Nos permiten pasar argumentos a una función utilizando sus nombres en lugar de su posición. Esto puede hacer que el código sea más legible y menos propenso a errores, especialmente cuando tenemos muchas opciones de argumentos o valores predeterminados en una función.

Para definir una función con parámetros nominales, debemos usar un símbolo de igual = después del nombre del parámetro y proporcionar un valor predeterminado:

def saludar(nombre="Invitado", apellido=""):
    print(f"¡Hola, {nombre}!")

saludar(nombre="Juan", apellido="Ruiz")    # Salida: ¡Hola, Juan Ruiz!
saludar(apellido="Rubio", nombre="María")  # Salida: ¡Hola, María Rubio!

# En la siguiente llamada se usarán los valores predeterminados
saludar()  # Salida: ¡Hola, Invitado!

En el ejemplo, la función saludar tiene dos parámetros nominales nombre y apellido, con valores predeterminados establecidos. Podemos llamar a la función pasando los argumentos utilizando sus nombres. Esto hace que el orden de los argumentos no sea relevante y permite que el código sea más claro.

Los parámetros nominales también son útiles cuando una función tiene muchos argumentos, algunos de los cuales pueden tener valores predeterminados que no deseas cambiar. En lugar de pasar valores específicos para todos los argumentos, simplemente pasas los valores que necesitas cambiar.

Paso de argumentos variables

Si no estamos seguro de cuántos argumentos recibirás, puedes utilizar *args para pasar un número variable de argumentos posicionales y **kwargs para pasar un número variable de argumentos clave-valor:

def funcion(*args, **kwargs):
    print(args)     # Una tupla con argumentos posicionales
    print(kwargs)   # Un diccionario con argumentos clave-valor

funcion(1, 2, 3, nombre="Ana", edad=30)
# Salida:
# (1, 2, 3)
# {'nombre': 'Ana', 'edad': 30}

Valor de retorno

Las funciones pueden devolver un valor utilizando la palabra clave return. Esto es útil cuando queremos que una función realice un cálculo o una tarea y devuelva el resultado para su posterior uso:

def sumar(a, b):
    resultado = a + b
    return resultado

resultado_suma = sumar(3, 5)
print(resultado_suma)  # Salida: 8

Podemos devolver múltiples valores utilizando la instrucción return con una tupla:

def calcular(a, b):
    suma = a + b
    resta = a - b
    multiplicacion = a * b
    return suma, resta, multiplicacion

resultado = calcular(10, 5)
print(resultado)  # Salida: (15, 5, 50)

Además, podemos definir el valor y el tipo que devolverá la función dependiendo del flujo de código. A continuación vamos a ver un ejemplo más complejo donde utilizamos una sentencia match case para calcular el área de diferentes figuras geométricas, y devolvemos el resultado como valor de retorno. Vamos a crear una función llamada calcular_area que recibe el nombre de la figura geométrica («cuadrado», «rectángulo», «círculo» o «triángulo») y las medidas necesarias para calcular el área. Dependiendo del caso, el match case realiza el cálculo del área y lo utiliza como valor de retorno de la función. Incluso contemplamos la posibilidad de que la figura que se reciba como parámetro no exista, en cuyo caso se devolverá el valor por defecto con el tipo None:

def calcular_area(figura, medida1, medida2=None):
    match figura:
        case "cuadrado":
            return medida1 ** 2
        case "rectangulo":
            return medida1 * medida2
        case "circulo":
            return 3.1416 * medida1 ** 2
        case "triangulo":
            return 0.5 * medida1 * medida2
        case _:
            return None

area_cuadrado = calcular_area("cuadrado", 5)
area_rectangulo = calcular_area("rectangulo", 4, 6)
area_circulo = calcular_area("circulo", 3)
area_triangulo = calcular_area("triangulo", 2, 8)

print("Área del cuadrado:", area_cuadrado)     # Área del cuadrado: 25
print("Área del rectángulo:", area_rectangulo) # Área del rectángulo: 24
print("Área del círculo:", area_circulo)       # Área del círculo: 28.2744
print("Área del triángulo:", area_triangulo)   # Área del triángulo: 8.0

En este último ejemplo, el match case nos permite realizar los cálculos del área de diferentes formas geométricas de manera sencilla y legible. Cada caso corresponde a una forma geométrica específica, y el resultado se calcula utilizando las medidas proporcionadas. Al utilizar el match case, evitamos la necesidad de utilizar una serie de sentencias if-elif-else para identificar la forma geométrica y realizar el cálculo correspondiente. El código resultante es más limpio y más claro.

Ámbito de variables

Las variables declaradas dentro de una función tienen un alcance local y solo existen dentro de la función:

def funcion():
    variable_local = "Soy local"
    print(variable_local)

funcion()  # Salida: Soy local

# La siguiente línea generaría un error ya que variable_local no está definida en este ámbito
# print(variable_local)

Las variables declaradas fuera de una función tienen un alcance global y son accesibles desde cualquier parte del programa. Si queremos utilizar una variable global dentro de una función, debemos usar la palabra clave global antes de la variable:

contador_global = 0

def incrementar_contador():
    global contador_global
    contador_global += 1

incrementar_contador()
print(contador_global)  # Salida: 1

Funciones anidadas

Podemos definir funciones dentro de otras funciones, lo que se conoce como funciones anidadas:

def funcion_principal():
    print("Función principal")

    def funcion_anidada():
        print("Función anidada")

    funcion_anidada()

funcion_principal()
# Salida:
# Función principal
# Función anidada

Funciones lambda

Las funciones lambda son funciones anónimas y pequeñas que pueden tener cualquier número de parámetros, pero solo pueden tener una expresión. Se definen utilizando la palabra clave lambda:

# Función lambda que devuelve el cuadrado de un número
cuadrado = lambda x: x ** 2

resultado = cuadrado(5)
print(resultado)  # Salida: 25

Programación con Python: Diccionarios

Los diccionarios de Python son una estructura de datos extremadamente útil y versátil, ya que permiten almacenar y organizar datos en pares clave-valor. Cada elemento en un diccionario está compuesto por una clave única y su valor correspondiente. Los diccionarios son ampliamente utilizados en el desarrollo de aplicaciones, y su capacidad para manejar datos complejos y anidados los convierte en una herramienta esencial. A continuación explicaremos la funcionalidad básica sobre diccionarios en Python, con explicaciones y ejemplos prácticos.

Creación de diccionarios

Podemos crear un diccionario en Python encerrando los pares clave-valor entre llaves {}. Cada par clave-valor se separa por dos puntos : y los pares se separan por comas:

# Diccionario vacío
diccionario_vacio = {}

# Diccionario de personas y sus edades
personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

# Diccionario con diferentes tipos de valores
datos = {"nombre": "Ana", "edad": 28, "es_estudiante": True}

Acceso a elementos

Podemos acceder a los valores de un diccionario utilizando sus claves. Para ello, podemos emplear la sintaxis nombre_diccionario[clave]:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

edad_juan = personas_edades["Juan"]    # 30
edad_maria = personas_edades["María"]  # 25

Si intentamos acceder a una clave que no existe en el diccionario, se generará un error. Para evitarlo, podemos usar el método get(), que permite obtener el valor de la clave o un valor predeterminado si la clave no existe:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

edad_juan = personas_edades.get("Juan")       # 30
edad_pedro = personas_edades.get("Pedro", 0)  # 0 (Pedro no está en el diccionario)

Modificación de elementos

Podemos modificar los valores de un diccionario utilizando la clave correspondiente:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

personas_edades["Juan"] = 32
personas_edades["María"] = 26

print(personas_edades)  # {"Juan": 32, "María": 26, "Carlos": 35}

Añadir elementos

Podemos añadir nuevos elementos a un diccionario simplemente asignando un nuevo par clave-valor:

personas_edades = {"Juan": 30, "María": 25}

personas_edades["Carlos"] = 35

print(personas_edades)  # {"Juan": 30, "María": 25, "Carlos": 35}

Eliminar elementos

Podemos eliminar elementos de un diccionario utilizando la palabra clave del seguida de la clave del elemento que queramos eliminar:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

del personas_edades["Juan"]

print(personas_edades)  # {"María": 25, "Carlos": 35}

Funciones y métodos disponibles para diccionarios

Los diccionarios en Python vienen acompañados con varios métodos útiles.

len()

La función len() nos permite obtener la cantidad de elementos (pares clave-valor) en el diccionario:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

cantidad_elementos = len(personas_edades)  # 3

keys(), values() e items()

Estos métodos permiten obtener las claves, los valores y los pares clave-valor del diccionario, respectivamente:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

claves = personas_edades.keys()              # dict_keys(['Juan', 'María', 'Carlos'])
valores = personas_edades.values()           # dict_values([30, 25, 35])
pares_clave_valor = personas_edades.items()  # dict_items([('Juan', 30), ('María', 25), ('Carlos', 35)])

pop()

El método pop() nos permite eliminar un elemento del diccionario y devolver su valor. También podemos proporcionar un valor predeterminado si la clave no existe en el diccionario:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

edad_carlos = personas_edades.pop("Carlos")   # 35
edad_pedro = personas_edades.pop("Pedro", 0)  # 0 (Pedro no está en el diccionario)

update()

El método update() nos permite fusionar un diccionario con otro. Si una clave existe en el diccionario original y en el diccionario que se está fusionando, el valor de la clave en el diccionario original se actualizará con el valor correspondiente del diccionario que se está fusionando. Si la clave no existe en el diccionario original, se agregará al diccionario junto con su valor:

diccionario1 = {"a": 1, "b": 2}
diccionario2 = {"b": 3, "c": 4}

diccionario1.update(diccionario2)

print(diccionario1)  # {'a': 1, 'b': 3, 'c': 4}

clear()

El método clear() nos permite eliminar todos los elementos del diccionario, dejándolo vacío:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

personas_edades.clear()

print(personas_edades)  # {}

Iteración en diccionarios

Podemos utilizar un bucle for para iterar sobre las claves o los elementos de un diccionario:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

# Iterar sobre las claves
for clave in personas_edades:
    print(clave)

# Iterar sobre los valores
for valor in personas_edades.values():
    print(valor)

# Iterar sobre los pares clave-valor
for clave, valor in personas_edades.items():
    print(f"{clave}: {valor}")

Comprobación de existencia en diccionarios

Podemos utilizar el operador in para comprobar si una clave existe en un diccionario:

personas_edades = {"Juan": 30, "María": 25, "Carlos": 35}

if "Juan" in personas_edades:
    print("La clave 'Juan' está en el diccionario.")

Diccionarios anidados

En Python, los diccionarios pueden estar anidados, es decir, podemos tener diccionarios dentro de diccionarios:

personas = {
    "Juan": {"edad": 30, "ciudad": "Madrid"},
    "María": {"edad": 25, "ciudad": "Barcelona"},
    "Carlos": {"edad": 35, "ciudad": "Sevilla"}
}

print(personas["Juan"]["edad"])     # 30
print(personas["María"]["ciudad"])  # "Barcelona"

Diccionarios y comprensión

También podemos utilizar la comprensión de diccionarios para crear diccionarios de manera muy simple. Por ejemplo, podemos crear un diccionario donde las claves son los números de una lista y los valores son el cuadrado de esos números:

numeros = [1, 2, 3, 4, 5]
diccionario_numeros = {numero: numero**2 for numero in numeros}

print(diccionario_numeros)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

Diccionarios como argumentos de funciones

Los diccionarios son útiles cuando queremos pasar un número variable de argumentos a una función, tal como veremos en la unidad específica de funciones. En el siguiente ejemplo utilizamos **datos para indicar que la función aceptará un número variable de argumentos clave-valor y luego iteramos sobre los elementos del diccionario datos:

def imprimir_datos(**datos):
    for clave, valor in datos.items():
        print(f"{clave}: {valor}")

imprimir_datos(nombre="Ana", edad=28, ciudad="Madrid")
# Salida:
# nombre: Ana
# edad: 28
# ciudad: Madrid

Programación con Python: Tuplas

Las tuplas en Python son una estructura de datos que se utiliza para almacenar colecciones de elementos, pero a diferencia de las listas, las tuplas son inmutables, lo que significa que una vez que se crean, no pueden modificarse. A continuación mostraremos la funcionalidad básica que nos proporcionan las tuplas en Python, con explicaciones y ejemplos prácticos.

Creación de tuplas

Podemos crear una tupla en Python encerrando elementos entre paréntesis ():

# Tupla vacía
tupla_vacia = ()

# Tupla de números enteros
numeros_enteros = (1, 2, 3, 4, 5)

# Tupla de números flotantes
numeros_flotantes = (1.5, 2.25, 3.75)

# Tupla de cadenas de texto
nombres = ("Ana", "Carlos", "María")

# Tupla con elementos de diferentes tipos
mixta = (10, "Python", True, 3.14)

También podemos crear una tupla sin usar paréntesis, simplemente separando los elementos por comas. Esto se conoce como «tupla implícita»:

tupla = 1, 2, 3

Acceso a elementos

Podemos acceder a los elementos de una tupla utilizando índices, al igual que con las listas. El primer elemento tiene un índice de 0 y el último de -1:

numeros = (10, 20, 30, 40, 50)

primer_elemento = numeros[0]   # 10
segundo_elemento = numeros[1]  # 20
ultimo_elemento = numeros[-1]  # 50 (índice negativo para acceder al último elemento)

Inmutabilidad de las tuplas

La principal diferencia entre las listas y las tuplas es que las tuplas son inmutables. Si necesitamos una estructura de datos que no cambie, podemos usar tuplas en lugar de listas para evitar modificaciones accidentales. Una vez que se crea una tupla, no podemos modificar sus elementos ni agregar nuevo contenido:

tupla = (1, 2, 3)

tupla[0] = 10  # Esto dará un error, ya que no se pueden modificar los elementos de una tupla

tupla.append(4)  # Esto también dará un error, ya que no se pueden agregar elementos a una tupla

Operaciones con tuplas

Longitud de la tupla

Podemos obtener la longitud de una tupla utilizando la función len():

numeros = (1, 2, 3, 4, 5)
longitud = len(numeros)  # 5

Concatenación de tuplas

Podemos concatenar dos o más tuplas utilizando el operador +:

tupla1 = (1, 2, 3)
tupla2 = (4, 5, 6)
concatenada = tupla1 + tupla2  # (1, 2, 3, 4, 5, 6)

Repetición de tuplas

Podemos repetir una tupla utilizando el operador *:

tupla = (1, 2, 3)
repetida = tupla * 3  # (1, 2, 3, 1, 2, 3, 1, 2, 3)

Búsqueda en tuplas

Podemos buscar elementos en una tupla utilizando el operador in:

numeros = (1, 2, 3, 4, 5)

existe_3 = 3 in numeros  # True
existe_6 = 6 in numeros  # False

Troceado de tuplas (slicing)

Al igual que con las listas, podemos obtener subtuplas (slices) de una tupla utilizando la sintaxis [inicio:fin]:

numeros = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

sub_tupla = numeros[2:6]  # (3, 4, 5, 6)

tupla_inversa = numeros[::-1]  # (10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

Métodos disponibles para las tuplas

Dado que las tuplas son inmutables, no tienen muchos métodos incorporados como las listas. Sin embargo, tienen dos métodos principales:

count()

La función count() nos permite contar cuántas veces aparece un elemento en una tupla:

tupla = (1, 2, 2, 3, 4, 2)
contador = tupla.count(2)  # 3 (el número 2 aparece 3 veces en la tupla)

index()

La función index() nos permite encontrar el índice de la primera aparición de un elemento en una tupla:

tupla = (10, 20, 30, 40, 50)
indice = tupla.index(30)  # 2 (el número 30 está en el índice 2 de la tupla)

Convertir tuplas en listas y viceversa

Si necesitamos modificar una tupla, podemos convertirla en una lista, realizar las modificaciones y luego convertirla de nuevo en una tupla:

tupla = (1, 2, 3)
lista = list(tupla)  # [1, 2, 3]

# Realizar modificaciones en la lista (agregar o eliminar elementos)
lista.append(4)  # [1, 2, 3, 4]

# Convertir la lista de vuelta en una tupla
tupla_modificada = tuple(lista)  # (1, 2, 3, 4)

Por otro lado, si tenemos una lista y queremos evitar modificaciones, podemos convertirla en una tupla:

lista = [1, 2, 3]
tupla = tuple(lista)  # (1, 2, 3)

Desempaquetado de tuplas

Podemos desempaquetar los elementos de una tupla en variables individuales:

tupla = (10, 20, 30)
a, b, c = tupla

print(a)  # 10
print(b)  # 20
print(c)  # 30

El desempaquetado de tuplas también se puede usar para intercambiar valores de variables de manera sencilla:

a = 10
b = 20

# Intercambiar los valores de a y b utilizando una tupla
a, b = b, a

print(a)  # 20
print(b)  # 10

Iterar sobre tuplas

Podemos utilizar un bucle for para iterar sobre los elementos de una tupla de manera similar a las listas:

numeros = (1, 2, 3, 4, 5)

for numero in numeros:
    print(numero)

Comprobación de existencia en tuplas

Al igual que con las listas, podemos utilizar el operador in para comprobar si un elemento está presente en una tupla:

numeros = (1, 2, 3, 4, 5)

if 3 in numeros:
    print("El número 3 está en la tupla.")

Tuplas y funciones

A continuación enumeramos algunas ventajas adicionales asociadas al uso de funciones y otros tipos de estructuras de datos que veremos de forma detallada en otras unidades. Hemos creído conveniente exponerlas ahora para ser conscientes del gran potencial que presentan las tuplas de Python.

Retorno de valores

Las tuplas son útiles cuando queremos devolver múltiples valores desde una función:

def dividir_y_restar(a, b):
    division = a / b
    resta = a - b
    return division, resta

resultado = dividir_y_restar(10, 3)
print(resultado)  # (3.3333333333333335, 7)

# Desempaquetar los valores de la tupla en variables separadas
division_resultado, resta_resultado = dividir_y_restar(10, 3)
print(division_resultado)  # 3.3333333333333335
print(resta_resultado)  # 7

Tuplas y funciones con argumentos variables

Las tuplas también son útiles cuando trabajamos con funciones que aceptan argumentos variables:

def suma_todos(*numeros):
    total = sum(numeros)
    return total

resultado = suma_todos(1, 2, 3, 4, 5)
print(resultado)  # 15

# También podemos pasar una tupla como argumento
tupla_numeros = (1, 2, 3, 4, 5)
resultado = suma_todos(*tupla_numeros)
print(resultado)  # 15

En este ejemplo, el operador * se utiliza para desempaquetar la tupla y pasar sus elementos como argumentos separados a la función.

Cuándo utilizar tuplas en vez de listas

Las tuplas tienen varias ventajas:

  1. Inmutabilidad: Al ser inmutables, son más seguras para almacenar datos que no deben cambiar, como fechas, coordenadas o claves de diccionarios.
  2. Claves de diccionarios: Las tuplas son «hashables», lo que significa que pueden usarse como claves en diccionarios. Las listas, al ser mutables, no son «hashables» y no se pueden utilizar como claves.
  3. Rendimiento: Las tuplas son ligeramente más rápidas que las listas debido a su inmutabilidad. Si tienes un conjunto de datos que no cambiará, usar tuplas puede ser más eficiente.
  4. Empaquetado y desempaquetado: Las tuplas permiten un empaquetado y desempaquetado sencillo de elementos.
  5. Comparación: Las tuplas se pueden comparar directamente, lo que facilita su uso para ordenar o realizar comparaciones.

Tuplas como claves de diccionarios

Hablaremos de los diccionarios en otra unidad, pero queremos anticipar ahora la justificación del uso de tuplas para acceder a nuestros datos. Como hemos mencionado, a diferencia de las listas, las tuplas se pueden utilizar como claves en diccionarios debido a su inmutabilidad. Por ello, cuando necesitamos una clave compuesta por múltiples elementos, las tuplas son la opción más adecuada:

diccionario = {("Juan", 25): "Estudiante", ("María", 30): "Profesora"}

# Acceder al valor utilizando una tupla como clave
valor = diccionario[("Juan", 25)]  # "Estudiante"

Programación con Python: Listas

Las listas en Python son una estructura de datos muy versátil que permite almacenar y manipular colecciones de elementos. Son secuencias ordenadas y modificables, y pueden contener elementos de diferentes tipos. A continuación mostraremos las principales operaciones que podemos realizar con listas en Python, e incluiremos explicaciones y ejemplos prácticos.

Creación de listas

Podemos crear listas en Python encerrando los elementos entre corchetes []:

# Lista vacía
lista_vacia = []

# Lista de números enteros
numeros_enteros = [1, 2, 3, 4, 5]

# Lista de números flotantes
numeros_flotantes = [1.5, 2.25, 3.75]

# Lista de cadenas de texto
nombres = ["Ana", "Carlos", "María"]

# Lista con elementos de diferentes tipos
mixta = [10, "Python", True, 3.14]

Acceso a elementos

Podemos acceder a los elementos de una lista utilizando índices, teniendo en cuenta que el primer elemento tiene un índice de 0, y el último de -1:

numeros = [10, 20, 30, 40, 50]

primer_elemento = numeros[0]   # 10
segundo_elemento = numeros[1]  # 20
ultimo_elemento = numeros[-1]  # 50 (índice negativo para acceder al último elemento)

Modificación de elementos

Podemos modificar los elementos de una lista asignando nuevos valores a través de sus índices:

nombres = ["Ana", "Carlos", "María"]

nombres[1] = "Juan"  # Modificar el segundo elemento de la lista

print(nombres)  # ["Ana", "Juan", "María"]

Operaciones con listas

Longitud de la lista

Podemos obtener la longitud de una lista utilizando la función len():

numeros = [1, 2, 3, 4, 5]
longitud = len(numeros)  # 5

Concatenación de listas

Podemos concatenar dos o más listas utilizando el operador +:

lista1 = [1, 2, 3]
lista2 = [4, 5, 6]
concatenada = lista1 + lista2  # [1, 2, 3, 4, 5, 6]

Repetición de listas

Podemos repetir una lista utilizando el operador *:

lista = [1, 2, 3]
repetida = lista * 3  # [1, 2, 3, 1, 2, 3, 1, 2, 3]

Añadir elementos a una lista

Podemos añadir elementos a una lista utilizando los métodos append() e insert():

frutas = ["manzana", "plátano"]

# Método append(): añade un elemento al final de la lista
frutas.append("naranja")  # ["manzana", "plátano", "naranja"]

# Método insert(): inserta un elemento en una posición específica de la lista
frutas.insert(1, "pera")  # ["manzana", "pera", "plátano", "naranja"]

Eliminar elementos de una lista

Podemos eliminar elementos de una lista utilizando los métodos remove() y pop(), y también mediante una sentencia del:

nombres = ["Ana", "Carlos", "María"]

# Método remove(): elimina el primer elemento que coincida con el valor proporcionado
nombres.remove("Carlos")  # ["Ana", "María"]

# Método pop(): elimina el elemento en la posición dada (o el último si no se proporciona índice)
nombres.pop(0)  # ["María"]

# Sentencia del: elimina el elemento en la posición dada (o toda la lista si no se proporciona índice)
del nombres[0]  # []

Búsqueda en listas

Podemos buscar elementos en una lista utilizando el operador in:

numeros = [1, 2, 3, 4, 5]

existe_3 = 3 in numeros  # True
existe_6 = 6 in numeros  # False

Comprobar si una lista está vacía

Podemos comprobar si una lista está vacía simplemente usando el valor booleano de la lista. En Python, una lista vacía se evalúa como False, mientras que una lista con elementos se evalúa como True:

lista_vacia = []
lista_no_vacia = [1, 2, 3]

if not lista_vacia:
    print("Esta lista está vacía.")

if lista_no_vacia:
    print("Esta lista contiene elementos.")

Comprobar si un elemento existe en una lista

Podemos utilizar el operador in para comprobar si un elemento está presente en una lista:

numeros = [1, 2, 3, 4, 5]

if 3 in numeros:
    print("El número 3 está en la lista.")

Comparación de listas

Podemos comparar listas para verificar si son iguales o diferentes:

lista1 = [1, 2, 3]
lista2 = [1, 2, 3]
lista3 = [4, 5, 6]

print(lista1 == lista2)  # True (misma estructura y elementos)
print(lista1 == lista3)  # False (elementos diferentes)

Troceado de listas (slicing)

Podemos obtener sublistas (slices) de una lista utilizando la sintaxis [inicio:fin] teniendo en cuenta que el índice de inicio sí está incluido, pero el de fin no:

numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

print(numeros[2:6])  # [3, 4, 5, 6]
print(numeros[2:])   # [3, 4, 5, 6, 7, 8, 9, 10]
print(numeros[:-2])  # [1, 2, 3, 4, 5, 6, 7, 8]

También disponemos de funcionalidades más avanzadas, como el paso o la asignación de valores con la sintaxis [inicio:fin:paso]:

numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Obtener una sublista con paso negativo (se recorre en orden inverso)
sub_lista_inversa = numeros[::-1]  # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

# Obtener una sublista con paso positivo (obtener los números pares)
pares = numeros[1::2]  # [2, 4, 6, 8, 10]

# Modificar valores usando slicing
numeros[1:4] = [20, 30, 40]  # [1, 20, 30, 40, 5, 6, 7, 8, 9, 10]

# Eliminar elementos usando slicing (reemplazar con una lista vacía)
numeros[1:4] = []  # [1, 5, 6, 7, 8, 9, 10]

Listas anidadas

En Python podemos tener listas dentro de listas, lo que se conoce como listas anidadas:

lista_anidada = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

print(lista_anidada[1][2])  # 6 (tercer elemento de la segunda lista)

Copia de listas

Debemos tener en cuenta que al asignar una lista a otra variable, ambas variables apuntan a la misma lista en memoria. Si modificamos una de ellas, la otra también cambiará. Para crear una copia independiente de una lista, podemos utilizar el método copy() o la notación de slicing:

original = [1, 2, 3]

# Copia utilizando copy()
copia1 = original.copy()

# Copia utilizando slicing
copia2 = original[:]

# Modificar la copia independiente
copia1[0] = 100

print(original)  # [1, 2, 3]
print(copia1)    # [100, 2, 3]
print(copia2)    # [1, 2, 3]

Como se puede observar en el ejemplo, la modificación de copia1 no afecta a la lista original, ya que son listas diferentes en memoria. Sin embargo, debemos tener en cuenta que si la lista contiene objetos mutables (como listas anidadas o diccionarios), las copias independientes todavía pueden compartir referencias a esos objetos internos. Por lo tanto, si modificamos un objeto mutable dentro de una lista copiada, ese cambio se reflejará en ambas listas.

Listas y bucles

Las listas son especialmente útiles cuando se combinan con bucles para procesar múltiples elementos de manera eficiente.

Bucle «for» con listas

Este bucle for recorre la lista numeros y muestra cada elemento en la consola:

numeros = [1, 2, 3, 4, 5]

for numero in numeros:
    print(numero)

Bucle «for» con índices y enumeración

A veces resulta útil acceder tanto al índice como al elemento en cada iteración. Podemos usar la función enumerate() para obtener ambos:

frutas = ["manzana", "plátano", "naranja"]

for indice, fruta in enumerate(frutas):
    print(f"Índice: {indice}, Fruta: {fruta}")

Bucle «while» con listas

El bucle while se puede usar para recorrer una lista hasta que se cumpla una condición:

numeros = [1, 2, 3, 4, 5]
indice = 0

while indice < len(numeros):
    print(numeros[indice])
    indice += 1

Comprensión de listas

Las comprensiones de listas nos proporcionan una forma muy concisa y legible para crear listas basadas en una expresión y un bucle for:

# Crear una lista de cuadrados de los números del 1 al 5
cuadrados = [numero ** 2 for numero in range(1, 6)]  # [1, 4, 9, 16, 25]

# Crear una lista solo con números pares del 1 al 10
pares = [numero for numero in range(1, 11) if numero % 2 == 0]  # [2, 4, 6, 8, 10]

También podemos utilizar comprensión avanzada para realizar tareas más complejas:

# Obtener todos los números impares mayores que 10 en una lista
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
impares_mayores_que_10 = [numero for numero in numeros if numero > 10 and numero % 2 != 0]

# Crear una matriz de ceros de 3x3 utilizando comprensión de listas anidadas
matriz_ceros = [[0 for _ in range(3)] for _ in range(3)]

Funciones y métodos útiles para listas

Python proporciona diversas funciones y métodos que pueden resultar muy útiles para trabajar con listas:

numeros = [3, 1, 5, 2, 4]

# Ordenar la lista de forma ascendente
numeros.sort()  # [1, 2, 3, 4, 5]

# Ordenar la lista de forma descendente
numeros.sort(reverse=True)  # [5, 4, 3, 2, 1]

# Obtener el índice de la primera aparición de un elemento
indice = numeros.index(4)  # 1

# Contar el número de veces que aparece un elemento en la lista
apariciones = numeros.count(3)  # 1

# Obtener una copia ordenada de la lista sin modificar la original
lista_ordenada = sorted(numeros)  # [1, 2, 3, 4, 5]

# Limpiar la lista (eliminar todos los elementos)
numeros.clear()  # []

Función map()

La función map() se puede utilizar para aplicar una función a todos los elementos de una lista y devolver una nueva lista con los resultados:

def cuadrado(numero):
    return numero ** 2

numeros = [1, 2, 3, 4, 5]
cuadrados = list(map(cuadrado, numeros))  # [1, 4, 9, 16, 25]

También podemos utilizar comprensión de listas para lograr el mismo resultado de manera más concisa:

numeros = [1, 2, 3, 4, 5]
cuadrados = [numero ** 2 for numero in numeros]  # [1, 4, 9, 16, 25]

Función filter()

La función filter() se puede utilizar para filtrar elementos de una lista según una condición dada:

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

numeros = [1, 2, 3, 4, 5, 6]
pares = list(filter(es_par, numeros))  # [2, 4, 6]

Nuevamente, podemos utilizar comprensión de listas para lograr el mismo resultado:

numeros = [1, 2, 3, 4, 5, 6]
pares = [numero for numero in numeros if numero % 2 == 0]  # [2, 4, 6]

Función sum()

La función sum() nos permite sumar todos los elementos de una lista numérica:

numeros = [1, 2, 3, 4, 5]
total = sum(numeros)  # 15

Función join()

La función join() nos permite unir elementos de una lista para formar una cadena de texto:

nombres = ["Ana", "Carlos", "María"]
cadena_unida = ", ".join(nombres)  # "Ana, Carlos, María"

Función zip()

La función zip() combina elementos de dos o más listas en tuplas:

nombres = ["Ana", "Carlos", "María"]
edades = [25, 30, 28]

combinado = list(zip(nombres, edades))
# [('Ana', 25), ('Carlos', 30), ('María', 28)]

Función sorted()

La función sorted() nos permite obtener una nueva lista ordenada a partir de una lista dada sin modificar la original:

numeros = [3, 1, 5, 2, 4]
numeros_ordenados = sorted(numeros)  # [1, 2, 3, 4, 5]

Función reversed()

La función reversed() nos permite obtener una nueva lista con sus elementos en orden inverso sin modificar la original:

numeros = [1, 2, 3, 4, 5]

for numero in reversed(numeros):
    print(numero) # [5, 4, 3, 2, 1]

for numero in numeros:
    print(numero) # [1, 2, 3, 4, 5]

Función enumerate()

La función enumerate() nos permite obtener tanto el índice como el elemento durante la iteración:

frutas = ["manzana", "plátano", "naranja"]

for indice, fruta in enumerate(frutas):
    print(f"Índice: {indice}, Fruta: {fruta}")

Método count()

El método count() nos permite contar cuántas veces aparece un elemento específico en una lista:

numeros = [1, 2, 2, 3, 4, 2]
contador = numeros.count(2)  # 3 (el número 2 aparece 3 veces en la lista)

Método index()

El método index() nos permite encontrar el índice de la primera aparición de un elemento en una lista:

numeros = [10, 20, 30, 40, 50]
indice = numeros.index(30)  # 2 (el número 30 está en el índice 2 de la lista)

Método clear()

El método clear() se utiliza para eliminar todos los elementos de una lista, dejándola vacía:

numeros = [1, 2, 3, 4, 5]
numeros.clear()  # []

Listas como pilas y colas

Podemos usar una lista para implementar una pila (LIFO) o una cola (FIFO):

# Implementación de una pila usando una lista
pila = []
pila.append(1)         # [1]
pila.append(2)         # [1, 2]
elemento = pila.pop()  # elemento = 2, pila = [1]

# Implementación de una cola usando una lista (menos eficiente)
cola = []
cola.append(1)          # [1]
cola.append(2)          # [1, 2]
elemento = cola.pop(0)  # elemento = 1, cola = [2]

Listas y mutabilidad

Las listas son mutables, lo que significa que podemos modificar sus elementos sin crear una nueva lista. Por ejemplo, podemos agregar, eliminar o actualizar elementos en una lista sin tener que crear una copia:

numeros = [1, 2, 3]
numeros[0] = 10  # [10, 2, 3]

numeros.append(4)  # Agregar un elemento al final: [10, 2, 3, 4]
numeros.insert(1, 5)  # Insertar un elemento en una posición: [10, 5, 2, 3, 4]

numeros.remove(2)  # Eliminar el elemento 2: [10, 5, 3, 4]
numeros.pop()  # Eliminar el último elemento: [10, 5, 3]

del numeros[1]  # Eliminar un elemento por índice: [10, 3]

numeros.extend([6, 7, 8])  # Extender la lista con otra: [10, 3, 6, 7, 8]

Como se observa en el ejemplo, podemos modificar los elementos de una lista directamente y realizar varias operaciones para agregar o eliminar elementos según sea necesario.

Clonación de listas

Si queremos crear una copia independiente de una lista, debemos asegurarnos de no hacer una asignación directa, ya que ambas variables apuntarían a la misma lista en memoria. Podemos utilizar el método copy() o la notación de slicing para clonar una lista:

original = [1, 2, 3]

# Clonar usando copy()
copia1 = original.copy()

# Clonar usando slicing
copia2 = original[:]

Ahora, las tres listas son independientes y cualquier modificación en una lista no afectará a las otras.

Conversiones de datos básicos a listas

A continuación veremos cómo realizar conversiones de diferentes tipos de datos básicos (cadenas y enteros por ejemplo) a listas en Python. En general, podremos convertir cualquier secuencia o estructura iterable en una lista utilizando la función list().

Conversión de cadenas de texto a lista

Podemos convertir una cadena de texto en una lista utilizando el método split(). Este método separa la cadena en subcadenas usando un delimitador específico y devuelve una lista con las subcadenas:

cadena = "Hola,esto,es,una,cadena"
lista = cadena.split(",")  # ['Hola', 'esto', 'es', 'una', 'cadena']

El delimitador que hemos utilizado en el ejemplo es la coma (,), pero podemos usar cualquier carácter como delimitador:

otra_cadena = "Esto-es-una-cadena-con-guiones"
otra_lista = otra_cadena.split("-")  # ['Esto', 'es', 'una', 'cadena', 'con', 'guiones']

Si no especificamos un delimitador en split(), se utilizará el espacio en blanco como delimitador por defecto:

texto = "Hola esto es una cadena"
palabras = texto.split()  # ['Hola', 'esto', 'es', 'una', 'cadena']

Conversión de caracteres de una cadena a lista

Si tenemos una cadena de texto y queremos utilizar cada carácter para formar una lista, podemos hacerlo utilizando la función list() o también con una comprensión de listas. En el siguiente ejemplo, iteramos por cada carácter en la cadena y creamos una lista con cada carácter como elemento:

cadena = "Hola"
lista1 = list(cadena)  # ['H', 'o', 'l', 'a']
lista2 = [caracter for caracter in cadena]  # ['H', 'o', 'l', 'a']

Conversión de valores numéricos a lista

Si queremos convertir un valor numérico en una lista, podemos hacerlo utilizando la función list() pasando el valor como un argumento iterable. En el siguiente ejemplo convertimos el número en una cadena de texto utilizando str(), y luego utilizamos list() para formar una lista con todos los dígitos:

numero = 12345
lista = list(str(numero))  # ['1', '2', '3', '4', '5']

Conversión de rangos a lista

Los rangos en Python también se pueden convertir a listas utilizando la función list(). Por ejemplo, podemos crear un rango del 1 al 5 y luego convertirlo en una lista:

rango = range(1, 6)
lista = list(rango)  # [1, 2, 3, 4, 5]

Listas, funciones y conjuntos

A continuación enumeramos algunas ventajas adicionales asociadas al uso de funciones y otros tipos de estructuras de datos que veremos de forma detallada en otras unidades. Hemos creído conveniente exponerlas ahora para ser conscientes del gran potencial que presentan las listas de Python.

Listas como argumentos de funciones

Las listas se pueden utilizar como argumentos de funciones, lo que nos proporciona la funcionalidad necesaria para pasar múltiples elementos a una función agrupándolos en una sola variable. Además, esto nos permitirá por ejemplo realizar cambios en los elementos de la lista dentro de la función:

def duplicar_elementos(lista):
    for i in range(len(lista)):
        lista[i] *= 2

numeros = [1, 2, 3, 4, 5]
duplicar_elementos(numeros)

print(numeros)  # [2, 4, 6, 8, 10]

Listas y funciones lambda

Las funciones lambda son funciones anónimas que se pueden usar en combinación con las funciones map(), filter(), y otras:

numeros = [1, 2, 3, 4, 5]

# Elevar al cuadrado cada número usando una función lambda con map()
cuadrados = list(map(lambda x: x ** 2, numeros))  # [1, 4, 9, 16, 25]

# Filtrar solo los números pares usando una función lambda con filter()
pares = list(filter(lambda x: x % 2 == 0, numeros))  # [2, 4]

Listas y operaciones con conjuntos

Podemos realizar operaciones de conjuntos (unión, intersección, diferencia) con listas utilizando el tipo de dato set:

lista1 = [1, 2, 3]
lista2 = [3, 4, 5]

# Unión de listas
union = list(set(lista1) | set(lista2))  # [1, 2, 3, 4, 5]

# Intersección de listas
interseccion = list(set(lista1) & set(lista2))  # [3]

# Diferencia entre listas (elementos que están en lista1 pero no en lista2)
diferencia = list(set(lista1) - set(lista2))  # [1, 2]

Debemos matizar que el tipo de dato set no mantiene el orden original de los elementos, por lo que al convertirlo de vuelta a una lista, el orden puede variar.

Programación con Python: Bucles

Los bucles son estructuras de control que nos permiten repetir un bloque de código varias veces o recorrer elementos en una secuencia. En Python disponemos del bucle for y el bucle while. A continuación mostraremos cómo utilizarlos con ejemplos para comprender mejor su funcionamiento.

Bucle «for»

El bucle for se utiliza para iterar sobre secuencias de datos tales como cadenas, listas, tuplas, o diccionarios (estos tres últimos tipos de datos se explican con más detalle en otra unidad).

En el siguiente ejemplo, el bucle for recorre la cadena mensaje y muestra en la consola cada uno de sus caracteres:

# Ejemplo con una cadena
mensaje = "Hola, Python!"
for caracter in mensaje:
    print(caracter)

En el siguiente ejemplo empleamos un bucle for para recorrer la lista frutas y mostrar cada uno de sus elementos:

# Ejemplo con una lista
frutas = ["manzana", "plátano", "naranja"]
for fruta in frutas:
    print(fruta)

A continuación mostramos un bucle for que recorre cada par clave-valor del diccionario estudiantes y muestra el nombre y la edad de cada estudiante:

# Ejemplo con un diccionario
estudiantes = {"Juan": 18, "María": 20, "Carlos": 22}
for nombre, edad in estudiantes.items():
    print(nombre, "tiene", edad, "años.")

Bucle «while»

El bucle while se utiliza para repetir un bloque de código mientras una condición sea verdadera. En el siguiente ejemplo, el bucle while muestra el valor del contador mientras sea menor o igual a 5:

contador = 1
while contador <= 5:
    print("Contador:", contador)
    contador += 1

Sentencias de control en bucles

Dentro de los bucles podemos utilizar las sentencias break, continue y else para controlar el flujo de ejecución.

break

Permite salir del bucle de manera prematura si se cumple una condición. En el siguiente ejemplo, el bucle for imprimirá los números del 1 al 4 y luego se detendrá cuando llegue a 5:

for numero in range(1, 11):
    if numero == 5:
        break
    print(numero)

continue

Permite saltar a la siguiente iteración del bucle si se cumple una condición. En el siguiente ejemplo, el bucle for imprimirá solo los números impares del 1 al 10, saltando los números pares:

for numero in range(1, 11):
    if numero % 2 == 0:
        continue
    print(numero)

else

Se ejecuta cuando el bucle se completa sin encontrar una sentencia break. Por ejemplo, el siguiente bucle for imprimirá los números del 1 al 5 y luego el programa mostrará «Fin del bucle»:

for numero in range(1, 6):
    print(numero)
else:
    print("Fin del bucle")

Bucles anidados

Podemos incluir bucles dentro de bucles, lo que se conoce como bucles anidados. También podemos utilizar break y continue para controlar el flujo de ejecución dentro de ellos. Por ejemplo, los siguientes bucles for anidados imprimirán todos los pares (i, j) del 1 al 3, excepto cuando i sea igual a 2 y j sea igual a 2. En este último caso, el break provocará la salida del bucle interno, continuando con la siguiente iteración del bucle externo:

for i in range(1, 4):
    for j in range(1, 4):
        if i == 2 and j == 2:
            break
        print(i, j)

Bucles infinitos

Es importante tener cuidado con los bucles infinitos, ya que pueden llegar a ejecutar el código que contengan de forma indefinida si no incluimos correctamente una condición que los detenga. Pueden provocar que el programa se bloquee o entre en un estado no deseado. Por ejemplo, el siguiente bucle while se ejecutará de forma indefinida, imprimiendo «Este es un bucle infinito» una y otra vez:

# Bucle infinito con while
while True:
    print("Este es un bucle infinito")

Programación con Python: Condicionales

Los condicionales son una parte fundamental de la programación que nos permite controlar el flujo de ejecución de un programa. En Python se utilizan principalmente tres tipos de condicionales: if, elif (opcional) y else. A continuación mostraremos cómo utilizarlos con ejemplos para comprender mejor su funcionamiento.

Condicionales «if»

El condicional if se utiliza para ejecutar un bloque de código solo si una condición es verdadera. En el siguiente ejemplo, si la variable edad es mayor o igual a 18, se imprimirá «Eres mayor de edad.»:

edad = 18

if edad >= 18:
    print("Eres mayor de edad.")

Condicionales «if» y «else»

El condicional else se utiliza para ejecutar un bloque de código cuando la condición del if es falsa. En el siguiente ejemplo, si la variable edad es mayor o igual a 18, se imprimirá «Eres mayor de edad.», y en caso contrario, se imprimirá «Eres menor de edad.»:

edad = 15

if edad >= 18:
    print("Eres mayor de edad.")
else:
    print("Eres menor de edad.")

Condicionales «if», «elif» y «else»

El condicional elif (abreviatura de «else if») se utiliza para evaluar múltiples condiciones. Se verifica solo si la condición del if anterior es falsa. En el siguiente ejemplo, el programa evalúa la variable calificacion y muestra diferentes mensajes dependiendo del rango en el que se encuentre:

calificacion = 85

if calificacion >= 90:
    print("Tienes una A.")
elif calificacion >= 80:
    print("Tienes una B.")
elif calificacion >= 70:
    print("Tienes una C.")
else:
    print("Tienes una calificación inferior a C.")

Uso de operadores lógicos en condicionales

Podemos combinar múltiples condiciones utilizando los operadores lógicos and (y), or (o) y not (no). En el siguiente ejemplo se evalúa tanto la temperatura como si es un día soleado o no, utilizando el operador and. Si ambas condiciones son verdaderas, se imprimirá «Hace calor y es un día soleado.» Si al menos una de las condiciones es verdadera (usando el operador or), se imprimirá «Hace calor o es un día soleado.» De lo contrario, se imprimirá «No hace calor y no es un día soleado.»:

temperatura = 25
dia_soleado = True

if temperatura > 30 and dia_soleado:
    print("Hace calor y es un día soleado.")
elif temperatura > 30 or dia_soleado:
    print("Hace calor o es un día soleado.")
else:
    print("No hace calor y no es un día soleado.")

Condicionales anidados

Se pueden anidar condicionales dentro de otros condicionales para manejar situaciones más complejas. En el siguiente ejemplo, se verifica primero si la persona es mayor o igual a 18 años. Si es así, se verifica si tiene carnet de conducir para determinar si puede conducir o no:

edad = 25
carnet_conducir = True

if edad >= 18:
    if carnet_conducir:
        print("Puedes conducir.")
    else:
        print("Eres mayor de edad, pero no puedes conducir.")
else:
    print("Eres menor de edad.")

Sentencia match-case

Antes de Python 3.10, las comparaciones múltiples se realizaban utilizando la estructura if-elif-else, lo que podía llevar a una sintaxis un tanto repetitiva y menos legible en ciertos casos. El nuevo match case resuelve este problema ya que permite realizar comparaciones más elegantes y fáciles de leer.

Sintaxis básica

La sintaxis del match case se asemeja a una declaración switch en otros lenguajes de programación. Su forma básica es la siguiente:

match valor:
    case patron1:
        # código a ejecutar si el valor coincide con patron1
    case patron2:
        # código a ejecutar si el valor coincide con patron2
    ...
    case patronN:
        # código a ejecutar si el valor coincide con patronN
    case _:
        # código a ejecutar si no se cumple ninguno de los patrones anteriores

Un ejemplo muy sencillo

En su forma más sencilla, el match case permite comprobar coincidencias con valores específicos de cualquier tipo (números, cadenas, etc). Por ejemplo, utilizando el código del siguiente ejemplo podemos determinar el estado civil de una persona basándonos en el valor de la variable codigo:

match codigo:
    case "S":
        print("Soltero/a")
    case "C":
        print("Casado/a")
    case "D":
        print("Divorciado/a")
    case "V":
        print("Viudo/a")
    case _:
        print("Código inválido")

Equivalencia entre «if-elif-else» y «match-case»

Supongamos que queremos determinar el día de la semana correspondiente a un número del 1 al 7, donde 1 representa el lunes y 7 representa el domingo.

Usando if-elif-else:

if numero == 1:
    print("Lunes")
elif numero == 2:
    print("Martes")
elif numero == 3:
    print("Miércoles")
elif numero == 4:
    print("Jueves")
elif numero == 5:
    print("Viernes")
elif numero == 6:
    print("Sábado")
elif numero == 7:
    print("Domingo")
else:
    print("Número inválido")

Usando match case:

match numero:
    case 1:
        print("Lunes")
    case 2:
        print("Martes")
    case 3:
        print("Miércoles")
    case 4:
        print("Jueves")
    case 5:
        print("Viernes")
    case 6:
        print("Sábado")
    case 7:
        print("Domingo")
    case _:
        print("Número inválido")

Como se puede apreciar, el uso del match case reduce significativamente la cantidad de consultas que se realizan para comprobar el valor de la variable numero y hace que el código resultante sea más fácil de leer.

Combinación de patrones

El match case también permite combinar múltiples patrones utilizando la sintaxis case patron1 | patron2 | ...:. En el siguiente ejemplo agrupamos diversos valores de una nota numérica con el propósito de mostrar el texto descriptivo de la calificación que se haya obtenido:

match nota:
    case 0 | 1 | 2 | 3 | 4:
        print("Suspendido")
    case 5 | 6:
        print("Aprobado")
    case 7 | 8:
        print("Notable")
    case 9 | 10:
        print("Sobresaliente")
    case _:
        print("Nota no válida")

Además Python nos permite comprobar coincidencias con datos de cualquier tipo (cadenas, listas, tuplas, diccionarios, etc). Vamos a ver un ejemplo en el que tenemos una lista de alimentos representados por cadenas de texto. Utilizamos el match case para clasificar cada alimento en diferentes categorías: «Frutas», «Verduras», «Proteínas» o «Carbohidratos». Si el alimento no coincide con ninguna de estas categorías, lo clasificamos como «Desconocido». De esta forma, el match case nos permitirá clasificar rápidamente cada alimento en su respectiva categoría sin tener que escribir múltiples sentencias if-elif-else, y por lo tanto obtenemos un código más simple y fácil de entender:

match alimento:
    case "manzana" | "pera" | "plátano" | "uva":
        print("Frutas")
    case "zanahoria" | "tomate" | "espinaca" | "lechuga":
        print("Verduras")
    case "pollo" | "pescado" | "res" | "cerdo":
        print("Proteínas")
    case "arroz" | "pasta" | "pan" | "patata":
        print("Carbohidratos")
    case _:
        print("Desconocido")

Consideraciones adicionales

Es importante tener en cuenta que el match case es una característica introducida en Python 3.10, por lo que si estamos utilizando una versión anterior, esta funcionalidad no estará disponible. Además, es recomendable utilizar el match case sólo cuando el código resulte más legible o apropiado para el problema en cuestión. En muchos casos, la estructura if-elif-else tradicional puede ser resultar más adecuada por motivos de compatibilidad o en caso de que podamos resolver nuestro problema utilizando expresiones booleanas sencillas.