Declaración de funciones en Python

Encuentra el código de la lección:

Tipado y funciones: estático vs. dinámico

Un recordatorio rápido de cómo se diferencian Kotlin y Python en cuanto al sistema de tipos:

  • Kotlin: tipado estático con inferencia y nulabilidad explícita. Los errores de tipo se detectan en compilación.
  • Python: tipado dinámico en tiempo de ejecución. Cuenta con type hints (PEP 484), opcionales, que pueden validarse con herramientas como ty, mypy o Pyright.
Estructura general en Python
def function_name(param_1: Type1, param_2: Type2) -> ReturnType:
    # cuerpo de la función
    return result

Explicación de la sintaxis

  • def : palabra clave para declarar una función.
  • function_name : nombre en snake_case.
  • (param: Type) : parámetros opcionales con anotaciones de tipo.
  • -> ReturnType : tipo de retorno, opcional.
  • Cuerpo: bloque indentado; usa return para devolver valores.

Comparación rápida con Kotlin

En Python, las anotaciones de tipo son sugerencias que pueden validarse con herramientas externas, pero el intérprete las ignora en tiempo de ejecución.

Kotlin, en cambio, sí valida los tipos y la nulabilidad en compilación, ofreciendo seguridad antes de ejecutar el programa.

Estilo de nombres

En Python, las funciones y variables deben nombrarse usando la convención snake_case, de acuerdo a las guías de estilo de PEP 8.

  • El nombre comienza con una letra minúscula.
  • Cada palabra siguiente se separa con un guion bajo.

Ejemplos correctos:

  • calculate_total
  • print_message
  • main

Usar un estilo de nombres consistente mejora la legibilidad y asegura que tu código esté alineado con las prácticas idiomáticas de Python.

Estilos incorrectos

Evita estilos heredados de otros lenguajes o que no son válidos en Python:

  • CalculateTotal PascalCase, reservado para clases.
  • calculateTotal camelCase, típico en Java/Kotlin, no se utiliza en funciones de Python.
  • calculate-total kebab-case, inválido como identificador.
  • CALCULATE_TOTAL UPPER_SNAKE_CASE, reservado para constantes.

Ejemplo: Sumar dos números

Función con tipos y doctest
type-fundamentals/basics/functions/typing.py
def add(a: int, b: int) -> int:
    return a + b

if __name__ == "__main__":
    # Example usage of the add function
    print("Adding 2 and 3 gives:", add(2, 3))  # ➜ 5
    # At runtime, Python allows this and produces '12' (concatenation).
    # However, ty/mypy/pyright will flag a *type error* (expected ints).
    print("Adding '1' and '2' gives:", add("1", "2")) # ➜ 12

Argumentos por defecto y nombrados

Puedes definir valores por defecto y usar argumentos nombrados para mejorar la legibilidad:

Función con parámetros por defecto
type-fundamentals/basics/functions/default.py
def summon(character: str, location: str = "Rivendell") -> str:
    return f"{character} has been summoned to {location}."


if __name__ == "__main__":
    print(summon("Gandalf"))  # ➜ Gandalf has been summoned to Rivendell.
    print(
        summon(character="Aragorn", location="Minas Tirith")
    )  # ➜ Aragorn has been summoned to Minas Tirith.

Valores por defecto mutables: bug clásico

En Python, los valores por defecto de los parámetros se evalúan una sola vez al definir la función. Si usas una lista, dict o set como valor por defecto, se compartirá entre invocaciones y acumulará estado.

Problema con valores por defecto mutables
type-fundamentals/basics/functions/default.py
from typing import Any

def log_event(
    event: dict[str, Any], buffer: list[dict[str, Any]] = []
) -> list[dict[str, Any]]:
    buffer.append(event)
    return buffer


if __name__ == "__main__":
    event_a = {
        "trainer": "Brendan",
        "action": "catch",
        "pokemon": "Ralts",
        "location": "Route 102",
    }
    event_b = {
        "trainer": "May",
        "action": "badge",
        "badge": "Stone Badge",
        "gym": "Rustboro City",
    }

    a = log_event(event_a)
    b = log_event(event_b)

    print(a is b)  # → True; same shared object
    print(len(b))  # → 2; Brendan and May's events mixed
    print([e["trainer"] for e in b])  # → ['Brendan', 'May']

Cómo solucionarlo

Patrón del centinela con None (referencia)
type-fundamentals/basics/functions/default.py
from typing import Any

def buffer_event_safe(
    event: dict[str, Any],
    buffer: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
    if buffer is None:
        buffer = []
    buffer.append(event)
    return buffer


if __name__ == "__main__":
    event_a = {
        "trainer": "Brendan",
        "action": "catch",
        "pokemon": "Ralts",
        "location": "Route 102",
    }
    event_b = {
        "trainer": "May",
        "action": "badge",
        "badge": "Stone Badge",
        "gym": "Rustboro City",
    }

    safe_a = buffer_event_safe(event_a)
    safe_b = buffer_event_safe(event_b)

    print(safe_a is safe_b)                 # → False; different objects
    print(len(safe_b))                      # → 1; only May's event
    print([e["trainer"] for e in safe_b])   # → ['May']

¿Qué acabamos de hacer?

  • Los objetos mutables como valores por defecto se evalúan una sola vez y se comparten entre invocaciones.
  • El uso de None como centinela permite crear una nueva lista en cada llamada.
  • La anotación list[dict[str, Any]] | None mantiene la intención clara y verificable por herramientas de análisis estático como ty, mypy o Pyright.
  • Si se pasa un buffer explícito, la mutación es intencional; si no, se crea una lista fresca de manera segura.

Funciones variádicas ( *args y **kwargs )

En Python, los argumentos posicionales capturados por *args se agrupan en una tupla, y los argumentos con nombre capturados por **kwargs se agrupan en un diccionario. Es decir, la llamada se transforma internamente en estos contenedores estándar.

Veamos un ejemplo centrado en tipos: distingue el tipo del contenedor ( tuple / dict ) y el tipo de cada elemento almacenado en ellos.

Ejercicio: ¿Qué imprime el siguiente código?

def variadic_types(*args: int, **kwargs: int | str) -> str:
    return "\n".join(
        (
            f"args: {type(args)}",    # type of "*args"
            *map(lambda a: f"{a}: {type(a)}", args),    # type of each element in "*args"
            f"kwargs: {type(kwargs)}",  # type of "**kwargs"
            *map(
                # type of each key-value pair in "**kwargs"
                lambda kv: f"({kv[0]}: {type(kv[0])}) -> ({kv[1]}: {type(kv[1])})",
                kwargs.items(),
            ),
        )
    )


if __name__ == "__main__":
    print(
        variadic_types(1, 2, k1="v1", k2=3)
    )

Explicación mínima

"\n".join(...)" concatena líneas en un único str . map(f, iterable) aplica f a cada elemento para producir las líneas sin usar bucles. kwargs.items() recorre las parejas clave–valor como tuplas (key, value) .

El objetivo es evidenciar cómo *args y **kwargs empaquetan los argumentos (tupla/diccionario) y cómo inspeccionar el tipo de cada elemento.

Salida del programa
args: <class 'tuple'>
1: <class 'int'>
2: <class 'int'>
Kwargs: <class 'dict'>
(k1: <class 'str'>) -> (v1: <class 'str'>)
(k2: <class 'str'>) -> (3: <class 'int'>)

TODO: completar esta sección

from functools import reduce
from operator import add

def sum_positives(*nums: int) -> int:
    print(type(nums))   # Outputs: <class 'tuple'>
    return reduce(add, filter(lambda n: n > 0, nums), 0)


if __name__ == "__main__":
    print(sum_positives(1, -2, 3, 4, -5))  # Outputs: 8
    print(sum_positives())  # Outputs: 0
    print(sum_positives(-1, -2, -3))  # Outputs: 0