Imperativo, declarativo y funcional
Pyzed no obliga a una forma de escribir una app. Un handler puede ser un método, un mensaje o una función que devuelve un estado nuevo, y las tres se mezclan en el mismo programa. Esta página escribe la misma lista de tareas en cada estilo, para que puedas compararlos línea a línea, y dice cuándo encaja cada uno.
| Estilo | El estado vive en | Un evento | Encaja en |
|---|---|---|---|
| Imperativo | Atributos de la app (self.tasks). |
Llama a un método que los cambia (on_submit=self.add). |
Apps pequeñas, prototipos, formularios diseñados. |
| Declarativo | Atributos de la app. | Produce un mensaje (on_submit=Add()); un único update() cambia el estado. |
Apps con muchas formas de hacer lo mismo, registro de acciones, deshacer, tests sin ventana. |
| Funcional | Un único valor inmutable (State). |
Pide un estado nuevo (set_state(draft="")); la vista es una función pura. |
Lógica que quieres probar como funciones normales, funciones de vista reutilizables, caché con @pz.memo. |
En los tres la vista es declarativa: view() describe la ventana entera a partir del estado y Pyzed averigua qué
cambió. Los estilos se diferencian en cómo llegan los eventos al estado.
La misma app de tres formas
Sección titulada «La misma app de tres formas»"""The same to-do list three ways (1 of 3): imperative.
The state is plain attributes of the app and every event calls a method that changes them.See docs/guides/code-styles.md; the other two are declarative.py and functional.py.
python examples/code_styles/imperative.py"""
import pyzed as pzfrom pyzed import ui
class Todo(pz.App): title = "Tasks" size = (420, 360)
def __init__(self): self.tasks = [{"title": "Write the docs", "done": False}] self.draft = "" self.filter = "All"
# -- event handlers: each one changes the state -------------------------- def type(self, text): self.draft = text
def add(self): if self.draft.strip(): self.tasks.append({"title": self.draft.strip(), "done": False}) self.draft = ""
def toggle(self, task, done): task["done"] = done
def remove(self, task): self.tasks.remove(task)
# -- the view reads the state ---------------------------------------------- def view(self): shown = [t for t in self.tasks if self.filter == "All" or t["done"] == (self.filter == "Done")] return pz.column([ ui.input(self.draft, self.type, placeholder="New task", on_submit=self.add), ui.segmented(["All", "To do", "Done"], self.filter, lambda f: setattr(self, "filter", f)), *[pz.row([ ui.checkbox(t["done"], lambda done, t=t: self.toggle(t, done), label=t["title"]), pz.hspace(), ui.icon_button("trash-2", lambda t=t: self.remove(t), size="sm"), ], align="center") for t in shown], ], spacing=8, padding=16)
if __name__ == "__main__": pz.run(Todo)"""The same to-do list three ways (2 of 3): declarative.
Every event is a message (a small dataclass) and one ``update()`` decides what each one does.See docs/guides/code-styles.md; the other two are imperative.py and functional.py.
python examples/code_styles/declarative.py"""
from dataclasses import dataclass
import pyzed as pzfrom pyzed import ui
# -- the messages: everything that can happen ------------------------------------@dataclassclass Type: text: str
@dataclassclass Add: pass
@dataclassclass Toggle: index: int done: bool
@dataclassclass Remove: index: int
@dataclassclass Filter: name: str
class Todo(pz.App): title = "Tasks" size = (420, 360)
def __init__(self): self.tasks = [{"title": "Write the docs", "done": False}] self.draft = "" self.filter = "All"
# -- one place decides how each message changes the state ------------------ def update(self, message): match message: case Type(text): self.draft = text case Add() if self.draft.strip(): self.tasks.append({"title": self.draft.strip(), "done": False}) self.draft = "" case Toggle(index, done): self.tasks[index]["done"] = done case Remove(index): del self.tasks[index] case Filter(name): self.filter = name
# -- the view only describes the window and the messages ------------------- def view(self): rows = [(i, t) for i, t in enumerate(self.tasks) if self.filter == "All" or t["done"] == (self.filter == "Done")] return pz.column([ ui.input(self.draft, Type, placeholder="New task", on_submit=Add()), ui.segmented(["All", "To do", "Done"], self.filter, Filter), *[pz.row([ ui.checkbox(t["done"], lambda done, i=i: Toggle(i, done), label=t["title"]), pz.hspace(), ui.icon_button("trash-2", Remove(i), size="sm"), ], align="center") for i, t in rows], ], spacing=8, padding=16)
if __name__ == "__main__": pz.run(Todo)"""The same to-do list three ways (3 of 3): functional.
The state is one frozen dataclass, the view and the logic are pure functions, and the only classkeeps the current state. See docs/guides/code-styles.md; the other two are imperative.py anddeclarative.py.
python examples/code_styles/functional.py"""
from dataclasses import dataclass, replace
import pyzed as pzfrom pyzed import ui
@dataclass(frozen=True)class Task: title: str done: bool = False
@dataclass(frozen=True)class State: # the whole state, immutable tasks: tuple = (Task("Write the docs"),) draft: str = "" filter: str = "All"
# -- pure functions: they change nothing, they return new values -----------------def added(tasks, title): return tasks + (Task(title.strip()),) if title.strip() else tasks
def toggled(tasks, index, done): return tuple(replace(t, done=done) if i == index else t for i, t in enumerate(tasks))
def removed(tasks, index): return tasks[:index] + tasks[index + 1:]
def visible(tasks, filter): return [(i, t) for i, t in enumerate(tasks) if filter == "All" or t.done == (filter == "Done")]
# -- the view: a pure function of the state ---------------------------------------def task_row(task, on_toggle, on_remove): # a function component return pz.row([ ui.checkbox(task.done, on_toggle, label=task.title), pz.hspace(), ui.icon_button("trash-2", on_remove, size="sm"), ], align="center")
def view(state, set_state): return pz.column([ ui.input(state.draft, lambda text: set_state(draft=text), placeholder="New task", on_submit=lambda: set_state(tasks=added(state.tasks, state.draft), draft="")), ui.segmented(["All", "To do", "Done"], state.filter, lambda f: set_state(filter=f)), *[task_row(t, lambda done, i=i: set_state(tasks=toggled(state.tasks, i, done)), lambda i=i: set_state(tasks=removed(state.tasks, i))) for i, t in visible(state.tasks, state.filter)], ], spacing=8, padding=16)
# -- the only class: it keeps the current state ------------------------------------class Todo(pz.App): title = "Tasks" size = (420, 360) state = State()
def set_state(self, **changes): self.state = replace(self.state, **changes)
def view(self): return view(self.state, self.set_state)
if __name__ == "__main__": pz.run(Todo)Ejecuta cualquiera con python examples/code_styles/imperative.py (o declarative.py, functional.py): las tres
ventanas se ven y funcionan igual.
Imperativo: los métodos cambian el estado
Sección titulada «Imperativo: los métodos cambian el estado»Cada handler es un método o una lambda, y cambia self. Después, Pyzed vuelve a llamar a view().
def add(self): if self.draft.strip(): self.tasks.append({"title": self.draft.strip(), "done": False}) self.draft = ""
ui.input(self.draft, self.type, on_submit=self.add)ui.checkbox(t["done"], lambda done, t=t: self.toggle(t, done), label=t["title"])- Un handler que no recibe argumentos se llama sin nada (
on_press,on_submit); el de un control que produce un valor lo recibe (on_input(texto),on_toggle(hecho),on_select(elemento)). - Para pasar tus propios datos, átalos con un argumento por defecto:
lambda t=t: self.remove(t). Sint=t, todas las lambdas del bucle verían la última tarea. - Es el estilo más corto, y el que usan los handlers de un
formulario diseñado:
form.status.text = "Guardado".
Declarativo: mensajes y un único update()
Sección titulada «Declarativo: mensajes y un único update()»Los handlers son valores, los mensajes, que van a update(message). Un mensaje dice qué pasó; update decide
qué significa.
@dataclassclass Type: text: str
@dataclassclass Toggle: index: int done: bool
ui.input(self.draft, Type, on_submit=Add()) # Type(texto) se construye con el texto nuevoui.checkbox(t["done"], lambda done, i=i: Toggle(i, done))
def update(self, message): match message: case Type(text): self.draft = text case Toggle(index, done): self.tasks[index]["done"] = done- Un mensaje es cualquier valor que no se puede llamar: un texto (
"save"), una tupla (("delete", 3)) o, mejor, una dataclass pequeña, que se lee bien enmatch. - Los controles con valor llaman a lo que les das con ese valor. Una clase dataclass se puede llamar, así que
on_input=TypeconstruyeType(texto)y lo manda aupdate. Para eventos sin valor pasa una instancia:on_submit=Add(). - También vale una función que devuelve un mensaje:
lambda done, i=i: Toggle(i, done). updatepuede devolver unCommand(enfocar, cerrar, abrir una ventana) o lanzar trabajo asíncrono, como cualquier handler.- Como todos los cambios pasan por un método, puedes registrar los mensajes, guardar un historial para deshacer, repetirlos o probar la lógica sin abrir una ventana:
app = Todo()app.update(Type("Comprar leche"))app.update(Add())assert app.tasks[-1]["title"] == "Comprar leche"Los mensajes también llegan desde fuera de la vista: atajos (on_key), temporizadores (pz.every(1, Tick())), menús e
hilos (pz.post(Downloaded(...))). Mira Arquitectura de una app.
Funcional: un estado inmutable y funciones puras
Sección titulada «Funcional: un estado inmutable y funciones puras»El estado es una única dataclass congelada. La vista es una función view(state, set_state) que solo lo lee, y la
lógica vive en funciones que reciben datos y devuelven datos nuevos:
@dataclass(frozen=True)class State: tasks: tuple = () draft: str = ""
def added(tasks, title): # pura: sin self, sin efectos return tasks + (Task(title.strip()),) if title.strip() else tasks
def view(state, set_state): return ui.input(state.draft, lambda text: set_state(draft=text), on_submit=lambda: set_state(tasks=added(state.tasks, state.draft), draft=""))
class Todo(pz.App): # la única clase: guarda el estado actual state = State()
def set_state(self, **changes): self.state = replace(self.state, **changes)
def view(self): return view(self.state, self.set_state)- Nada cambia en el sitio:
set_state(...)construye unStatenuevo condataclasses.replace. Usa tuplas yfrozenseten lugar de listas y conjuntos. - Las funciones puras se prueban sin esfuerzo:
assert added((), "Leche") == (Task("Leche"),). - Las funciones de vista son tus componentes:
task_row(task, on_toggle, on_remove)recibe datos y callbacks y devuelve widgets, como las funciones depyzed.ui. Como sus argumentos son valores simples, se pueden cachear con@pz.memo, que solo las reconstruye cuando cambian.
Para un reductor puro (al estilo de Elm o Redux), combínalo con mensajes: update sustituye el estado por lo que
devuelve una función pura.
def reduce(state: State, message) -> State: match message: case Type(text): return replace(state, draft=text) case Add(): return replace(state, tasks=added(state.tasks, state.draft), draft="") return state
class Todo(pz.App): state = State()
def update(self, message): self.state = reduce(self.state, message)
def view(self): return view(self.state) # la vista solo manda mensajesManejar el estado en clases
Sección titulada «Manejar el estado en clases»Viva donde viva el estado, las mismas reglas hacen que una app sea predecible:
| Regla | Por qué |
|---|---|
Pon el estado inicial en __init__ (o en atributos de clase si son valores inmutables). |
Un atributo de clase que es una lista o un dict lo comparten todas las instancias; __init__ le da a cada app el suyo. |
No cambies nunca el estado dentro de view(). |
view() se ejecuta tras cada evento; calcula ahí lo que vas a mostrar, nada más. |
Calcula en view() los valores derivados en lugar de guardarlos. |
shown = [t for t in self.tasks if ...] nunca se queda desfasado. |
| Guarda los datos en Python normal: dataclasses, dicts, listas. | Pyzed redibuja con lo que tengas; no hay observables que envolver. |
| Sustituye las listas en lugar de modificarlas cuando una parte de la vista está cacheada. | @pz.memo compara argumentos; self.tasks = [*self.tasks, t] es un valor nuevo. |
El estado de una ventana va en su Window; el compartido, en la App (self.app). |
Cada ventana tiene su view() y lee la app a través de self.app. |
El estado de una pieza reutilizable va en un pz.Component (pz.state(...)). |
Cada instancia guarda el suyo, y el padre solo ve sus props y sus eventos. |
Un componente tiene dentro las mismas tres opciones: métodos que cambian self, self.emit(...) para avisar al padre o
un render() que solo lee sus props. Mira Apps, ventanas, formularios y componentes.
Mezclar estilos
Sección titulada «Mezclar estilos»La mayoría de las apps reales los mezclan: handlers imperativos para el estado pequeño de la interfaz (un menú abierto, la fila bajo el puntero), mensajes para las acciones que importan (guardar, borrar, sincronizar) y funciones puras para la lógica y las partes reutilizables de la vista.
class Editor(pz.App): def __init__(self): self.menu_open = False # detalle de la interfaz: imperativo self.doc = Document()
def update(self, message): # acciones: mensajes match message: case Save(): return save(self.doc) # función pura, devuelve un Command
def view(self): return pz.column([ toolbar(self.doc.name, on_save=Save()), # una función de vista ui.icon_button("menu", lambda: setattr(self, "menu_open", True)), ])Cada página de la biblioteca muestra los eventos de su componente en los tres estilos, en Eventos → Manejar los eventos.