Módulo 2 · Lección 8 de 28
Argumentos con sys.argv
Cómo llegan a tu programa las palabras que se escriben detrás de su nombre en la terminal, y hasta dónde aguanta leerlas a mano.
Hasta ahora, cuando un programa necesitaba un dato, lo pedía con input() y esperaba a que alguien lo tecleara. Funciona, pero obliga a estar delante. Un programa que se para a preguntar no se puede meter en un script, ni programar a las tres de la mañana, ni encadenar con otros.
Las herramientas de terminal de verdad reciben sus datos en la misma línea con la que se las invoca: cp origen destino, python3 build.py --servir, git commit -m "arreglado". Todo lo que va detrás del nombre del programa son sus argumentos, y Python te los entrega en una lista.
La lista sys.argv#
El módulo sys de la biblioteca estándar tiene una variable llamada argv que contiene los argumentos de la invocación, todos como cadenas de texto:
"""Muestra lo que llega en sys.argv."""
import sys
print(sys.argv)
print(f"Han llegado {len(sys.argv)} elementos")Ejecútalo sin nada detrás:
python3 argumentos.py['argumentos.py']
Han llegado 1 elementosLa lista nunca está vacía. En la posición cero está el nombre del script, tal cual lo has escrito tú, no el primer argumento. Los argumentos de verdad empiezan en la posición 1:
python3 argumentos.py hola 42 --verboso['argumentos.py', 'hola', '42', '--verboso']
Han llegado 4 elementosFíjate en dos cosas de esa salida. La primera: 42 llega como '42', con comillas, porque todo llega como texto. Si lo necesitas como número, tendrás que convertirlo tú con int() o float(), exactamente igual que con input(). La segunda: --verboso no tiene nada de especial para sys.argv, es una cadena más. Los guiones solo significan algo cuando alguien decide interpretarlos, y de momento ese alguien eres tú.
Leer un argumento a mano#
El caso más simple es un único argumento obligatorio:
import sys
nombre = sys.argv[1]
print(f"Hola, {nombre}.")python3 saludo.py ErwinHola, Erwin.Funciona mientras se use bien. En cuanto alguien lo lance sin el nombre, el programa revienta:
python3 saludo.pyTraceback (most recent call last):
File "/Users/erwin/terminal/saludo.py", line 3, in <module>
nombre = sys.argv[1]
~~~~~~~~^^^
IndexError: list index out of rangeEse sys.exit(1) termina el programa inmediatamente. El número es el código de salida: cero significa "todo bien" y cualquier otro valor significa "algo ha fallado". La terminal lo usa para encadenar órdenes, y lo verás con detalle más adelante en el módulo. Por ahora quédate con la costumbre: si tu programa no ha podido hacer su trabajo, no salgas con cero.
Todos los argumentos de golpe#
Cuando no sabes cuántos van a llegar, rebana la lista desde la posición 1 y trátala como cualquier otra lista:
"""Suma todos los números que reciba por la línea de comandos."""
import sys
numeros = [float(dato) for dato in sys.argv[1:]]
print(f"He recibido {len(numeros)} números")
print(f"Suma: {sum(numeros)}")python3 sumar.py 10 20 5.5He recibido 3 números
Suma: 35.5sys.argv[1:] es el modismo que verás en todas partes: "la lista sin el nombre del script".
Cómo parte la terminal la línea#
Esto no lo hace Python, lo hace la terminal antes de arrancarlo: corta la línea por los espacios y le pasa a tu programa el resultado ya troceado. Por eso un nombre con espacios se convierte en varios argumentos:
python3 argumentos.py informe final.txt['argumentos.py', 'informe', 'final.txt']
Han llegado 3 elementosHan llegado dos argumentos donde tú querías uno. Para que un valor con espacios viaje entero hay que encerrarlo entre comillas, simples o dobles:
python3 argumentos.py "informe final.txt"['argumentos.py', 'informe final.txt']
Han llegado 2 elementosLas comillas desaparecen por el camino: son una instrucción para la terminal, no parte del dato. Lo mismo vale para cualquier valor que contenga espacios, aunque no sea un nombre de fichero:
python3 argumentos.py "2 + 3" "5 * 4"['argumentos.py', '2 + 3', '5 * 4']
Han llegado 3 elementosUn programa completo#
Con lo anterior ya se puede escribir una herramienta útil de verdad. Este conversor de unidades de longitud recibe tres argumentos y los valida todos antes de calcular nada:
"""Convierte una cantidad entre unidades de longitud.
Uso: python3 conversor.py CANTIDAD ORIGEN DESTINO
"""
import sys
# Cuántos metros vale una unidad de cada tipo.
METROS_POR_UNIDAD = {
"m": 1.0,
"km": 1000.0,
"milla": 1609.344,
"pie": 0.3048,
"pulgada": 0.0254,
}
def convertir(cantidad: float, origen: str, destino: str) -> float:
"""Pasa la cantidad de la unidad de origen a la de destino."""
metros = cantidad * METROS_POR_UNIDAD[origen]
return metros / METROS_POR_UNIDAD[destino]
def main() -> None:
if len(sys.argv) != 4:
print("Uso: python3 conversor.py CANTIDAD ORIGEN DESTINO")
print(f"Unidades: {', '.join(METROS_POR_UNIDAD)}")
sys.exit(1)
try:
cantidad = float(sys.argv[1])
except ValueError:
print(f"'{sys.argv[1]}' no es un número.")
sys.exit(1)
origen = sys.argv[2]
destino = sys.argv[3]
if origen not in METROS_POR_UNIDAD or destino not in METROS_POR_UNIDAD:
print(f"Unidad desconocida. Usa una de: {', '.join(METROS_POR_UNIDAD)}")
sys.exit(1)
resultado = convertir(cantidad, origen, destino)
print(f"{cantidad:g} {origen} = {resultado:.4g} {destino}")
if __name__ == "__main__":
main()Funcionando:
python3 conversor.py 42 km milla42 km = 26.1 millapython3 conversor.py 1 milla m1 milla = 1609 mY los tres caminos de error, cada uno con su mensaje:
python3 conversor.pyUso: python3 conversor.py CANTIDAD ORIGEN DESTINO
Unidades: m, km, milla, pie, pulgadapython3 conversor.py mucho km m'mucho' no es un número.python3 conversor.py 5 km leguasUnidad desconocida. Usa una de: m, km, milla, pie, pulgadaMerece la pena mirar cómo está montado. La función convertir no sabe nada de sys.argv: recibe tres valores ya limpios y devuelve un número, así que se puede importar y probar por separado. Toda la suciedad de la línea de comandos está en main, que es la que comprueba, convierte y se rinde con sys.exit(1) cuando algo no cuadra. Es la misma separación entre lógica e interfaz del gestor de tareas del módulo anterior, solo que ahora la interfaz no es un menú sino la propia invocación.
Dónde se rompe esto#
El conversor está bien para tres argumentos obligatorios y en orden fijo. Ahora imagina que quieres añadir una opción para elegir los decimales del resultado. La opción es opcional, puede aparecer en cualquier sitio de la línea y lleva un valor pegado detrás. A mano queda así:
import sys
argumentos = sys.argv[1:]
decimales = 2
# Buscar la opción y sacarla de la lista antes de contar los posicionales.
if "--decimales" in argumentos:
posicion = argumentos.index("--decimales")
try:
decimales = int(argumentos[posicion + 1])
except (IndexError, ValueError):
print("--decimales necesita un número entero detrás.")
sys.exit(1)
del argumentos[posicion:posicion + 2]
if len(argumentos) != 3:
print("Uso: python3 conversor_manual.py CANTIDAD ORIGEN DESTINO [--decimales N]")
sys.exit(1)
print(f"decimales={decimales}, posicionales={argumentos}")python3 conversor_manual.py 42 km milla --decimales 4decimales=4, posicionales=['42', 'km', 'milla']python3 conversor_manual.py --decimales 4 42 km milladecimales=4, posicionales=['42', 'km', 'milla']Veinte líneas para una sola opción, y ni siquiera son veinte líneas completas: falta aceptar -d como forma corta, falta admitir --decimales=4 con el igual pegado, falta que -h explique el programa, falta rechazar una opción mal escrita en vez de tomarla por un posicional. Cada opción nueva multiplica el enredo, y el código que decide qué es un valor y qué es una opción acaba siendo más largo que el programa de verdad.
Esa es exactamente la razón de que la biblioteca estándar traiga argparse, que es la lección siguiente. No es una comodidad de lujo: es que el problema, aunque parezca trivial desde fuera, tiene demasiados casos particulares para resolverlo bien a mano dos veces.