S'ha acabat la teoria. En aquesta lliçó, Papyrus obre la seva primera porta al món: instal·laràs Flask, escriuràs una aplicació web completa en cinc línies i, en obrir el navegador, veuràs el catàleg de l'Anna — L'Odissea, Hamlet, El Quixot, Faust — servit per HTTP des de la teva pròpia màquina. Pel camí descobriràs que ja coneixies la peça central de Flask sense saber-ho (@app.route és un decorador dels de 08-02), aprendràs a generar HTML amb plantilles Jinja2 en lloc de concatenar cadenes, i construiràs un cercador que reutilitza, sense tocar-ne ni una línia, el cercar_llibre() que vas escriure a M3 i vas blindar amb tests a M9. Aquest és el missatge profund de la lliçó: la web és una capa nova sobre el codi de sempre.
Contingut
- Instal·lació a l'entorn virtual
- L'aplicació mínima: cinc línies, línia a línia
@app.routeés un decorador (i tu ja saps què vol dir això)- El servidor de desenvolupament i el perill de
debug=True - Rutes amb variables:
/llibre/<titol> - Plantilles Jinja2: HTML amb forats
- Herència de plantilles:
base.html - Fitxers estàtics: un CSS mínim
- El cercador: un formulari GET sobre
cercar_llibre() - Estructura de projecte raonable
Instal·lació a l'entorn virtual
Com qualsevol paquet des de M1: entorn virtual activat, pip install:
Comprova la instal·lació amb flask --version. Treballarem en una carpeta papyrus_web_flask/ que conté, al costat del nou codi web, el paquet papyrus/ de M3–M9 tal qual i el seu dades/cataleg.json. No copiïs funcions: copia (o enllaça) el paquet sencer; la suite de pytest de M9 continua passant i continua protegint-te.
L'aplicació mínima: cinc línies, línia a línia
Crea app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def inici():
return "<h1>Papyrus, llibreria de barri</h1><p>Benvingut.</p>"Cada línia mereix la seva explicació:
from flask import Flask— importa la classe central del framework.app = Flask(__name__)— crea l'aplicació.__name__(M3: val"__main__"o el nom del mòdul) diu a Flask on és el teu codi, perquè sàpiga localitzar les carpetestemplates/istatic/relatives a ell.@app.route("/")— registra la funció per a la ruta/(l'arrel del lloc). És la peça màgica i la desmuntem a la secció següent.def inici():— una funció Python normal i corrent. En argot Flask s'anomena vista (view).return "<h1>...</h1>"— el que la vista retorna es converteix en el cos de la resposta HTTP, amb codi200 OKautomàtic. El navegador rep aquest HTML i el pinta.
Això és una aplicació web sencera: rep peticions GET / i respon HTML. Compara-la mentalment amb la lliçó 10-01: Flask està fent de fontaner HTTP (parsejar la petició, muntar la resposta, capçaleres, codi d'estat) perquè tu només escriguis la funció.
@app.route és un decorador (i tu ja saps què vol dir això)
Atura't a @app.route("/"). Aquesta sintaxi és exactament la dels decoradors amb arguments de la lliçó 08-02: una funció que rep la teva funció i la registra o l'embolcalla. Sense sucre sintàctic, el codi equival a:
def inici():
return "<h1>Papyrus...</h1>"
inici = app.route("/")(inici) # el que fa el @ per sota (08-02)app.route("/") no executa la teva vista: l'apunta en una taula interna — "quan arribi una petició a /, crida inici()" — i te la retorna intacta. És el patró decorador de registre que vas veure a 08-02, el mateix que feia servir @pytest.fixture a M9. Quan al seu dia et vas preguntar per a què servien els decoradors més enllà de cronometrar(): per a això. Frameworks sencers s'hi construeixen al damunt.
flowchart LR
A["GET /llibre/Hamlet"] --> B{"Taula de rutes de Flask"}
B -->|"/"| C["inici()"]
B -->|"/llibre/<titol>"| D["fitxa_llibre('Hamlet')"]
B -->|"sense coincidència"| E["404 automàtic"]
D --> F["return HTML → resposta 200"]
El servidor de desenvolupament i el perill de debug=True
Per arrencar l'aplicació hi ha dues maneres equivalents. La moderna, des del terminal:
O afegint al final d'app.py el bloc if __name__ == "__main__": de M3:
i executant python app.py. En tots dos casos veuràs alguna cosa com:
Obre http://127.0.0.1:5000 al navegador: aquí tens la teva pàgina. 127.0.0.1 és la teva pròpia màquina (localhost) i 5000 el port — les peces d'URL de 10-01. El mode debug et dona dos regals durant el desenvolupament: recàrrega automàtica en desar canvis (adeu a rearrencar a mà) i pàgines d'error amb el traceback complet (M7) al navegador, fins i tot amb una consola interactiva.
I aquí hi ha també el perill, dit sense embuts: debug=True mai en un servidor accessible des de fora. Aquesta consola interactiva del navegador executa codi Python arbitrari al teu servidor: per a un atacant és una porta oberta de bat a bat. A més, aquest servidor de desenvolupament no està pensat per a trànsit real; en producció es fa servir un servidor WSGI com gunicorn darrere de nginx — ho esmentem aquí per honestedat i ho tocarem de passada a 10-05, però el desplegament en producció queda fora d'aquest curs.
Rutes amb variables: /llibre/<titol>
El catàleg té quatre llibres i no escriuràs quatre vistes. Les rutes admeten parts variables entre angles, que Flask extreu i passa com a argument:
from papyrus.magatzem import carregar_cataleg, cercar_llibre
cataleg = carregar_cataleg("dades/cataleg.json")
@app.route("/llibre/<titol>")
def fitxa_llibre(titol):
llibre = cercar_llibre(cataleg, titol)
if llibre is None:
return "<p>No tenim aquest llibre.</p>", 404
return (f"<h1>{llibre.titol}</h1>"
f"<p>Preu: {llibre.preu:.2f} € — "
f"soci: {llibre.preu_final(soci=True):.2f} €</p>"
f"<p>Estoc: {llibre.estoc}</p>")Punts clau:
GET /llibre/Hamletexecutafitxa_llibre("Hamlet"): la variable de la ruta es converteix en el paràmetre de la funció, amb el mateix nom.- Reutilització pura:
carregar_cataleg,cercar_llibreipreu_finalsón els de M5–M6, intactes. La vista només tradueix entre HTTP i el teu paquet. Hamlet respon9.95 € — soci: 9.83 €, els números que la teva suite de M9 vigila. - Retornar una tupla
(cos, codi)fixa el codi d'estat: aquí, el404de 10-01 quan el llibre no existeix. (A 10-03 farem aquesta traducció de manera més elegant ambobtenir_llibrei les seves excepcions.) - Es pot exigir tipus:
/llibre/<int:posicio>només casaria amb enters. Per a títols ens val el conversor per defecte (text).
Plantilles Jinja2: HTML amb forats
Construir HTML amb f-strings escala fatal i és perillós (si un títol contingués <script>, l'injectaries tal qual a la pàgina). La solució del framework són les plantilles: fitxers HTML amb forats que Flask omple amb les teves dades fent servir el motor Jinja2. Van a la carpeta templates/, germana d'app.py.
templates/inici.html:
<h1>Catàleg de Papyrus</h1>
<table>
<tr><th>Títol</th><th>Preu</th><th>Preu de soci</th><th>Estoc</th></tr>
{% for llibre in llibres %}
<tr>
<td><a href="/llibre/{{ llibre.titol }}">{{ llibre.titol }}</a></td>
<td>{{ "%.2f"|format(llibre.preu) }} €</td>
<td>{{ "%.2f"|format(llibre.preu_final(soci=True)) }} €</td>
<td>{% if llibre.hi_ha_estoc() %}{{ llibre.estoc }}{% else %}Esgotat{% endif %}</td>
</tr>
{% endfor %}
</table>I la vista es redueix a lliurar les dades:
from flask import render_template
@app.route("/")
def inici():
return render_template("inici.html", llibres=list(cataleg.values()))La gramàtica de Jinja2 té dos delimitadors i amb ells n'hi ha prou per a gairebé tot:
| Sintaxi | Què és | Exemple |
|---|---|---|
{{ expressió }} |
Forat: s'avalua i s'imprimeix | {{ llibre.titol }} |
{% instrucció %} |
Lògica: bucles i condicionals, no imprimeix | {% for %}...{% endfor %}, {% if %} |
| `{{ valor | filtre }}` | Filtre: transforma abans d'imprimir |
Dins dels forats pots accedir a atributs (llibre.estoc) i cridar mètodes (llibre.hi_ha_estoc()) dels teus objectes: les plantilles veuen les teves dataclasses de M5 directament. Cada argument amb nom de render_template (aquí llibres=...) es converteix en una variable disponible a la plantilla — el context. I el regal de seguretat: Jinja2 escapa l'HTML automàticament als {{ }}; un títol maliciós amb <script> es mostraria com a text inofensiu.
Herència de plantilles: base.html
Totes les pàgines de Papyrus compartiran capçalera, menú i peu. Repetir-los a cada plantilla seria el mateix pecat que repetir codi a M3. Jinja2 ho resol amb herència — sí, el concepte és cosí del de M5: una plantilla base defineix l'estructura i blocs sobreescrivibles.
templates/base.html:
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="utf-8">
<title>{% block titol %}Papyrus{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='estils.css') }}">
</head>
<body>
<header><a href="/">Papyrus</a> — llibreria de barri</header>
<main>{% block contingut %}{% endblock %}</main>
<footer>Papyrus · Anna i socis · des de M1</footer>
</body>
</html>I inici.html passa a heretar i omplir només els seus blocs:
{% extends "base.html" %}
{% block titol %}Catàleg — Papyrus{% endblock %}
{% block contingut %}
<h1>Catàleg de Papyrus</h1>
<table> ... la taula d'abans ... </table>
{% endblock %}{% extends %} ha de ser la primera línia; cada {% block %} de la filla substitueix l'homònim de la base. Afegir ara una pàgina nova costa cinc línies i surt uniformada.
Fitxers estàtics: un CSS mínim
El CSS, les imatges i la resta de fitxers que se serveixen tal qual van a static/. A base.html ja l'hem enllaçat amb url_for('static', filename='estils.css') — url_for construeix URLs a partir de noms, millor que escriure-les a mà. Un static/estils.css mínim perquè Papyrus no sembli de 1996:
body { font-family: Georgia, serif; max-width: 52rem; margin: 2rem auto; }
header, footer { color: #666; border-bottom: 1px solid #ddd; padding: .5rem 0; }
table { border-collapse: collapse; width: 100%; }
td, th { border: 1px solid #ddd; padding: .4rem .8rem; text-align: left; }No és un curs de CSS (allò promès a 10-01 continua en peu): amb això n'hi ha prou.
El cercador: un formulari GET sobre cercar_llibre()
La pàgina d'inici necessita el cercador que la Júlia farà servir des del mòbil. A inici.html, dins del bloc de contingut:
<form action="/cercar" method="get">
<input type="text" name="titol" placeholder="Títol...">
<button type="submit">Cerca</button>
</form>És el formulari GET de 10-01: en cercar "odissea" el navegador demanarà GET /cercar?titol=odissea. Per llegir aquesta query string, Flask ofereix l'objecte request:
from flask import request
@app.route("/cercar")
def cercar():
terme = request.args.get("titol", "").strip()
llibre = cercar_llibre(cataleg, terme)
return render_template("cercar.html", terme=terme, llibre=llibre)request.argsés un diccionari (M4) amb els paràmetres de la query string;.get()amb valor per defecte evita elKeyErrorsi algú visita/cercarsense paràmetre — reflex defensiu de M7.- De nou, la lògica és la de sempre:
cercar_llibre()retornaLlibre | None(M8 hi va posar aquest type hint), i la plantilla decideix què pintar:
{% extends "base.html" %}
{% block contingut %}
{% if llibre %}
<h1>{{ llibre.titol }}</h1>
<p>{{ "%.2f"|format(llibre.preu) }} € — en queden {{ llibre.estoc }}</p>
{% else %}
<p>Cap resultat per a «{{ terme }}». Prova amb un altre títol.</p>
{% endif %}
{% endblock %}Amb això, la web de Papyrus versió 1 està completa: portada amb el catàleg i cercador, i fitxa per llibre.
Estructura de projecte raonable
Per a una aplicació d'aquesta mida, aquesta disposició és idiomàtica i t'acompanyarà fins a 10-03:
papyrus_web_flask/ ├── .venv/ # entorn virtual (M1) — fora del control de versions ├── app.py # l'aplicacio Flask: rutes i vistes ├── papyrus/ # EL PAQUET DE M3-M9, sense canvis │ ├── models.py # Llibre, preu_final, hi_ha_estoc │ ├── magatzem.py # cercar_llibre, vendre, carregar_cataleg... │ ├── errors.py # ErrorPapyrus i familia │ └── cupons.py ├── dades/ │ ├── cataleg.json │ └── papyrus.log ├── templates/ │ ├── base.html │ ├── inici.html │ └── cercar.html ├── static/ │ └── estils.css └── tests/ # la suite de M9 continua vigilant
La regla d'or de la separació: app.py no calcula preus ni toca estoc; només tradueix HTTP ↔ paquet papyrus. Si demà canvies l'IVA, toques models.py, la suite de M9 ho verifica, i la web ni se n'assabenta. Quan app.py creixi, Flask ofereix blueprints per trossejar-lo; en tindràs prou de saber que existeixen.
Errors Comuns i Consells
TemplateNotFound: inici.html: la carpeta s'ha de dir exactamenttemplates/i ser al costat d'app.py(per aixòFlask(__name__)). Mateix remei per als estàtics ambstatic/.- Edites i el navegador no canvia: o no vas arrencar amb
--debug(sense recàrrega automàtica), o és la memòria cau del navegador — recarrega ambCtrl+F5. Address already in use: has deixat un altre servidor corrent al 5000. Tanca'l, o arrenca en un altre port:flask --app app run --debug --port 5001.- Retornar objectes directament:
return llibrefalla — una vista retorna text/HTML (o JSON, a 10-03), no unLlibre. La plantilla és qui presenta l'objecte. - Lògica de negoci a les vistes: si et descobreixes calculant descomptes a
app.py, atura't. Això pertany apapyrus/, on està testejat. La vista demana, el paquet resol, la plantilla pinta. - Oblidar el 404: si
cercar_llibreretornaNonei no ho comproves, la plantilla petarà amb un error críptic. Tracta sempre el cas buit — M7 et va entrenar exactament per a això.
Exercicis
Exercici 1: pàgina "Qui som"
Afegeix la ruta /socis amb una plantilla socis.html que hereti de base.html i mostri una llista <ul> amb els socis de Papyrus generada amb {% for %} a partir d'una llista de tuples passada des de la vista: [("Lluis", "LLUIS-001"), ("Marta", "MARTA-002"), ("Pau", "PAU-003")].
Exercici 2: preu de soci sota demanda
Modifica la vista cercar perquè accepti un segon paràmetre opcional de query string, soci (/cercar?titol=faust&soci=si). Si val "si", la plantilla ha de mostrar el preu de soci (preu_final(soci=True)) en lloc del normal. Comprova que Faust mostra 21.00 € sense el paràmetre i 20.75 € amb ell.
Exercici 3: fitxa amb plantilla
La vista fitxa_llibre d'aquesta lliçó encara retorna f-strings. Refactoritza-la perquè faci servir una plantilla llibre.html que hereti de base.html, mostri títol, preu, preu de soci i estoc (amb «Esgotat» si hi_ha_estoc() és fals), i retorni la tupla (render_template("no_trobat.html", titol=titol), 404) quan el llibre no existeixi.
Solucions
Exercici 1:
@app.route("/socis")
def socis():
llista = [("Lluis", "LLUIS-001"), ("Marta", "MARTA-002"), ("Pau", "PAU-003")]
return render_template("socis.html", socis=llista){% extends "base.html" %}
{% block contingut %}
<h1>Els socis de Papyrus</h1>
<ul>
{% for nom, codi in socis %}
<li>{{ nom }} — carnet {{ codi }}</li>
{% endfor %}
</ul>
{% endblock %}El desempaquetatge nom, codi al for de Jinja2 funciona com el de Python (M4).
Exercici 2:
@app.route("/cercar")
def cercar():
terme = request.args.get("titol", "").strip()
es_soci = request.args.get("soci") == "si"
llibre = cercar_llibre(cataleg, terme)
return render_template("cercar.html", terme=terme,
llibre=llibre, es_soci=es_soci)A la plantilla: {{ "%.2f"|format(llibre.preu_final(soci=es_soci)) }} €. Nota: preu_final(soci=False) retorna el preu amb IVA de M5, així que "sense paràmetre" mostra el preu de venda habitual; amb soci=si, Faust baixa a 20.75 — un dels quatre números canònics de la suite de M9.
Exercici 3:
@app.route("/llibre/<titol>")
def fitxa_llibre(titol):
llibre = cercar_llibre(cataleg, titol)
if llibre is None:
return render_template("no_trobat.html", titol=titol), 404
return render_template("llibre.html", llibre=llibre)llibre.html reutilitza els mateixos forats que ja vas escriure a cercar.html; no_trobat.html poden ser tres línies amb «No tenim «{{ titol }}»». L'essencial: el codi 404 viatja al costat de la plantilla dins de la tupla, perquè els clients (i els cercadors) sàpiguen que la pàgina no existeix.
Conclusió
Papyrus ja és una aplicació web: flask run, i el catàleg sencer — amb preus de soci calculats pel preu_final() de M5 i vigilats pels tests de M9 — se serveix a http://127.0.0.1:5000 amb portada, cercador i fitxa per llibre. Has après que @app.route és un decorador de registre dels de 08-02, que el mode debug és or en desenvolupament i verí en producció, que les rutes amb <variables> converteixen URLs en arguments de funció, i que Jinja2 (forats {{ }}, lògica {% %}, herència amb base.html) genera HTML segur a partir dels teus propis objectes. Sobretot, has comprovat el principi rector del mòdul: la capa web només tradueix; la lògica continua al paquet papyrus, intacta i testejada. Però aquesta web parla HTML, l'idioma dels navegadors. L'app mòbil que la Júlia somia per reservar llibres no vol taules HTML: vol dades — JSON net, codis d'estat precisos, rutes predictibles. Això és una API REST, i construir-la amb Flask, connectant LlibreNoTrobatError amb el 404 i asdict() amb jsonify, és exactament la propera lliçó.
Curs de Programació en Python
Mòdul 1: Introducció a Python
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
