class: center, middle, inverse, title-slide .title[ # Python II y GitHub ] .institute[ ### Licenciatura en Ciencias Genómicas,UNAM ] .date[ ### First version: yyy-mm-dd; Last update: 2025-04-25 ] --- <style type="text/css"> /* From https://github.com/yihui/xaringan/issues/147 */ .scroll-output { height: 80%; overflow-y: scroll; } /* https://stackoverflow.com/questions/50919104/horizontally-scrollable-output-on-xaringan-slides */ pre { max-width: 100%; overflow-x: scroll; } </style> # 🎯 Objetivo Comprender el concepto, la utilidad y la sintaxis de las funciones en Python, aplicando buenas prácticas de programación. <img src="https://realpython.com/cdn-cgi/image/width=1920,format=auto/https://files.realpython.com/media/Pythons-Math-Module-Guide_Watermarked.c882e267cbd0.jpg" width="600px" style="display: block; margin: auto;" /> --- ## 🔹 ¿Por qué usar funciones en programación? Cuando escribimos un programa, muchas veces necesitamos **repetir ciertas operaciones** u **organizar el código** para que sea más claro y fácil de mantener. Ahí es donde entran las **funciones**. --- # 🧠 ¿Qué es una función? Una **función** es un bloque de código reutilizable que recibe entradas (parámetros), ejecuta instrucciones, y puede retornar un valor. Nos permite: - **Evitar la repetición** de código (principio DRY: _Don't Repeat Yourself_). - **Dividir un problema complejo** en partes pequeñas, más fáciles de entender. - **Reutilizar** código en distintas partes del programa sin copiar/pegar. - **Hacer pruebas más fácilmente**, porque cada función se puede probar por separado. --- ## 🌟 ¿Qué pasa si no usamos funciones? Estás trabajando con secuencias de ADN y necesitas calcular el contenido GC de **varias secuencias** obtenidas de diferentes genes. ``` python seq1 = "ATGCGCGTAAAGC" gc1 = (seq1.count('G') + seq1.count('C')) / len(seq1) * 100 seq2 = "TTATGGCCATAT" gc2 = (seq2.count('G') + seq2.count('C')) / len(seq2) * 100 seq3 = "GGCCGCGCGCGG" gc3 = (seq3.count('G') + seq3.count('C')) / len(seq3) * 100 print(gc1, gc2, gc3) ``` 🧨 Este código: .tiny[ - Repite la misma lógica varias veces. - Es difícil de modificar (imagina que ahora quieres redondear o validar...). - Es propenso a errores si cambias una línea pero no las demás. ] --- ## ✅ Con función: ``` python def contenido_gc(seq): return (seq.count('G') + seq.count('C')) / len(seq) * 100 secuencias = [ "ATGCGCGTAAAGC", "TTATGGCCATAT", "GGCCGCGCGCGG" ] for i, seq in enumerate(secuencias, start=1): print(f"Secuencia {i} → GC%: {contenido_gc(seq):.2f}") ``` 🧠 Ventajas claras: - Reutilización del código. - Mejor lectura. - Más fácil de **modificar o mejorar** (por ejemplo, agregar validación). - Lista para usar en otros proyectos o pruebas. --- ### 🎓 Lección > "Con funciones, puedes resolver un problema **una sola vez** y usar esa solución tantas veces como necesites. Así piensan los programadores profesionales: resolver inteligentemente, no repetir." --- # 🧩 Sintaxis de una función ```python def nombre_de_funcion(parámetros): # bloque de código return valor ``` 🧱 Partes de una función .tiny[ 1. **`def`** Palabra clave para **definir una función**. 2. **Nombre de la función** Debe ser **descriptivo** (por ejemplo: `calcular_promedio`, `limpiar_secuencia`), siguiendo la convención `snake_case`. 3. **Paréntesis `()`** Dentro van los **parámetros de entrada**, que son opcionales si no se necesitan. 4. **Dos puntos `:`** Indica el comienzo del **bloque de código** que pertenece a la función. 5. **Bloque de código** Todas las líneas dentro deben estar **indentadas** con 4 espacios (PEP8). 6. **Sentencia `return` (opcional)** Especifica el **valor que devuelve** la función. Si no se indica `return`, la función devuelve `None` por defecto. ] --- # ✍️ Ejemplo básico :Saludo Una función puede definirse **sin parámetros** y **sin valor de retorno**. Solo ejecuta una acción cuando es llamada. ``` python # Definimos una función sin parámetros ni retorno def di_hola(): print("¡Hola!") # Llamamos a la función: imprime el mensaje en la consola di_hola() ``` -- .tiny[ ### 📌 Explicación: - `def di_hola():` → define una función llamada `di_hola`, que **no recibe datos**. - Dentro, ejecuta `print("¡Hola!")`. - Al llamar `di_hola()`, se imprime el mensaje. - **No hay `return`**, así que el valor devuelto es `None` (implícitamente). ] --- ## ✍ Función con un parámetro Una función puede recibir **información de entrada** (llamada _parámetro_) y usarla para personalizar su comportamiento. ``` python # Definimos una función que recibe un nombre como parámetro def saludar_con_nombre(nombre): print(f"¡Hola, {nombre}!") # Llamamos a la función pasando un argumento (una cadena) saludar_con_nombre("AnaSofi") ``` --- ## ✍ Función con múltiples parámetros y valor de retorno Una función puede aceptar **más de un parámetro** y retornar un valor, que luego puede usarse o imprimirse. ``` python # Definimos una función que multiplica dos valores def multiplica(valor1, valor2): return valor1 * valor2 # Llamamos a la función con dos argumentos resultado = multiplica(2, 3) # Imprimimos el resultado: 6 print(resultado) ``` .tiny[ 📌 Explicación: - `valor1` y `valor2` son **parámetros** definidos en la función. - `multiplica(2, 3)` pasa dos **argumentos**: `2` y `3`. - La función retorna el producto de esos valores con `return`. ] --- ## 🧩 Algunos conceptos clave Cuando trabajamos con funciones, es importante diferenciar entre: ### 🔹 Parámetro Es una **variable definida en la función**. Es como un espacio reservado que recibirá el valor al momento de llamar la función. ### 🔹 Argumento Es el **valor real** que se pasa a la función cuando la invocamos. Es el dato concreto que sustituye al parámetro. --- # 📌 Ejemplo ``` python def multiplica(valor1, valor2): return valor1 * valor2 ``` - Aquí, `a` y `b` son **parámetros**. - Son variables **internas a la función** que se usarán para multiplicar. ``` python print(multiplica(4, 5)) # 20 ``` - Aquí, `4` y `5` son **argumentos**. - Son los **valores reales** que se pasan a la función. --- ## 🧠 ¡Vamos a transformar este código en una función! Tenemos este código para contar el contenido de `A` y `T` en una secuencia de ADN: ``` python my_dna = "ACTGATCGATTACGTATAGTATTTGCTATCATACATATATATCGATGCGTTCAT" length = len(my_dna) a_count = my_dna.count('A') t_count = my_dna.count('T') at_content = (a_count + t_count) / length print("AT content is " + str(at_content)) ``` .tiny[ Antes de convertirlo en función, piensa en: - 🔹 ¿Cuál es la **entrada**? - 🔹 ¿Cuál es la **salida**? - 🔹 ¿Qué parte del código se **puede generalizar o reutilizar**? ] --- ### 🤔 Preguntas para realzar la transformación: 1. ✅ ¿Qué dato debería poder cambiar sin reescribir el código? - 👉 **¿Y si quiero calcular el contenido AT de otra secuencia?** 2. ✅ ¿Qué parte se repite si quiero hacer esto muchas veces? - 👉 **¿Y si quiero analizar 10 secuencias diferentes?** 3. ✅ ¿Qué parte es fija y qué parte podría ir como parámetro? - 👉 **¿Qué cambiarías si quisieras usar este código como parte de un análisis más grande?** --- ### ✅ Posible solución (la función): - Funcion: contenido_at --- --- ``` python def contenido_at(secuencia): #Calcula el contenido AT de una secuencia de ADN. secuencia = secuencia.tupper() length = len(secuencia) a_count = secuencia.count('A') t_count = secuencia.count('T') return (a_count + t_count) / length #resultado = contenido_at("ACTGATCGATTACGTATAGTATTTGCTATCATACATATATATCGATGCGTTCAT") #print(f"AT content: {resultado:.2f}") ``` Quita los 2 últimos comentarios ... ¿qué ocurre? --- ### 💡 Nota importante .content-box-green[ > Python **no ejecuta el código dentro de una función** cuando esta se define. > Solo lo **interpreta y lo guarda en memoria** con su nombre. > Por eso, si hay un error dentro del bloque de la función, **no se mostrará** al definirla — el error solo aparecerá cuando realmente **llames (ejecutes)** la función. ] --- ``` python def contenido_at(secuencia): # Calcula el contenido AT de una secuencia de ADN. secuencia = secuencia.upper() # ¡¡corregido!! length = len(secuencia) a_count = secuencia.count('A') t_count = secuencia.count('T') return (a_count + t_count) / length resultado = contenido_at("ACTGATCGATTACGTATAGTATTTGCTATCATACATATATATCGATGCGTTCAT") print(f"AT content: {resultado:.2f}") ``` --- ## 🧠 Ejercicio para transformar en función Conteo de bases de una secuencia de ADN. ``` python # Secuencia de ADN dna = "ATGCTTCAGAAAGGTCTTACG" # Contar bases a = dna.count('A') t = dna.count('T') g = dna.count('G') c = dna.count('C') # Imprimir resultados print("A:", a) print("T:", t) print("G:", g) print("C:", c) ``` --- ### ❓ Preguntas para guiar la transformación 1. 🔹 ¿Cuál es la **entrada**? - ¿Qué cambiarías si tuvieras otra secuencia? 2. 🔹 ¿Cuál es la **salida**? - ¿Debería imprimir o devolver los conteos? 3. 🔹 ¿Qué parte podrías **generalizar**? 4. 🔹 ¿Qué pasa si necesitas usar este código 100 veces en diferentes secuencias? --- ### Respuesta ``` python def contar_bases(dna): """ Cuenta e imprime el número de A, T, G y C en una secuencia de ADN. """ dna = dna.upper() # Asegura que todas las letras estén en mayúsculas a = dna.count('A') t = dna.count('T') g = dna.count('G') c = dna.count('C') print("A:", a) print("T:", t) print("G:", g) print("C:", c) # Llamando a la función dna = "ATGCTTCAGAAAGGTCTTACG" contar_bases(dna) ``` --- ## 🔁 ¿Imprimir o retornar? ### Opción 1: retornar con `return` → más flexible ``` python def contar_bases(dna): ... return conteo ``` Usa `print()` solo al momento de mostrar resultados. ### Opción 2: imprimir desde dentro de la función ``` python def print_contar_bases(dna): ... print(conteo) ``` Útil para tareas simples, **pero limita** el uso posterior del valor (no se puede guardar, comparar, ni reutilizar). --- ## Versión modificada - No imprimir dentro de la función, sino regresar los conteos. - Qué tipo de variable usamos para guardar los valores? --- ## Versión con diccionarios ``` python def contar_bases(dna): # Cuenta el número de A, T, G y C en una secuencia de ADN. dna = dna.upper() conteo = { 'A': dna.count('A'), 'T': dna.count('T'), 'G': dna.count('G'), 'C': dna.count('C') } return conteo # Llamada a la función dna = "ATGCTTCAGAAAGGTCTTACG" resultado = contar_bases(dna) # Imprimir los resultados fuera de la función for base, cantidad in resultado.items(): print(f"{base}: {cantidad}") ``` --- ## 🧠 **Ámbito de las variables (Scope)** ### 🔹 ¿Qué es el _scope_ (ámbito) de una variable? -- Cuando defines una variable en Python, **su ámbito determina en qué parte del programa esa variable es accesible**. - Las variables **definidas dentro de una función** son **locales** a esa función. - Las variables **definidas fuera** de cualquier función son **globales**. --- ## Acceso a variables globales dentro de una función ``` python a = 1 b = 10 def fn(): print(a) # Se refiere a la variable global 'a' b = 20 # Se crea una nueva variable local 'b' print(b) # Imprime la 'b' local fn() print(b) # Imprime la 'b' global, que NO cambió ``` ✅ Salida esperada:: --- ## 🔒 Regla general de búsqueda de variables (LEGB) Python busca una variable en este orden: 1. **L**ocal – dentro de la función actual 2. **E**nclosing – en funciones anidadas (si existen) 3. **G**lobal – en el script principal 4. **B**uilt-in – funciones internas de Python (`print`, `len`, etc.) --- ## ⚠️ Error común: acceder a variables locales fuera de su ámbito ``` python def get_at_content(dna): length = len(dna) a_count = dna.count('A') return (a_count + dna.count('T')) / length print(a_count) # ❌ ERROR: a_count está definida solo dentro de la función ``` --- ## 🧪 Argumentos para personalizar el resultado Podemos agregar **más argumentos** a nuestras funciones para que el usuario pueda **modificar el comportamiento** o la salida. En este ejemplo, permitimos que quien use la función decida **cuántas cifras decimales** mostrar en el resultado del contenido AT: ``` python def get_at_content(dna, sig_figs): # Calcula el contenido AT de una secuencia de ADN, # redondeando a 'sig_figs' cifras decimales. length = len(dna) a_count = dna.upper().count('A') t_count = dna.upper().count('T') at_content = (a_count + t_count) / length return round(at_content, sig_figs) # usamos el argumento para redondear test_dna = "ATGCATGCAACTGTAGC" print(get_at_content(test_dna, 1)) # → 0.5 print(get_at_content(test_dna, 2)) # → 0.50 print(get_at_content(test_dna, 3)) # → 0.500 ``` --- ### 🧠 Reflexión para alumnos: - ¿Qué pasaría si no pasamos el segundo argumento? (→ Generará un error a menos que tenga un valor por defecto). - ¿Cómo podrías hacer que `sig_figs` sea **opcional**? --- ## 🎯 Paso de argumentos En Python, puedes **pasar argumentos** a una función de distintas formas: ### 🟢 1. Argumentos posicionales Se pasan **en el orden en que se definen** los parámetros en la función. ``` python # definicion def get_at_content(dna, sig_figs): #.... return round(at_content, sig_figs) # llamado a la función get_at_content("ATCGTGACTCG", 2) ``` - `"ATCGTGACTCG"` se asigna a **`dna`** - `2` se asigna a **`sig_figs`** **Y si se te olvida el orden y los intercambias??** --- ### 🟡 2. Argumentos con nombre (keyword arguments) Puedes especificar el **nombre del parámetro**, lo que hace que el **orden no importe**. ``` python get_at_content(dna="ATCGTGACTCG", sig_figs=2) get_at_content(sig_figs=2, dna="ATCGTGACTCG") ``` ✔️ Ambas llamadas hacen lo mismo. --- ### ⚠️ Reglas importantes - Los **argumentos posicionales deben ir primero**. - Después se pueden poner los **argumentos con nombre**. ❌ Este ejemplo es inválido: ``` python get_at_content(dna="ATCGTGACTCG", 2) # ❌ ERROR ``` Llamadas válidas: ``` python # Posicional + keyword → ✔️ correcto get_at_content("ATCGTGACTCG", sig_figs=3) # Solo keyword arguments → ✔️ correcto get_at_content(dna="ATCGTGACTCG", sig_figs=2) # Solo posicionales → ✔️ correcto get_at_content("ATCGTGACTCG", 2) ``` --- ## 🧩 Definir parámetros con valores por defecto Puedes definir un valor por defecto en la función para que **ese parámetro sea opcional**. ``` python def get_at_content(dna, sig_figs=2): length = len(dna) a = dna.upper().count('A') t = dna.upper().count('T') at_content = (a + t) / length return round(at_content, sig_figs) get_at_content("ATCGTGACTCG") # Usa el valor por defecto (2) get_at_content("ATCGTGACTCG", sig_figs=3) # Usa 3 cifras significativas ``` #### ✅ Ventajas de usar parámetros por nombre o con valor por defecto - Mejora la **legibilidad** del código. - Permite **parámetros opcionales**. - Hace que las funciones sean más **versátiles** y fáciles de usar. --- # 🔄 Parámetros variables: *args y **kwargs ## 🧠 ¿Qué son `*args` y `**kwargs`? Permiten que una función reciba **una cantidad variable de argumentos**, sin saber de antemano cuántos serán. --- ## 🔹 `*args` → argumentos posicionales variables - Usamos `*args` para recibir **cualquier número de argumentos posicionales**. - Internamente, Python los guarda como una **tupla**. ### ✅ Ejemplo: ``` python def suma(*numeros): return sum(numeros) print(suma(1, 2)) # 3 print(suma(1, 2, 3, 4, 5)) # 15 ``` --- ## 🔸 `**kwargs` → argumentos nombrados variables - Usamos `**kwargs` para recibir **cualquier número de argumentos con nombre**. - Internamente, Python los guarda como un **diccionario**. ### ✅ Ejemplo: ``` python def mostrar_info(**datos): for clave, valor in datos.items(): print(f"{clave}: {valor}") mostrar_info(nombre="Ana", edad=28, ciudad="CDMX") ``` --- ## 💡 Puedes combinarlos ``` python def ejemplo_completo(a, b, *args, **kwargs): print(f"a = {a}, b = {b}") print("args:", args) print("kwargs:", kwargs) ejemplo_completo(1, 2, 3, 4, 5, nombre="Ana", edad=30) ``` #### 📌 Salida: --- ## 🧠 Reglas importantes - El orden siempre debe ser: ``` python def funcion(fijos, *args, **kwargs): ``` - Solo puede haber **uno** de cada uno en la definición. - `args` y `kwargs` son nombres convencionales, pero puedes usar otros: ``` python def funcion(*valores, **opciones): ``` --- ## ✅ Buenas prácticas (PEP8 + PEP257) | Práctica | ¿Por qué? | |-----------------------------|----------------------------------------------------------| | **Nombre descriptivo** | Facilita entender el propósito de la función | | **Una sola responsabilidad** | Evita funciones confusas y difíciles de probar | | **Retornar, no imprimir** | Permite reutilizar y testear los resultados | | **Valores por defecto** | Mejora flexibilidad y facilidad de uso | | Docstring | Documenta claramente qué hace y cómo usarla | | Probar con `assert` | Asegura que la función funciona como se espera | | Validar entradas | Evita errores difíciles de detectar | | Reutilizar en módulos | Mantiene el código organizado y limpio | --- ## 💡 ¿Qué es un docstring? Un **docstring** (_documentation string_) es un texto que se escribe justo después de la definición de una función (o clase/módulo) para **describir qué hace y cómo se usa**. ### ✍️ Se escribe entre triple comillas (`""" """` o `''' '''`) Se trata de una **documentación interna**, que: - Facilita la lectura del código por otras personas (o por ti mismo en el futuro). - Aparece cuando usas funciones como `help()` o herramientas como Jupyter o VSCode. - Es una **buena práctica obligatoria** en proyectos profesionales y académicos. --- ## 🧪 Ejemplo: get_at_content Versión con docstring ``` python def get_at_content(dna, sig_figs=2): """ Calcula el contenido AT de una secuencia de ADN, redondeando a un número específico de cifras decimales. Parámetros: dna (str): Secuencia de ADN (ej. 'ATGCGC') sig_figs (int, opcional): número de cifras decimales (por defecto = 2) Retorna: float: contenido AT redondeado """ dna = dna.upper() length = len(dna) a = dna.count('A') t = dna.count('T') at_content = (a + t) / length return round(at_content, sig_figs) ``` --- ## ℹ️ ¿Qué debe incluir un buen docstring? 1. **Una línea resumen clara**. 2. (Opcional) Una descripción más extensa si lo amerita. 3. Lista de **parámetros** con sus tipos y significados. 4. **Valor retornado** y su tipo. ✅ Buenas prácticas PEP257 - Usar comillas triples. - Comenzar con una frase en modo afirmativo (“Calcula…”, “Devuelve…”). - Describir argumentos, especialmente si hay más de uno. - No olvidar incluir el tipo de dato esperado y retornado. Ver https://www.datacamp.com/es/tutorial/docstrings-python --- ## ✅ Introducción al **Testing** en Python ### 🧠 ¿Por qué probar nuestro código? En programación, **cometer errores es inevitable**. Por eso, es fundamental incluir mecanismos que: - Detecten errores antes de que el programa se use en producción. - Verifiquen que las funciones realmente hacen lo que dicen en su docstring. - Faciliten el mantenimiento al evitar que los cambios rompan funcionalidades anteriores. > ✨ El testing es como una red de seguridad para tu código. --- ## 🧪 ¿Cómo hacemos testing en Python? ### ✅ Con la instrucción `assert` ``` python assert expresión_booleana ``` - Si la expresión es `True`, **el programa sigue normalmente**. - Si es `False`, lanza un **`AssertionError`** que detiene el programa. --- ## 🧬 Ejemplo aplicado: función get_at_content ``` python def get_at_content(dna, sig_figs=2): """ Calcula el contenido AT de una secuencia de ADN. Retorna el resultado redondeado a 'sig_figs' cifras decimales. """ dna = dna.upper() length = len(dna) a_count = dna.count('A') t_count = dna.count('T') at_content = (a_count + t_count) / length return round(at_content, sig_figs) ``` 🧪 Testing básico con assert ``` python assert get_at_content("ATGC", 1) == 0.5 assert get_at_content("ATGC", 1) == 0.4 # ❌ AssertionError ``` --- ## ⚠️ Testing también revela problemas lógicos ``` python assert get_at_content("ATGCNNNNNN", 1) == 0.5 ``` ❌ Este test fallará si la función no descarta las N. 💡 Solución: limpiar la secuencia dentro de la función: ``` python dna = dna.replace("N", "") ``` --- ## 🔁 Testing con múltiples casos Puedes validar diferentes entradas así: ``` python assert get_at_content("A") == 1.0 assert get_at_content("G") == 0.0 assert get_at_content("ATGC") == 0.5 assert get_at_content("AGG", 1) == 0.3 assert get_at_content("AGG", 5) == 0.33333 ``` ✔️ Cada línea valida un caso distinto. --- ## 🛡️ Validación con `assert` dentro de la función Puedes proteger la función de errores de uso con `assert`: ``` python def get_at_content(dna, sig_figs=2): assert isinstance(dna, str), \ f"dna debe ser string, no {type(dna)}" assert dna != "", "dna no puede ser una cadena vacía" assert sig_figs >= 0, "sig_figs debe ser mayor o igual a cero" dna = dna.upper().replace("N", "") length = len(dna) a_count = dna.count('A') t_count = dna.count('T') return round((a_count + t_count) / length, sig_figs) ``` > La función `isinstance(objeto, clase_tipo)` sirve para verificar si un objeto es de un tipo o clase específica. --- ## ✅ ¿Se deben dejar los `assert` en producción? ### 🧪 1. **Durante el desarrollo**: ¡Sí, úsalos! - `assert` es perfecto para **testing interno**, validar suposiciones y detectar errores temprano. - Ayuda a encontrar fallos lógicos o mal uso de funciones. - No requiere librerías externas ni estructura formal de testing. --- ### 🚨 2. **En producción (programas ya en uso)**: **❌ No deberías confiar en `assert` para validaciones importantes** #### ¿Por qué? - Los `assert` **pueden ser ignorados automáticamente** si Python se ejecuta con el modo optimizado (`python -O programa.py`). - En ese modo, **todos los `assert` se eliminan al compilar**. ✅ En producción, usa validaciones explícitas: ``` if not isinstance(dna, str): raise TypeError("dna debe ser una cadena de texto") if sig_figs < 0: raise ValueError("sig_figs debe ser mayor o igual a cero") ``` Esto nunca será ignorado y puedes manejarlo con try/except. --- ### ✅ Reglas prácticas para usar `assert` | Contexto | ¿Usar `assert`? | ¿Por qué? | |--------------------|-----------------|-----------------------------------------------| | Desarrollo | ✅ Sí | Para validar suposiciones y detectar errores | | Scripts personales | ✅ Sí | Rápido y cómodo para testing | | Producción | ⚠️ No | Puede ser ignorado con `-O`; usa excepciones | | Testing formal | ⚠️ No | Usa `unittest` o `pytest` en su lugar | --- ## ✅ Conclusión > Usa `assert` como herramienta **de desarrollo**, no como validación definitiva en producción. > Mas adelante veremos otras formas de hacer Testing. --- ## 🔝 Características avanzadas de funciones en Python | Característica | Descripción breve | Ejemplo corto | |--------------------------------------|-----------------------------------------------------------------------------------|---------------| | Funciones anidadas | Definir funciones dentro de otras para encapsular lógica | `def externa(): def interna(): ...` | | Closures | Funciones internas que recuerdan variables del entorno externo | `def outer(x): def inner(): return x` | | Decoradores | Añaden funcionalidad a otra función sin modificarla directamente | `@mi_decorador` | | Funciones como objetos | Se pueden pasar, retornar y asignar a variables | `f = saludar` | | Recursividad | Función que se llama a sí misma | `def factorial(n): return n * factorial(n-1)` | --- ## 🔝 Características avanzadas de funciones en Python | Característica | Descripción breve | Ejemplo corto | |--------------------------------------|-----------------------------------------------------------------------------------|---------------| | Funciones lambda | Funciones anónimas en una sola línea | `lambda x: x ** 2` | | Funciones de orden superior | Reciben funciones como argumentos o devuelven funciones | `map(lambda x: x+1, lista)` | | Type hints | Anotan tipos de parámetros y retorno | `def f(x: int) -> int:` | | Funciones generadoras (`yield`) | Generan secuencias sin cargar todo en memoria | `def gen(): yield 1` | --- # 📚 Recursos extra - [PEP8](https://peps.python.org/pep-0008/) - [Documentación oficial de funciones](https://docs.python.org/es/3/tutorial/controlflow.html#defining-functions)