Funciones, paquetes y APIs
Funciones
Sin darte cuenta, llevas todo el curso usando funciones: print(), len(), type(), int(), sum()… Una función es un bloque de código con nombre que hace una tarea concreta. Para usarla (llamarla) escribes su nombre seguido de paréntesis, y dentro le pasas los datos con los que tiene que trabajar: sus argumentos. La función hace su trabajo y devuelve un resultado, que puedes guardar en una variable.
Argumentos posicionales y con nombre
Algunas funciones aceptan varios argumentos. Los posicionales se identifican por su orden; los argumentos con nombre se escriben como nombre=valor y suelen ser opcionales, porque tienen un valor por defecto:
Devolver no es lo mismo que imprimir
print() muestra un valor en pantalla, pero no lo devuelve: si guardas su resultado en una variable, obtienes None. En cambio, len() devuelve un valor con el que puedes seguir trabajando.
Definir tus propias funciones
Cuando repites el mismo cálculo varias veces, conviene convertirlo en una función. Se definen con def, el nombre de la función, sus parámetros entre paréntesis y dos puntos. El cuerpo de la función va indentado (igual que en un if o un for), y return indica qué valor devuelve:
Fíjate en que hemos escrito el cálculo una sola vez y lo podemos reutilizar con cualquier secuencia. El texto entre triples comillas es la docstring: una descripción de la función que aparece al usar help(contenido_gc).
Los parámetros también pueden tener un valor por defecto, que se usa si al llamar a la función no se indica otro:
Las variables que creas dentro de una función (como num_gc) solo existen dentro de ella. Si intentas usarlas fuera, Python dará un error NameError. Lo que quieras sacar de la función, devuélvelo con return.
Mejora la función contenido_gc para que funcione aunque la secuencia tenga minúsculas y espacios en blanco. Añade un parámetro decimales (por defecto, 2) para redondear el resultado.
Recuerda los métodos de cadenas .upper() y .replace(), y la función round().
Escribe una función media_sin_faltantes(valores, faltante=99) que calcule la media de una lista ignorando los valores faltantes.
Módulos y paquetes
Python trae de serie unas pocas funciones básicas (print, len, sum…), pero la mayor parte de sus herramientas están organizadas en módulos: archivos de Python que agrupan funciones relacionadas. Un paquete es simplemente un conjunto de módulos. Hay dos tipos:
- La biblioteca estándar: módulos que vienen instalados con Python, como
math,random,statistics,csvojson. - Paquetes externos: los escribe la comunidad y hay que instalarlos, como
requests(peticiones web),pandas(tablas de datos) obiopython(bioinformática).
Importar
Para usar un módulo primero hay que importarlo con import. Después, sus funciones se llaman con la sintaxis modulo.funcion():
Hay otras formas de importar:
Igual que con las cadenas, puedes explorar un módulo con dir() y consultar su documentación con help():
Un cultivo de E. coli se duplica cada 20 minutos. Usa el módulo math para calcular cuántos minutos tarda en pasar de 1000 a 1 000 000 de células, sin usar bucles.
El número de duplicaciones necesarias es el logaritmo en base 2 de final / inicial.
Instalar paquetes externos
Los paquetes externos se instalan una sola vez desde la terminal con pip, el gestor de paquetes de Python:
~$ pip install requests
Una vez instalado, se importa como cualquier otro módulo. Si intentas importar un paquete que no está instalado, obtendrás un error ModuleNotFoundError.
Un ejemplo: requests
requests es un paquete externo para hacer peticiones a páginas web desde Python. Su función más importante es requests.get(): recibe como argumento una dirección web (URL) y devuelve la respuesta del servidor.
import requests
respuesta = requests.get("https://rest.ensembl.org/info/ping?content-type=application/json")
print(respuesta.status_code)
print(respuesta.text)200
{"ping":1}
Lo que devuelve requests.get() no es un número ni una cadena, sino un objeto de tipo Response que agrupa toda la información de la respuesta. Como con las cadenas y las listas, accedes a ella con un punto: .status_code es un valor (el código de estado, 200 si todo ha ido bien), .text es el contenido de la respuesta como cadena, y .json() es un método que convierte ese contenido en un diccionario. Puedes ver todo lo que ofrece con dir(respuesta).
Y como requests.get() es una función más, puedes combinarla con tus propias funciones:
import requests
def descargar_json(url):
"""Descarga una URL y devuelve su contenido como diccionario, o None si falla."""
respuesta = requests.get(url)
if respuesta.status_code == 200:
return respuesta.json()
return None
datos = descargar_json("https://rest.ensembl.org/info/ping?content-type=application/json")
print(datos){'ping': 1}
La dirección que le hemos pasado a requests.get() no es una página web normal, sino un servicio pensado para que lo consulten programas: una API. En el resto del tema veremos qué son y cómo sacarles partido.
¿Qué es una API?
Hasta ahora has trabajado con archivos que ya tenías en tu ordenador. Pero gran parte de los datos biológicos que necesitarás — secuencias de referencia, anotaciones de genes, registros de biodiversidad — viven en bases de datos públicas enormes, accesibles por internet. Podrías abrir un navegador, buscar a mano y copiar/pegar, pero eso no escala: si necesitas los datos de 500 genes, no vas a hacerlo 500 veces a mano.
Una API (interfaz de programación de aplicaciones) es una puerta de entrada pensada para programas, no para personas: le envías una petición a una dirección concreta (un endpoint) y te responde con datos en un formato que un programa puede procesar directamente, normalmente JSON. Es como pedir un plato por su nombre en vez de entrar a la cocina a buscarlo tú mismo.
El formato JSON
JSON representa datos con la misma forma que ya conoces de Python: objetos como diccionarios ({clave: valor}) y listas ([...]). Esta respuesta de ejemplo:
{
"id": "ENSG00000139618",
"display_name": "BRCA2",
"species": "homo_sapiens",
"start": 32315086,
"end": 32400268
}… se convierte en Python, prácticamente sin cambios, en el diccionario {"id": "ENSG00000139618", "display_name": "BRCA2", "species": "homo_sapiens", "start": 32315086, "end": 32400268}. Por eso, en cuanto tengas una respuesta JSON en Python, la tratarás exactamente como los diccionarios del módulo 8: respuesta["display_name"], respuesta.get(...), for clave, valor in respuesta.items():.
Anatomía de una petición: descargar un FASTA de NCBI
Las APIs no siempre devuelven JSON. El servicio E-utilities de NCBI, por ejemplo, puede devolver directamente una secuencia en formato FASTA. Fíjate en la URL que usaríamos para pedir los primeros 200 pares de bases del genoma de E. coli:
https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nuccore&id=NC_000913.3&rettype=fasta&retmode=text&seq_stop=200
La URL tiene dos partes: la dirección base del endpoint (.../efetch.fcgi) y, tras el ?, los parámetros separados por &: qué base de datos (db=nuccore), qué accession (id=...), en qué formato quieres la respuesta (rettype=fasta&retmode=text) y seq_stop=200, que limita la descarga a los primeros 200 pares de bases para no traer un genoma completo en un ejercicio de clase.
Escribir todos esos parámetros a mano es incómodo y fácil de equivocar. requests permite pasarlos como un diccionario con el argumento params, y se encarga de construir la URL por ti:
import requests
url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
parametros = {
"db": "nuccore",
"id": "NC_000913.3",
"rettype": "fasta",
"retmode": "text",
"seq_stop": 200,
}
respuesta = requests.get(url, params=parametros)
print(respuesta.url) # la URL completa que ha construido requests
print(respuesta.text)https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nuccore&id=NC_000913.3&rettype=fasta&retmode=text&seq_stop=200
>NC_000913.3:1-200 Escherichia coli str. K-12 substr. MG1655, complete genome
AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC
TTCTGAACTGGTTACCTGCCGTGAGTAAATTAAAATTTTATTGACTTAGGTCACTAAATACTTTAACCAA
TATAGGCATAGCGCACAGACAGATAAAAATTACAGAGTACACAACATCCATGAAACGCAT
Como la respuesta es texto (no JSON), usamos .text en lugar de .json(). Y como .text es una cadena normal, puedes guardarla en un archivo tal como viste en el módulo 10:
with open("genoma_ecoli.fasta", "w") as f:
f.write(respuesta.text)Consultar una API JSON: Ensembl
La mayoría de APIs modernas responden en JSON. Ensembl, por ejemplo, devuelve la información de un gen a partir de su identificador:
import requests
url = "https://rest.ensembl.org/lookup/id/ENSG00000139618"
respuesta = requests.get(url, params={"content-type": "application/json"})
print(respuesta.status_code)
datos = respuesta.json()
print(datos["display_name"])200
BRCA2
requests.get() hace la petición. .status_code es el código de estado HTTP: 200 significa “todo correcto”; los códigos que empiezan por 4 indican un problema con tu petición, como 404 (“no encontrado”) o 400 (“petición incorrecta”, que es lo que devuelve Ensembl si el ID no existe). Los que empiezan por 5, como 500, indican un problema del servidor: no es culpa tuya, y suele bastar con volver a intentarlo más tarde. .json() convierte el texto de la respuesta directamente en un diccionario de Python, listo para usar como cualquier otro.
Comprueba siempre el status_code
Antes de asumir que .json() va a funcionar, comprueba que la petición salió bien — el mismo hábito de if/else del módulo 9, aplicado a peticiones de red:
if respuesta.status_code == 200:
datos = respuesta.json()
print(datos["display_name"])
else:
print(f"Error {respuesta.status_code}: no se pudo obtener el gen")Si la petición falla, la respuesta no contiene los datos que esperas: intentar acceder a datos["display_name"] provocaría un error confuso. Comprobar status_code primero te da un mensaje claro sobre qué ha ido mal. Prueba, por ejemplo, con el ID inexistente ENSG00000000000:
Error 400: no se pudo obtener el gen
Guardar la respuesta en un archivo
Ya sabes escribir archivos (módulo 10). Para JSON, en vez de escribir el texto crudo, usa el módulo json de la librería estándar, que se encarga del formato correctamente:
import json
with open(f"gene_{datos['id']}.json", "w") as f:
json.dump(datos, f, indent=2)json.dump(datos, f, indent=2) escribe el diccionario datos en el archivo f, con una indentación de 2 espacios para que sea legible si lo abres con gedit.
Otro ejemplo: ocurrencias de una especie en GBIF
GBIF (Global Biodiversity Information Facility) da acceso a millones de registros de observaciones de especies. Su respuesta trae los resultados dentro de una lista, bajo la clave "results", junto con el número total de coincidencias en "count":
import requests
url = "https://api.gbif.org/v1/occurrence/search"
parametros = {"scientificName": "Quercus robur", "country": "ES", "limit": 5}
respuesta = requests.get(url, params=parametros)
if respuesta.status_code == 200:
datos = respuesta.json()
print(f"Registros totales encontrados: {datos['count']}")
for registro in datos["results"]:
print(registro.get("stateProvince"), registro.get("eventDate"))Como no todos los registros tienen todos los campos, usamos registro.get("stateProvince") en vez de registro["stateProvince"]: si la clave no existe, .get() devuelve None en lugar de dar un error.
Ejercicios
Para estos ejercicios necesitas conexión a internet y ejecutarlos en tu ordenador (no en el navegador).
- Haz una petición a
https://rest.ensembl.org/info/pingcon el parámetrocontent-typeigual aapplication/json. Imprime elstatus_codey el contenido de la respuesta como texto. - Descarga con
requestslos primeros 200 pares de bases del genoma de E. coli (NC_000913.3) desde NCBI, pasando los parámetros conparams. Guárdalos engenoma_ecoli.fastae imprime solo la primera línea (la cabecera). - Modifica el ejercicio anterior para descargar los primeros 500 pares de bases. Sin guardar el archivo, calcula cuántas bases tiene la secuencia descargada (sin contar la cabecera ni los saltos de línea) y su contenido en GC.
- Consulta Ensembl para el gen
ENSG00000139618, comprueba elstatus_codey muestra sudisplay_name, suspeciesy su longitud en pares de bases (end - start + 1). - Amplía el ejercicio anterior para que guarde la respuesta completa en
gene_ENSG00000139618.jsonusando el módulojson(con indentación). - Escribe una función
nombre_gen(id_gen)que consulte Ensembl y devuelva eldisplay_namedel gen, oNonesi la petición falla. Úsala en un bucle sobre la lista["ENSG00000139618", "ENSG00000141510", "ENSG00000000000"]e imprime, para cada ID, su nombre o un mensaje de error. - Consulta GBIF con
scientificName="Pinus sylvestris"ycountry="ES", y muestra cuántos registros totales hay (datos["count"]) y la comunidad autónoma ("stateProvince") de los 5 primeros resultados. - (Reto) Para cada especie de la lista
["Pinus sylvestris", "Quercus ilex", "Lynx pardinus", "Aquila adalberti"], consulta en GBIF cuántos registros hay en España y guarda el resultado en un diccionario{especie: número_de_registros}. Después, imprime la especie con más registros. Pista: con"limit": 0GBIF devuelve elcountsin descargar ningún registro.
Soluciones
import requests respuesta = requests.get("https://rest.ensembl.org/info/ping", params={"content-type": "application/json"}) print(respuesta.status_code) print(respuesta.text)import requests url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi" parametros = {"db": "nuccore", "id": "NC_000913.3", "rettype": "fasta", "retmode": "text", "seq_stop": 200} respuesta = requests.get(url, params=parametros) if respuesta.status_code == 200: with open("genoma_ecoli.fasta", "w") as f: f.write(respuesta.text) print(respuesta.text.split("\n")[0]) else: print(f"Error {respuesta.status_code}")import requests url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi" parametros = {"db": "nuccore", "id": "NC_000913.3", "rettype": "fasta", "retmode": "text", "seq_stop": 500} respuesta = requests.get(url, params=parametros) secuencia = "" for linea in respuesta.text.split("\n"): if not linea.startswith(">"): secuencia += linea.strip() num_gc = secuencia.count("G") + secuencia.count("C") print(f"Longitud: {len(secuencia)} pb") print(f"Contenido GC: {num_gc / len(secuencia) * 100:.2f} %")import requests url = "https://rest.ensembl.org/lookup/id/ENSG00000139618" respuesta = requests.get(url, params={"content-type": "application/json"}) if respuesta.status_code == 200: datos = respuesta.json() longitud = datos["end"] - datos["start"] + 1 print(datos["display_name"], datos["species"], f"{longitud} pb") else: print(f"Error {respuesta.status_code}")import json if respuesta.status_code == 200: with open(f"gene_{datos['id']}.json", "w") as f: json.dump(datos, f, indent=2)import requests def nombre_gen(id_gen): """Devuelve el display_name de un gen de Ensembl, o None si falla.""" url = f"https://rest.ensembl.org/lookup/id/{id_gen}" respuesta = requests.get(url, params={"content-type": "application/json"}) if respuesta.status_code == 200: return respuesta.json()["display_name"] return None for id_gen in ["ENSG00000139618", "ENSG00000141510", "ENSG00000000000"]: nombre = nombre_gen(id_gen) if nombre is None: print(f"{id_gen}: no encontrado") else: print(f"{id_gen}: {nombre}")import requests url = "https://api.gbif.org/v1/occurrence/search" parametros = {"scientificName": "Pinus sylvestris", "country": "ES", "limit": 5} respuesta = requests.get(url, params=parametros) if respuesta.status_code == 200: datos = respuesta.json() print(f"Registros totales encontrados: {datos['count']}") for registro in datos["results"]: print(registro.get("stateProvince"))import requests url = "https://api.gbif.org/v1/occurrence/search" especies = ["Pinus sylvestris", "Quercus ilex", "Lynx pardinus", "Aquila adalberti"] registros = {} for especie in especies: parametros = {"scientificName": especie, "country": "ES", "limit": 0} respuesta = requests.get(url, params=parametros) if respuesta.status_code == 200: registros[especie] = respuesta.json()["count"] print(registros) especie_max = None for especie, n in registros.items(): if especie_max is None or n > registros[especie_max]: especie_max = especie print(f"Especie con más registros: {especie_max} ({registros[especie_max]})")