Programas sencillos, funciones y decisiones

¿Cuál es la diferencia entre el intérprete y ejecutar un programa?

En la terminal o línea de comandos de nuestro sistema operativo deberíamos tener disponible el comando python3.

Cuando ejecutamos python3 por sí solo se abre el interprete de Python. Por ejemplo desde una terminal de Ubuntu:

alumno@latitude3520:/home/alumno$ python3
Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

En el intérprete podemos escribir instrucciones de Python y ver su resultado. Se suele usar a modo de prueba o para consultar documentación. Para salir podemos escribir la instrucción exit().

Por otro lado, para ejecutar un programa disponible en un archivo de Python debemos indicarle al comando python3 el nombre de este archivo. Por ejemplo asumiendo que en la carpeta /home/alumno/ se encuentra un archivo prueba.py con una sola línea dentro de este:

print("Soy una demostración de la ejecución de un programa")

Podríamos ejecutar dicho archivo de la siguiente forma:

alumno@latitude3520:/home/alumno$ python3 prueba.py
Soy una demostración de la ejecución de un programa
alumno@latitude3520:/home/alumno$

Acá se ejecutó nuestro archivo en su totalidad y una vez finalizado volvimos a tener acceso a la entrada de la terminal.

Entonces para recapitular:

  • Con el comando python3 se abre el intérprete
  • Con el comando python3 nombre_archivo.py se ejecuta el programa dado por las instrucciones de nombre_archivo.py

Estoy intentando ejecutar mi programa pero me está saltando "can't open file or directory"

Una de las formas que recomendamos ejecutar archivos .py es mediante la línea de comandos o terminal del sistema operativo, que se la puede pensar como un explorador de archivos simple.

Cuando se abre la línea de comandos, seguramente no esté posicionada en la carpeta donde se encuentra el archivo .py relevante. Es necesario utilizar los comandos básicos para que logremos ejecutar el archivo. Algunos de estos comandos son:

  • cd <nombre-dir>: cambia al directorio indicado por <nombre-dir>.
  • ls o dir (para Windows o Linux/macOS respectivamente): muestra un listado del contenido del directorio actual. Si vemos nuestro archivo acá, ahora si podremos ejecutar nuestro archivo con el comando python <nombre-archivo>.py.

¿Cómo le puedo pedir al usuario un número para luego realizar operaciones?

La función input() nos ayuda a pedirle valores al usuario, pero lo que obtenemos siempre será una cadena:

>>> entrada = input("Ingrese un numero: ")
Ingrese un numero: 3
>>> entrada
'3'
>>> entrada + 5
TypeError: can only concatenate str (not "int") to str

Tal como nos dice el mensaje de error, no podemos tratar a entrada como un número inmediatamente. Es necesario transformar la salida de input() a un entero con el uso de la función int():

>>> entrada = int(input("Ingrese un numero: "))
Ingrese un numero: 3
>>> entrada
3
>>> entrada + 5
8

¿Por qué la función no modifica el valor de mi variable?

Si se tiene el siguiente código:

def restar_tres(n):
    n = n - 3

m = 5
restar_tres(m)
print(m)

Al ejecutarse, va a imprimir 5. ¿Por qué?

La variable m tiene un entero, y es un tipo de dato simple. Para este y otros como flotantes, booleanos (y otros tipos de datos inmutables como cadenas y tuplas) nuestra función realiza copias de los valores para asignarlos a los parámetros, creando nuevas variables locales dentro de lo que se llama el entorno de la función. Por esto el n de restar_tres() es local de la función e independiente del m definido fuera de este.

Lo mismo sucede si el parámetro de restar_tres tendría el mismo nombre que m. Dentro de la función el nombre m hace referencia al parámetro y no al m de afuera. El siguiente ejemplo también imprime 5:

def restar_tres(m):
    m = m - 3

m = 5
restar_tres(m)
print(m)

¿Cómo puedo utilizar el retorno de una función que devuelve más de un valor?

Si sabemos con seguridad que nuestra función siempre devuelve una determinada cantidad de valores N, podemos desempaquetar cada uno en N variables.

def numeros_adyacentes(n):
    return n - 1, n + 1

anterior, proximo = numeros_adyacentes(5)
print(anterior)
print(proximo)

Alternativamente, podemos tratar al retorno en una variable sola como una tupla de N valores. A los diferentes valores de las tuplas se las puede acceder con variable[i], tal como se volverá a repasar en la guía de secuencias.

def numeros_adyacentes(n):
    return n - 1, n + 1

numeros = numeros_adyacentes(5)
print(numeros[0])
print(numeros[1])

¿Cuáles son las diferentes formas de reutilizar funciones de otro archivo .py?

Supongamos que contamos con un archivo llamado operaciones_simples.py con las funciones sumar, restar, multiplicar, y las queremos reutilizar en un archivo main.py que se encuentra en el mismo directorio.

Existen tres formas de invocar a estas funciones. La primera es simplemente importando todo el archivo para que se encuentre bajo el nombre operaciones_simples:

import operaciones_simples

x = operaciones_simples.sumar(3, 2)
print(operaciones_simples.restar(x, 7))

Por otro lado, podemos utilizar la operación from para importar solo esas funciones

from operaciones_simples import suma
from operaciones_simples import resta
x = sumar(3, 2)
print(restar(x, 7))

Por último, se puede importar todo el archivo con el uso de from y *, pero su uso no se recomienda porque estaríamos importando todo el archivo en nuestro código y no sabemos qué puede llegar a incluir.

from operaciones_simples import *

x = sumar(3, 2)
print(restar(x, 7))

Extra: acá estamos tratando a operaciones_simples.py como un módulo. Es importante que el mismo no incluya ninguna llamada print ni input como lo tendría un programa normal, porque su fin es definir funciones para incluir en otro código. Si la llamada a import incluye algún tipo de entrada/salida como en este ejemplo, hay que borrarla. Por ejemplo, esto es lo que no debería suceder:

>>> import funciones_simple
Ingrese un número: 3
Ingrese otro número: 4
Suma dió 7
>>>

¿Cómo se definen constantes en Python?

Las constantes son una forma de asignar un nombre particular a un valor particular, resultando en un código más claro y fácil de modificar. Son similares a las variables, con la diferencia de que no pueden modificarse.

No se pueden definir constantes en Python, pero por convención podemos decir que variables definidas al comienzo del archivo py y escritas en mayúsculas con palabras separadas usando guión bajo son constantes. Por ejemplo:

NUMERO_PI = 3.14159

def perimetro_circulo(radio):
    return 2 * NUMERO_PI * radio

Por supuesto, como las constantes en Python no dejan de ser una convención, queda en nosotros asegurarnos de nunca modificar el valor de NUMERO_PI.

Veo ejercicios que piden programas y otros que piden funciones. ¿Cuál es la diferencia?

Con un programa se espera que se escriba un archivo .py y luego se cumpla todo lo que pida el enunciado cuando se ejecute el mismo con python <nombre-archivo>.py.

Por otro lado, una función sería escribir una función de Python la cuál puede llamar a otras funciones auxiliares. Generalmente producen un único resultado que se devuelve en vez de imprimirse.

El ejercicio no especifica exactamente cómo dar el resultado. ¿Qué debería hacer? ¿Imprimir o devolver?

Supongamos una función simple promediar(a, b, c). ¿Cuál de estas dos alternativas tendría más sentido?

def promediar_imprimir(a, b, c):
    print((a + b + c) / 3)

def promediar_devolver(a, b, c):
    return (a + b + c) / 3

Las dos funciones, al menos desde lejos, parece que hacen lo mismo:

>>> promediar_imprimir(1, 2, 3)
2
>>> promediar_devolver(1, 2, 3)
2

Pero la primer alternativa tiene un uso muy limitado, mientras que la segunda se puede reutilizar el valor para hacer cualquier operación: realizar otro cálculo, imprimir el mismo por pantalla, y más, solo porque puede guardarse su valor en una variable.

>>> v1 = promediar_imprimir(1, 2, 3)
2
>>> v1
None
>>> v2 = promediar_devolver(1, 2, 3)
>>> v2
2

Entonces salvo que busquemos una función que solo muestre por pantalla un resultado, la mejor alternativa es utilizar un return.

¿Qué debería escribir en la documentación de las funciones?

Depende de la función pero en líneas generales con una breve descripción de qué recibe y qué devuelve es suficiente, de tal forma que se entienda su funcionamiento. Por ejemplo:

def norma(x, y, z):
    """Recibe un vector en R3 y devuelve su norma"""
    . . .

Algunos ejemplos de documentación pobre o poco clara:

def norma(x, y, z):
    """calcula norma"""

def norma(x, y, z):
    """devuelve (x**2 + y**2 + z**2) ** 0.5"""

Nota: En muchos casos, un nombre adecuado para la función y los parámetros es suficiente para entender qué hace la función sin necesidad de escribir una documentación. No obstante, documentar no deja de ser una buena práctica. Tanto documentación como buenos nombres para las funciones y parámetros son características importantes a la hora de evaluar la calidad de código.

¿Qué puedo incluir como condición?

Cuando nosotros escribimos if <expresion>, internamente Python evalúa dicha expresión con la función bool, y si la misma devuelve True se entra al código del if.

Se puede experimentar un poco con la función bool() y ver qué devuelve:

>>> bool('hola')
True
>>> bool('holaaa')
True
>>> bool('')
False
>>> bool(True)
True
>>> bool(False)
False
>>> bool(None)
False
>>> bool(1)
True
>>> bool(2)
True
>>> bool(0)
False
>>> bool(-1)
True

Nota: Es por esto que si tenemos una variable booleana flag y escribimos if flag:, sería lo mismo que escribir if flag == True:. Por claridad de código, es mejor ir por la primer opción.

¿Cómo puedo estructurar mejor mi código y evitar el código flecha?

Le decimos código flecha a un código con demasiados niveles de indentación, el cuál es complicado de leer. Generalmente sucede cuando se usan múltiples condiciones.

def es_bisiesto(anio):
    if anio % 4 == 0:
        if anio % 100 == 0:
            if anio % 400 == 0:
                return True
            else:
                return False
        else:
            return True
    else:
        return False

A este ejemplo podemos aplicar algunas "prácticas" comunes:

  • Si el cuerpo del if termina con un return, se puede eliminar el else porque si no se cumple la condición nunca se ejecutaría lo que está abajo del if.
  • Si gran parte de nuestra función se encuentra en una condición if, para reducir el nivel de indentación podemos invertir la primera condición y los cuerpos de cada una. Es decir, cambiar if anio % 4 == 0 por if anio % 4 != 0 y en caso que se cumpla, devolver inmediatamente. Así el resto del cuerpo de la función puede bajar el nivel de indentación.

Después de aplicar estas prácticas podemos reducir el código a la siguiente función.

def es_bisiesto(anio):
  if anio % 4 != 0:
      return False
  if anio % 100 != 0 or anio % 400 == 0:
      return True
  return False

Y se puede simplificar aún más!

  • Depende mucho del caso, pero la existencia de if consecutivos puede reemplazarse por el uso de or o and.
  • Si nuestro código termina con la estructura if x: return True; else: return False donde solo estamos devolviendo un booleano de los dos lados del if, deberíamos simplemente devolver la condición que estamos evaluando.
def es_bisiesto(anio):
  return anio % 4 == 0 and (anio % 100 != 0 or anio % 400 == 0)

En este proceso existen muchos pasos intermedios que no incluímos. Recomendamos repetir este ejercicio desde el primer ejemplo, realizando todos los pasos intermedios para llegar a la expresión final.