--- weight: 4 title: "Apuntes de Python" date: 2020-05-29T12:35:51+0100 draft: false summary: "Notas sobre python" categories: - notes tags: - python - programacion --- {{< admonition type=warning title="Work in progress" open=true >}} Apuntes de Python, muy incompletos. {{< /admonition >}} ## Gestores de paquetes **pip** : El gestor de paquetes por defecto de python **pipx** : Un instalador pensado para aplicaciones CLI en Python, se encarga de instalar cada aplicación en un _virtualenv_ independiente. : Cosas instaladas con `pipx` en mi sistema son por ejemplo: `pypisearch`, `ranger` o `kikit` : Los _virtualenv_ creados por **pipx** quedan en `~/.local/pipx/venvs` **conda** : El instalador de las distribuciones _Anaconda_ y _Miniconda_ ## Gestores de entornos virtuales En cualquier distribución de Linux disponemos de un _Python del Sistema_, es el _Python_ que viene instalado por defecto con el S.O. (podemos averiguar cual es ejecutando: `which python` y/o `python -V`) En mi caso concreto, usando [Linux Mint](https://linuxmint.com/) el Python del sistema es el 2.7.18. Una versión anticuada, pero que es mejor no tocar ya que habrá partes del sistema operativo que dependan de ella. Podemos tener más versiones de Python instaladas en nuestro S.O. (yo ahora mismo tengo disponibles `python3`, `python3.8` y `python3.9`) pero solo una de ellas es el _Python del Sistema_, el Python que se ejecuta por defecto. Instalar paquetes de Python en el _Python del Sistema_ **no es nada recomendable**. Si instalamos versiones incompatibles de paquetes o actualizamos versiones instaladas podemos perturbar el funcionamiento de alguna parte del propio sistema operativo. Desinstalar paquetes en Python siempre es una aventura (a menudo imposible) así que si tenemos problemas la solución puede ser extremadamente complicada (a mi ya me tocó reinstalar Linux entero por enredar sin saber). {{< admonition type=tip title="Módulos Python que si que instalo" open=true >}} Hay algunos paquetes, como veremos en el siguiente punto, que si que nos conviene tenerlos siempre instalados. Son justo los que nos facilitan la gestión de los _virtualenvs_, como por ejemplo `venv`, `virtualenv` o `pipx`. {{< /admonition >}} Además de todo lo anterior es muy probable que la versión del *Python del Sistema* no sea la versión que queremos usar con nuestro proyecto. O podemos estar interesados en usar varias versiones diferentes de *Python*, ya sea para desarrollo o para ejecutar distintas aplicaciones. Un gestor de entorno python nos permitirá: 1. Instalar python en un entorno aislado 2. Instalar multiples versiones de python; cada una en su propio entorno, de forma que no interfieran entre si 3. Instalar distintas combinaciones de paquetes 4. Cambiar de versión de python sin más que cambiar de entorno ### virtualenv y virtualenvwrapper _virtualenv_ es probablemente la herramienta más básica para crear entornos virtuales para Python y _virtualenvwrapper_ es un _frontend_ que facilita mucho su uso. La idea es muy simple, se crea una instalación de Python independiente en un directorio dedicado y se modifica el `PATH` para que ese Python sea prioritario. _virtualenvwrapper_ centraliza todos los entornos virtuales (es decir los directorios con los distintos python), en un único directorio de entornos (especificado con `$WORKON_HOME`) En el Python 3 que viene con mi Linux Mint yo instalo: ```bash sudo apt install python-all-dev sudo apt install python3-all-dev sudo apt install virtualenv virtualenvwrapper python3-virtualenv ``` En el fichero `~/.profile` añadimos: ```bash # WORKON_HOME for virtualenvwrapper if [ -d "$HOME/.virtualenvs" ] ; then WORKON_HOME="$HOME/.virtualenvs" fi ``` Además en `zsh` uso el bundle `virtualenvwrapper` y tengo parcheado el _prompt_ del tema del zsh y el _prompt_ del _zsh-git-prompt_ (Ver [dotfiles](https://gitlab.com/salvari/usrcfg_legion_ulyana)) para visualizar en el _prompt_ el entorno virtual activo (si hay alguno activado claro) Este _bundle de zsh_ también se encarga de activar automáticamente el entorno virtual si cambiamos a un directorio de proyecto con el mismo nombre (se considera directorio de proyecto si está controlado por git). Aunque también hay formas de asociar un directorio de proyecto al crear un entorno virtual, como veremos. ```bash #------------------- # virtualenv virtualenv # Crea el entorno como un directorio en el # directorio donde lo hemos invocado (es típico llamarlo .env) source env/bin/activate # Activar el entorno deactivate # desactivar el entorno #------------------- # virtualenvwrapper mkvirtualenv -p /usr/bin/python3 # Crear un entorno virtual y activarlo mkvirtualenv -p /usr/bin/python3 -a # Crear un entorno virtual y asociar el directorio de proyecto deactivate # Salir del entorno virtual lsvirtualenv # ver la lista de entornos virtuales workon # activar un entorno ya definido rmvirtualenv # borrar un entorno virtual cpvirtualenv # copiar un entorno virtual lssitepackages # listar paquetes instalados en el entorno activo ``` ### pyenv {{< admonition type=danger title="Importante" open=true >}} No confundir con `pyenv` con `pyvenv` {{< /admonition >}} [_pyenv_](https://github.com/pyenv/pyenv#readme) es mucho más potente que _virtualenv_ y además podemos integrar tanto _virtualenv_ como _virtualenvwrapper_ con _pyenv_. Mi ciclo de trabajo con _pyenv_: 1. Instalar versiones de Python en mi máquina con _pyenv_ 1. Crear entornos virtuales basados en esas versiones, con el plugin _virtualenv_ de _pyenv_ 1. Asignar los entornos virtuales a proyectos (normalmente con `pyenv local ` Instalamos las dependencias en el sistema: ```bash sudo apt-get update sudo apt-get install --no-install-recommends make build-essential \ libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \ wget curl llvm libncurses5-dev xz-utils tk-dev \ libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev ``` Y a continuación instalamos el `pyenv-bundle` en _Antigen_, añadiendo la linea: `antigen bundle mattberther/zsh-pyenv` a nuestro fichero `~/.zshrc` La instalación que hace el plugin de `zsh` es muy completa, instala _pyenv_ con todos los plugins. Evidentemente este método de instalación solo vale si usas `zsh`, [aquí](https://github.com/pyenv/pyenv-installer) tienes una alternativa (que no he probado) o puedes currarte una instalación siguiendo las instrucciones oficiales en [el github del proyecto pyenv](https://github.com/pyenv/pyenv#installation) `zsh-pyenv` se encargará de la instalación de `pyenv` Tenemos que asegurarnos de dejar configurada la activación de `pyenv` en nuestros fichero `.profile` y `.zshrc` (o `.bashrc`) En `~/.profile` añadimos las siguientes lineas (las lineas comentadas se dejan como referencia por que ya lo configura el plugin de zsh): ```bash # pyenv if [ -d "$HOME/.pyenv" ] ; then export PYENV_ROOT="$HOME/.pyenv" # export PATH="$PYENV_ROOT/bin:$PATH" # eval "$(pyenv init --path)" fi ``` En `~/.zshrc` **no debería** ser necesario añadir nada por que se supone que el plugin de _oh-my-zsh_ se encarga de activar todo _pyenv_, pero en mi instalación el autocompletado de comandos de _pyenv_ solo funciona si añado este código: ```bash # pyenv activation if command -v pyenv 1>/dev/null 2>&1; then eval "$(pyenv init -)" fi ``` Si quieres echar un ojo a las versiones de Python que puedes instalar en tu sistema ejecuta `pyenv install --list` (hay 490 versiones en el momento de escribir esto) ```bash pyenv commands # La lista de comandos disponibles (¡son muchos!) pyenv --help # Ayuda para un comando pyenv install --list # Versiones de python disponibles pyenv versions # Versiones de python instaladas en nuestro sistema pyenv global # Establece la version de python global pyenv local # Establece la versión de python local pyenv shell # Establece la versión para el shell pyenv virtualenv # Crea un entorno virtual basado en la versión especificada ``` ### pyenv global Me he creado un entorno virtual para usar como _pyenv global_: * Creamos un entorno basado en la versión oficial de Python más reciente (3.9.6 al escribir esto) * En el nuevo entorno instalamos las últimas versiones de _pip_, _setuptools_, _pipx_, _virtualenv_ y _virtualenvwrapper_ ```bash pyenv virtualenv 3.9.6 ve_sys39 pyenv which python /home/salvari/.pyenv/versions/ve_sys39/bin/python pip install --upgrade pip pip install --upgrade setuptools pip install --upgrade pipx pip install --upgrade virtualenv virtualenvwrapper ``` {{< admonition type=danger >}} Parece que los plugins `pyenv` y `virtualenvwrapper` de `oh-my-zsh` no son compatibles. El plugin de `oh-my-zsh` para _virtualenvwrapper_ intenta activar el _virtualenvwrapper_ haciendo un `source virtualenvwrapper.sh`, pero ahora tenemos un _shimm_ que intercepta la llamada y no funciona correctamente, de hecho **aborta el terminal**. No parece aconsejable activar simultaneamente los dos plugins así que el plugin de `virtualenvwrapper` queda desactivado en el fichero _.zshrc_ {{< /admonition >}} Para poder activar el _virtualenvwrapper_ correctamente via el shimm, añadimos el siguiente alias: ```bash # virtualenvwrapper activation alias myvw='source `pyenv which virtualenvwrapper.sh`' ``` Esto funcionará correctamente siempre y cuando en el python activo tengamos instalado _virtualenvwrapper_ claro está. ### pyenv y emacs **Referencias** * Una referencia muy completa [Getting started with lsp-mode for Python](https://www.mattduck.com/lsp-python-getting-started.html) En _Emacs_ usamos [LSP]^(Language Server Protocol). Para Python necesitamos instalar [python-lsp-server](https://pypi.org/project/python-lsp-server/) **Mucho ojo**, no hay que instalar Palantir ([python-language-server](https://pypi.org/project/python-language-server/)), que se considera obsoleta. Con el siguiente comando instalamos en nuestro Python activo (por ejemplo el _Python global_) el servidor `pylsp` y todos sus _providers_ (ver el [github del proyecto](https://github.com/python-lsp/python-lsp-server)) ```bash pip install 'python-lsp-server[all]' ``` Si queremos que nuestro editor Emacs edite código Python a plena potencia necesitaremos instalar este paquete en el Python activo del proyecto en el que trabajemos. Para simplificar la creación de nuevos entornos creamos el alias `myve` que podemos invocar una vez creado nuestro nuevo entorno virtual para instalar los paquetes mínimos: ```bash myve(){ if [ `pyenv which python` = "/usr/bin/python" ] then echo "This is main python. Abort installation" else echo "OK, looks like you are in some venv" pip install --upgrade pip setuptools wheel pipx virtualenv virtualenvwrapper pip install 'python-lsp-server[all]' fi } ``` ### pyenv y conda Ojo, por que los entornos virtuales de Anaconda y Miniconda parece que hay que crearlos con `conda` para que funcionen bien. (ver [artículo](https://www.soudegesu.com/en/python/pyenv/anaconda/)). Supongamos que tenemos instalada la versión `anaconda3-2021.11`. Ejecutaríamos: ```bash pyenv shell anaconda3-2021.11 conda info -e conda create -n py4web_env python=3.7 conda info -e # Con esto hemos creado un virtualenv basado en Python 3.7 con conda, podemos activarlo con pyenv pyenv activate anaconda3-2021.11/envs/py4web_env ``` ### pyenv y poetry PENDIENTE DE MIRAR ### pyenv y pip-tools PENDIENTE DE MIRAR ## Instalando paquetes en Python __Muy importante leerse el tutorial oficial [Installing Packages](https://packaging.python.org/en/latest/tutorials/installing-packages/)__ ### Instalando un paquete desde el shell de python ```python ``` ## Modulos en Python {{< admonition type=info title="Referencias" open=false >}} - [Estructura mínima](https://python-packaging.readthedocs.io/en/latest/minimal.html) - [Estructura tu proyecto](https://docs.python-guide.org/writing/structure/) - [Paquetes y módulos](https://python-course.eu/python-tutorial/packages.php) - [Módulos (oficial)](https://docs.python.org/3/tutorial/modules.html) {{< /admonition >}} Todos los lenguajes de programación modernos soportan la programación modular. Cualquier programa minimamente complicado hace uso de "módulos" que encapsulan diferentes funcionalidades. En Python cualquier fichero con código Python puede considerarse un módulo. Podríamos acceder a cualquier código de cualquier fichero mediante un `import` (hay muchas formas de hacer el `import`) Por ejemplo si en un programa hacemos un `import math` podremos acceder a los simbolos (`math.pi`) o funciones (`math.sin(math.pi/2)`) definidos en ese módulo desde nuestro código (usando el prefijo del módulo que es `math.` en este ejemplo) ### Diferentes tipos de import ### Packages (Paquetes) El siguiente nivel de modularidad en Python es el `package`. Cuando una aplicación o biblioteca se complica y empieza a tener un gran número de módulos podemos organizarlos en paquetes (`package`) Basicamente un `package` es un directorio en el que dejamos módulos (ficheros con código Python) pero cumpliendo ciertas condiciones. ## Exceptions in python ~~~python try: dom_rq = MD.parse(os.path.join(d, 'request_{}.xml'.format(t))) dom_rs = MD.parse(os.path.join(d, 'response_{}.xml'.format(t))) except Exception as e: print('ERROR in some xml file {}'.format(e)) print(sys.exc_info()[0]) return None ~~~ ## Reverse string in Python ~~~python y = x[::-1] ~~~ ## Minidom - Acceder a un atributo ~~~python req_hotel_avail = dom_rq.getElementsByTagNameNS('*', 'OTA_HotelAvailRQ') if(req_hotel_avail): try: test = req_hotel_avail[0].getAttribute('xmlns') print(test) except Exception as e: print('Error {}'.format(e)) ~~~ - Acceder a la lista de atributos ~~~python req_hotel_avail = dom_rq.getElementsByTagNameNS('*', 'OTA_HotelAvailRQ') if(req_hotel_avail): try: test = req_hotel_avail[0].attributes.items() print(test) except Exception as e: print('Error {}'.format(e)) ~~~ ## tkinter _tkinter_ (_Tk interface_) es una biblioteca de Python para usar el _Tcl/Tk GUI Toolkit_. Un conjunto de herramientas clásico para desarrollar interfaces gráficos de usuario, y que está disponible en practicamente todas las distribuciones de *nix, incluyendo macOS, también hay disponibles versiones para Windows. Es bastante probable que _tkinter_ esté incluido en tu Python de sistema (el python que venga por defecto en tu Linux), o que se haya instalado como dependencia de otros programas de tu Linux lo puedes probar rápidamente sin más que ejecutar desde el terminal: ```bash python >>> import tkinter >>> tkinter._test() >>> ``` Deberías ver una ventana gráfica con la versión de _Tcl/Tk_ (en mi linux es la 8.6) y un par de botones de demostración (con el botón `Quit` terminas el programa de demo) En el caso de que no esté instalado te fallará el `import`, si lo quieres instalar en el python de sistema en Debian y derivadas el comando debería ser algo parecido al siguiente comando, que es el que hay que ejecutar en Ubuntu y derivadas: `sudo apt install python3-tk` ### Aprendiendo _tkinter_ Creamos un _virtualenv_: ```bash mkvirtualenv tkinter ``` Probamos que todo va bien ```python #!/usr/bin/env python import tkinter tkinter._test() ``` Vamos a echar un ojo a un programa completo ```python #!/usr/bin/env python # Importamos tkinter entero en nuestro espacio # Opcionalmente podriamos hacer # import tkinter as tk from tkinter import * from tkinter import ttk # Esta linea importa el submodulo ttk, # un conjunto de widgets incorporado en 8.5 def calculate(*args): try: value = float(feet.get()) meters.set(int(0.3048 * value * 10000.0 + 0.5) / 10000.0) except ValueError: pass # Creamos la ventana raiz y le ponemos un título root = Tk() root.title("Feet to Meters") # Creamos un 'frame' para el contenido, # esto es importante para usar widgets de un tema # grid, columnconfigure y rowconfigure determinan # el comportamiento cuando se cambia el tamaño de # la pantalla mainframe = ttk.Frame(root, padding="3 3 12 12") mainframe.grid(column=0, row=0, sticky=(N, W, E, S)) root.columnconfigure(0, weight=1) root.rowconfigure(0, weight=1) # Creamos un widget de tipo 'Entry' que será hijo de 'mainframe' # o sea está dentro del marco principal feet = StringVar() feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet) feet_entry.grid(column=2, row=1, sticky=(W, E)) meters = StringVar() ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W, E)) ttk.Button(mainframe, text="Calculate", command=calculate).grid(column=3, row=3, sticky=W) ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W) ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E) ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W) for child in mainframe.winfo_children(): child.grid_configure(padx=5, pady=5) feet_entry.focus() root.bind("", calculate) root.mainloop() ``` Lo mismito pero con OOP ```python from tkinter import * from tkinter import ttk class FeetToMeters: def __init__(self, root): root.title("Feet to Meters") mainframe = ttk.Frame(root, padding="3 3 12 12") mainframe.grid(column=0, row=0, sticky=(N, W, E, S)) root.columnconfigure(0, weight=1) root.rowconfigure(0, weight=1) self.feet = StringVar() feet_entry = ttk.Entry(mainframe, width=7, textvariable=self.feet) feet_entry.grid(column=2, row=1, sticky=(W, E)) self.meters = StringVar() ttk.Label(mainframe, textvariable=self.meters).grid(column=2, row=2, sticky=(W, E)) ttk.Button(mainframe, text="Calculate", command=self.calculate).grid(column=3, row=3, sticky=W) ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W) ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E) ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W) for child in mainframe.winfo_children(): child.grid_configure(padx=5, pady=5) feet_entry.focus() root.bind("", self.calculate) def calculate(self, *args): try: value = float(self.feet.get()) self.meters.set(int(0.3048 * value * 10000.0 + 0.5)/10000.0) except ValueError: pass root = Tk() FeetToMeters(root) root.mainloop() ``` ### widgets * Todos los widgets tienen un padre (excepto la ventana _root_) * Todos los widgets son de alguna clase [Widget roundup](https://tkdocs.com/widgets/index.html) * Las opciones de un widget controlan su comportamiento (incluida su visualización) Evidentemente dependen también de cual es su clase. Se intenta que los nombres de los atributos sean consistentes entre clases. * Puedes ver toda la configuración de un widget con `button.configure()` * Puedes cambiar una opción con `button['text']=newvalue` o con `button.configure(text=newvalue)` Con este programilla podemos ver toda la jerarquía de widgets ```python def print_hierarchy(w, depth=0): print(' '*depth + w.winfo_class() + ' w=' + str(w.winfo_width()) + ' h=' + str(w.winfo_height()) + ' x=' + str(w.winfo_x()) + ' y=' + str(w.winfo_y())) for i in w.winfo_children(): print_hierarchy(i, depth+1) print_hierarchy(root) ``` ### Gestión de la geometría _grid_ es más moderno que _pack_ **[usa _grid_](https://www.geeksforgeeks.org/python-grid-method-in-tkinter/)** ### Event handling * Los eventos son tratados por el _event handling loop_ que no debe ser bloqueado. * Las cosas interesantes se consiguen asociando _command callbacks_ a eventos ```python from tkinter import * from tkinter import ttk root = Tk() l =ttk.Label(root, text="Starting...") l.grid() l.bind('', lambda e: l.configure(text='Moved mouse inside')) l.bind('', lambda e: l.configure(text='Moved mouse outside')) l.bind('', lambda e: l.configure(text='Clicked left mouse button')) l.bind('<3>', lambda e: l.configure(text='Clicked right mouse button')) l.bind('', lambda e: l.configure(text='Double clicked')) l.bind('', lambda e: l.configure(text='right button drag to %d,%d' % (e.x, e.y))) root.mainloop() ``` #### Ejemplos ##### Un campo entrada de texto ```python #!/usr/bin/env python import tkinter as tk # Lo importamos con un alias para que quede claro que es de tk from tkinter import ttk # Esta linea importa el submodulo ttk, un conjunto de widgets incorporado en 8.5 '''Imprime el valor de la entrada de texto. Y lo muestra en una etiqueta''' class App(): def __init__(self): self.root = tk.Tk() self.root.title("Práctica 2.2") self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) self.frm_root = ttk.Frame(self.root, padding="3 3 12 12") self.frm_root.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) self.frm_entry = ttk.Frame(self.frm_root, borderwidth=2, relief='sunken', padding="3 3 12 12") self.frm_entry.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) self.var_entry = tk.StringVar() self.ent_text = ttk.Entry(self.frm_entry, textvariable=self.var_entry) self.ent_text.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) self.frm_label = ttk.Frame(self.frm_root, borderwidth=2, relief='sunken', padding="3 3 12 12") self.frm_label.grid(column=0, row=1, sticky=(tk.N, tk.W, tk.E, tk.S)) self.lbl_showEntry = ttk.Label(self.frm_label, text="No hay valor leido") self.lbl_showEntry.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) self.frm_button = ttk.Frame(self.frm_root, padding="3 3 12 12") self.frm_button.grid(column=1, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) self.but_show = ttk.Button(self.frm_button, text="Print entry", command=lambda: self.update_print()) self.but_show.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) self.root.mainloop() def update_print(self): print(self.var_entry.get()) self.lbl_showEntry['text'] = f"Valor leido: {self.var_entry.get()}" app = App() ``` ##### _Checkbuttons_ ```python #!/usr/bin/env python import tkinter as tk # Lo importamos con un alias para que quede claro que es de tk from tkinter import ttk # Esta linea importa el submodulo ttk, un conjunto de widgets incorporado en 8.5 '''Imprime el valor de la entrada de texto. Y lo muestra en una etiqueta''' class App(): def __init__(self): self.root = tk.Tk() self.root.title("Práctica 2.3") self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) # Creamos el frame ppal en la ventana root self.frm_root = ttk.Frame(self.root, padding="3 3 12 12") self.frm_root.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) # Creamos un frame para los checks dentro del frm_root self.frm_checks = ttk.Frame(self.frm_root, padding="3 3 12 12") self.frm_checks.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) # Creamos cuatro checks con sus variables para almacenar estado # Les metemos algo de padding para que no queden apelotonados self.var_check1 = tk.IntVar(value=0) self.chk_check1 = ttk.Checkbutton(self.frm_checks, text='Check 1', variable=self.var_check1, padding="3 3 12 12", command=self.update_chk1) self.var_check2 = tk.IntVar(value=0) self.chk_check2 = ttk.Checkbutton(self.frm_checks, text='Check 2', variable=self.var_check2, padding="3 3 12 12", command=self.update_chk2) self.var_check3 = tk.IntVar() self.chk_check3 = ttk.Checkbutton(self.frm_checks, text='Check 3', variable=self.var_check3, padding="3 3 12 12", command=self.update_chk3) self.var_check4 = tk.IntVar() self.chk_check4 = ttk.Checkbutton(self.frm_checks, text='Check 4', variable=self.var_check4, padding="3 3 12 12", command=self.update_chk4) # Colocamos los checks en su frame self.chk_check1.grid(column=0, row=0) self.chk_check2.grid(column=0, row=1) self.chk_check3.grid(column=1, row=0) self.chk_check4.grid(column=1, row=1) # Creamos un frame para el botón self.frm_button = ttk.Frame(self.frm_root, padding="3 3 12 12") self.frm_button.grid(column=1, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) # Creamos el boton y lo colocamos self.btn_readChecks = ttk.Button(self.frm_button, text="Read checks", command=lambda: self.update_status()) self.btn_readChecks.grid(column=0, row=0) # Vamos a crear un par de frames más para eventos y para status # En vez de usar Frame usamos LabelFrame para presumir self.frm_events = ttk.LabelFrame(self.frm_root, padding="3 3 12 12", text='Events') self.frm_status = ttk.LabelFrame(self.frm_root, padding="3 3 12 12", text='Status') # Y los colocamos en su sitio self.frm_events.grid(column=0, row=1) self.frm_status.grid(column=1, row=1) # Y por ultimo los mensajes de eventos y de status self.lbl_events = ttk.Label(self.frm_events, text="Sin eventos") self.lbl_status = ttk.Label(self.frm_status, text="Sin status") # Los colocamos como siempre self.lbl_events.grid(column=0, row=0) self.lbl_status.grid(column=0, row=0) # Last but not less lanzamos el bucle para atender a todos los eventos self.root.mainloop() # Lo que sigue son los métodos que actualizan las etiquetas de eventos # y status, lo de tener cuatro métodos iguales queda feo, pero no da tiempo a mas def update_chk1(self): if (self.var_check1.get()): self.lbl_events['text'] = 'Set check1' else: self.lbl_events['text'] = 'Unset check1' def update_chk2(self): if (self.var_check2.get()): self.lbl_events['text'] = 'Set check2' else: self.lbl_events['text'] = 'Unset check2' def update_chk3(self): if (self.var_check3.get()): self.lbl_events['text'] = 'Set check3' else: self.lbl_events['text'] = 'Unset check3' def update_chk4(self): if (self.var_check4.get()): self.lbl_events['text'] = 'Set check4' else: self.lbl_events['text'] = 'Unset check4' def update_status(self): self.lbl_status[ 'text'] = f"Check1 = {self.var_check1.get()} Check2 = {self.var_check2.get()} \ Check3 = {self.var_check3.get()} Check4 = {self.var_check4.get()}" app = App() ``` ## Estudio * [Análisis de un ransomware](https://www.welivesecurity.com/la-es/2020/07/29/analisis-codigo-fuente-ransomware-escrito-python/) * [Pomodoro en python](https://medium.com/@fidel.esquivelestay/build-a-pomodoro-timer-using-python-d52509730f60) * [Tkinter en RealPython](https://realpython.com/python-gui-tkinter/) * [Another Tkinter tutorial](https://tkdocs.com/tutorial/index.html) * [Tercer tutorial para Tkinter](https://www.geeksforgeeks.org/python-tkinter-tutorial/) ## Create http server Comparte los ficheros de la carpeta ```python import os from http.server import HTTPServer, CGIHTTPRequestHandler # Make sure the server is created at current directory os.chdir('.') # Create server object listening the port 80 server_object = HTTPServer(server_address=('', 80), RequestHandlerClass=CGIHTTPRequestHandler) # Start the web server server_object.serve_forever() ``` ## Decorators * [Codeship: Python decorators](https://www.thecodeship.com/patterns/guide-to-python-function-decorators/) * [Python Decorators Explained](https://jeffknupp.com/blog/2013/11/29/improve-your-python-decorators-explained//) ## Static method vs Classmethod * [Static methods](https://stackoverflow.com/questions/136097/difference-between-staticmethod-and-classmethod#1669524) * [More on static methods](https://stackabuse.com/pythons-classmethod-and-staticmethod-explained/) ## Referencias * [Python3 OOP (realpython)](https://realpython.com/python3-object-oriented-programming/) ## Curso de Python ### Info do curso Sabados 11.00 - 13.00 horas - @caligari (é mais de práctica) - @salvari (é mais de teoría) Estructura do curso - Teoría e práctica - Práctica "de deberes" - Preguntas dúbidas e tertulia Dinámica do curso: - Non se graba - Non hai transparencias, o profe teórico é un vago (tamén intenta ser presumido e impaciente) - [Apuntes colaborativos](https://pad.disroot.org/p/cafe-con-gotas-de-python) - Iremos incorporando ao libro exemplos e notas que vexamos útiles ### Bricolabs [Bricolabs](https://bricolabs.cc/) ### Documentación do curso - Gitlab de Ekaitz: https://gitlab.com/ElenQ/Publishing/python/-/tree/master/es - Tradución ao galego (coordinador: Rafa Gaioso [Melisa](https://www.melisa.gal/)) - https://pad.disroot.org/p/curso_python_memoria.md ### Outros enlaces de interese * Itinerario formativo con ou sen mentorización: https://exercism.io/tracks/python ## Apuntes sesión 01 Algo de Historia Fai 30 anos... a explosión dos intérpretes: Perl : Larry Wall (1987) : Killer app: : - CGI perlcgi integrado no código de Apache. : - Linguaxe pegamento para sysadmins : - Perl Template Toolkit Python : Guido Van Rosum (1991) Benevolent Dictator for Life (https://en.wikipedia.org/wiki/Benevolent_dictator_for_life), un título moi usado no mundo hacker e inventado para Guido : Killer app: : - Zope, Plone, Django, Calibre, Web2py : - Integración con Gimp, con KiCAD, con FreeCAD, con .... : - Big Data, iPython (agora Jupyter), Machine Learning, Deep Learning PHP : Rasmus Lerdorf (1995) : Killer app: : - Wordpress : - Milleiros de aplicacións web Ruby : Matz Matsumoto (1995) : Killer app: : - Ruby on rails : Pouco coñecido en occidente pero super implantado en Asia ## Las tres virtudes del programador (según Larry Wall, creador do Perl) Pereza : _"La virtud que te hace esforzarte para reducir tu gasto global de energía. Hace que escribas programas que te ahorran trabajo y te hace documentar esos programas para que no tener que contestar preguntas acerca de como funcionan."_ Impaciencia : _"El ansia que sientes cuando el ordenador remolonea. Esto hace que escribas programas que no sólo satisfacen tus necesidades si no que se anticipan a ellas. O al menos lo intentan."_ Orgullo : _"La virtud que te hace escribir (y mantener) programas intachables, de los que el resto de la gente no pueda hablar mal."_ O lema de Perl: ___TIMTOWTDI "There is more than one way to do it"___, ou sexa, hai moitas formas de facelo (e ademais eso mola moito) A resposta de Python (e decir, de Guido, que é colega de Larry) ___"There should be one — and preferably only one — obvious way to do it."___ Normalmente hai unha forma obvia e óptima de facelo (é iso mola moito mais de cara a manter o software) Qué é python? * Linguaxe de programación * Interpretado non compilado Pros * Simplicidade * Versatilidade facilidade de uso e desenrolo rápido * Software libre e aberto, cunha comunidade moi activa * Un conxunto de bibliotecas enorme e super completo * Excelente para prototipos (código compacto) Limitacións o Desventaxas do Python * Limitacións de velocidade (é interpretado e iso sempre penaliza a velocidade) * Problemas có Threading (estou empezando a programar, non me importa) * Non ten implantación destacable en dispositivos móbiles: Android e IOS (pero mira Kivy se estás moi interesado neste campo) Pingas de Python: * Terminal vitaminado para python: `bpython3` ou `ptpython` * Axuda interactiva: `help('string')` * Saber ruta do intérprete en linux: `which python` ou `which python3` * Script, empeza coa línea _shebang_, para decirlle que intérprete usar ```bash #!/ruta al intérprete ``` Ou tamén ```bash #!/usr/bin/env python3 ``` * Ver "posición da memoria" (en realidade `id` devolve un identificativo único) da variable ```python hex(id(a)) ``` * saber o tipo dunha variable: `type(variable)` Mutabilidad: * A tupla e inmutable * A lista e mutable ### Estilo de programación * Readability counts (the zen of python) * El código se lee mucho más a menudo de lo que se escribe * [Reglas de estilo pep8](https://www.python.org/dev/peps/pep-0008/) * [Reglas de docstring (los comentarios de documentación) pep257](https://www.python.org/dev/peps/pep-0257/) ### Tipos de datos ```text TIPOS | |- Numéricos | |- Enteros (int) | |- Coma flotante (float) | |- Complejos | |- Texto | |- Booleanos |- True (Verdadero) |- Falso (Falso) ``` ### Operadores ```text OPERADORES | |- Aritméticos | |- Suma + | |- Resta - | |- Multiplicación * | |- División / | |- División entera // | |- Módulo % | |- Exponente ** | |- Comparación | |- Igual que == | |- Diferente que != | |- Mayor que > | |- Menor que < | |- Mayor o igual que => | |- Menor o igual que <= | |- Lógicos | |- AND AND | |- OR OR | |- NOT NOT | |- Asignación | |- Igual = | |- Incremento += | |- ? *= | |- ? /= | |- ? %= | |- ? **= | |- ? //= | |- Especiales |- IS IS |- IS NOT IS NOT |- IN IN |- NOT IN NOT IN ``` ## Apuntes sesión 02 ### Bucle for El bucle `for` clásico se puede conseguir facilmente con la función `range()` ```python for i in range(10): print(i) ``` En los bucles `for` y `while` se pueden usar las palabras clave `continue`, `break` y `else`: ```python for i in (iterable): . . . continue # salta inmediatamente al siguiente bucle for . . break # interrumpe el bucle for y sale de el else: # Este bloque de ordenes se ejecuta si el for # se completa sin ejecutar el break ``` --- __OJO__ El `else` para bucles es una de las características más controvertidas de Python, el propio Guido dijo que nunca debió incluirla en la especificación del lenguaje, o al menos que se debió usar otra palabra que no fuera `else` --- Para un `while` sería lo mismo ```python while(condicion): . . . continue # salta al comienzo del siguiente bucle . . break # interrumpe el while y sale del bucle else: # Este bloque se ejecuta siempre que # la condición del while sea falsa # y no salgamos del mismo con break ``` Las sentencias de tratamiento de excepciones también tiene `else` y además una clausula `finally` ```python try: x = 1 except: print('Hay un erro, no se puede asignar x') else: print('No hay excepciones') finally: print('Siempre imprimimos esto') ``` ### Apuntes sesión 03 Sábado, 18/04/2020 Exercism Neste enlace tedes a páxina de exercism, onde vos podedes dar de alta (opcional) para facer o track de Python. https://exercism.io/tracks/python/exercises Se vos dades de alta en exercism, veredes que hai instruccións para instalar un cliente de linea (de linea de comandos) que permite descargar os exercicios e subir as solucións. E moi doado de instalar non vos preocupedes. (Guía de instalación) Lembrade traballar en un entorno virtual, se queredes instalar o módulo pytest que suxiren en exercism, facedeo dentro do voso virtualenv, poderíades facer algo como: ```bash mkdir curso cd curso python3 -m venv .venv source .venv/bin/activate pip install pytest ``` Lembrade o comentado no curso, agora estamos escribindo "Baby Python", non vos preocupedes do estilo ou se o facedes ben ou mal, o importante é que funcione. O voso python mellorará coa práctica e lendo código de outros. Lembrade tamén que haberá bibliotecas para facer todo tipo de cousas, é imposible coñecelas todas. Có tempo coñeceredes as que usedes mais frecuentemente, pero non serán moitas. Sempre hai que apoiarse na documentación a través de internet. Solución exercicio 1 (Adiviña o número) ```python #!/usr/bin/env python from random import randint sigue_xogando = True def es_valida(entrada): try: x = int(entrada) except ValueError as e: print('Tes que introducir un número enteiro entre o 1 e o 100') return False except Exception as e: print('Erro descoñecido, contacta co autor do programa') return False if (x > 100 or x < 1): print('Error, o número está fora do rango de 1 a 100') return False return True # Aquí empeza o programa principal while sigue_xogando: mi_numero = randint(1, 100) intentos = 0 finalizado = False while not finalizado: suposicion = input('Dime un número: ') if (es_valida(suposicion)): intentos += 1 if (mi_numero > int(suposicion)): print('O meu número e maior, Proba outra vez') elif (mi_numero < int(suposicion)): print('O meu número e menor. Proba outra vez') else: print('Acertaches!. O meu número é: ' + str(mi_numero)) print('E só necesitaches {} intentos'.format(intentos)) finalizado = True outra_vez = input('Queres xogar outra vez) [s/n]') if (outra_vez in ['N', 'n']): sigue_xogando = False ``` Solución exercicio 2: Simular tiradas de dados ```python #!/usr/bin/env python from sys import (argv, exit) from random import randint max_stars = 40 usage = '''Uso: dados.py numtiradas [dados] - numtiradas Parámetro obligatorio É o número de tiradas a simular, ten que ser un enteiro positivo. - dados Parámetro opcional E o número de dados que imos usar, ten que ser un enteiro maior que 2 ''' def check_parameters(): dados = 2 if (not (len(argv) in [2, 3])): raise SystemExit(usage) try: tiradas = int(argv[1]) if (len(argv) == 3): dados = int(argv[2]) except ValueError as e: raise SystemExit(usage) except Exception as e: print(type(e)) raise SystemExit(usage) return (tiradas, dados) # -------------------- main -------------------- tiradas, dados = check_parameters() # print('Simulando {} tiradas de {} dados'.format(tiradas, dados)) print(f'Simulando {tiradas} tiradas de {dados} dados') results = {i: 0 for i in range(dados, (6 * dados) + 1)} for i in range(tiradas): result = 0 for i in range(dados): result += randint(1, 6) results[result] += 1 padd = len(str(max(results.values()))) for i in sorted(results): print( f'{i:02}: [{str(results[i]).zfill(padd)}] {"*" * int(results[i] * max_stars / max(results.values()))}') ``` ## Apuntes sesión 04 Módulo : Escritos normalmente para ser importados Script : Escritos normalmente para ser ejecutados desde la linea de comandos Programa : Seguramente compuesto por varios módulos ### Importar módulos ```python import M # Hay que usar M.func para acceder a una f importada from M import f # No tenemos que calificar f para usarla from M import (f, g, h) # Podemos importar varios objetos del módulo al mismo tiempo ``` ### Scopes (LEGB) Estrictamente hablando Python recorre los siguientes `scopes` cuando busca un símbolo Local : Nombres asignados dentro de una función (también aplica a lambdas) y que no se preceden con la palabra clave global Enclosing-function : Nombres asignados dentro de cualquier función padre en una cadena de funciones anidadas. De dentro hacia fuera e incluye lambdas Global (a nivel de módulo) : Nombres asignados en el nivel superior de un modulo (fichero) de python. O asignados dentro de una función pero precedidos de la palabra clave global Built-in (Python) : Nombre preasignados en el built-in por el intérprete de Python. P.ej: open, range, SyntaxError, etc `global` y `nonlocal` Algunos namespaces: * Global * Módulos * Clases * Generadores * Función Utilizar `.get` para crear un contador cuando no has definido el número inicial del parámetro que quieres contar: `.get(loquesea, 0)` : Devuelve lo que sea si existe, y si no, devuelve 0. __palabrainterna__ ## Apuntes sesión 05 ### Flujo de programa ### lambdas Son funciones "telegráficas" que se usan a veces para acortar el código. En general las lambdas deberían ser de un solo uso, y en casos muy particulares donde necesitamos una función muy simple (casí siempre como parámetro para otra función o una _list comprehension_ Si te encuentras peleando para implementar una lambda compleja, es casi seguro que deberías dejarlo y emplear una función normal. ### Parámetros de funciones Tenemos que distinguir entre dos escenarios: antes de Python 3.8 y después de Python 3.8. Hasta ahora teníamos: Aquí tenemos una funcíon definida con tres parámetros, uno de ellos (c) con un valor por defecto Al llamar la función c es opcional puesto que tiene valor por defecto Cualquier parámetro lo podemos pasar por posición, pero todos los posicionales tienen que ir desde el principio. En cuanto pasemos un parámetro por nombre ya no podemos añadir ninguno posicional. Cualquier parámetro lo podemos pasar también por nombre, siempre dejando los posicionales (si los hay) al principio en su lugar. En cuanto pasemos un parámetro por nombre ya no podemos añadir ninguno posicional. ```python def fn (a, b, c = 1): # a y b obligatorios, c opcional ya que return a * b + c # tiene valor por defecto. print(fn(1, 2)) # devuelve 3, a y b son posicionales print(fn(1, 2, 3)) # devuelve 5, a, b y c son posicionales print(fn(c = 5, b = 2, a = 2)) # devuelve 9, pasamos todos "con nombre" print(fn(b = 2, a = 2)) # devuelve 5, pasamos a y b con nombre print(fn(5, c = 2, b = 1)) # devuelve 7, a es posicional, b y c con # nombre print(fn(8, b = 0)) # devuelve 1, a es posicional, # b lo pasamos con nombre y c toma el valor # por defecto ``` Si definimos la función como ```python def fn(a, b, c = 0, *args): ``` Sigue en pie la regla de que TODOS los parámetros posicionales tienen que ir al principio. Podemos pasar todos los parámetros posicionales que queramos, los tres primeros se asignan a: a, b y c respectivamente el resto se capturan en la tupla `args`. Por convención se emplea la tupla llamada `args`. Podríamos emplear cualquier otro nombre para la tupla, pero dificultaríamos la inteligibilidad del código por parte de otros programadores. ```python def fn(a, b, **kargs): ``` Esta función admite dos párametros posicionales, el resto de parámetros adicionales tienen que ser con nombre y quedan capturados en el diccionario `kargs` ```python def fn(a, b, *args, **kargs) ``` Esta función admite dos o mas parámetros posicionales, los dos primeros se asignan a `a` y `b`, el resto se capturan en la tupla `args`. También podemos meter todos los parámetros que queramos con nombre, se capturan en el diccionario `kargs` Desde Python versión 3.8 además tenemos dos separadores de paramétros `/` y `*` En el caso de que usemos los separadores: * A la izquierda de `/` son parámetros posicionales puros, no se puede llamarlos por nombre * A la derecha de `*` son parámetros por nombre obligatoriamente * Lo que va en el medio de los dos simbolos, pueden ser usados posicionalmente o por nombre ```python def f(a=2, /) # Parámetro posicional opcional # `def f(a, /)` # Parámetro posicional, obligatorio # def f(*, a=1) # Parámetro por nombre, opcional def f(*, a) # Parámetro por nombre, obligatorio def f(a=1) # Podemos pasar el parámetro por posición o por nombre y # además es opcional def f(a) # Podemos pasar el parámetro por posición o por nombre y # es obligatorio ``` --- __OJO: ¡No usar parámetros mutables en la definicion de las funciones!__ --- Algunos ejemplos de paso de parámetros con valor por defecto: Con valor por defecto inmutable. No tiene problemas. ```python def banner(message, border='-') line = border * len(message) print(line) print(message) print(line) ``` Con un valor que varía a lo largo de la ejecución del programa (problema: no va a variar) Pasamos como parámetro por defecto la hora actual, pero la llamada a ctime solo se ejecuta una vez al inicio del intérprete de python, no cada vez que llamamos a la función ~~~~python import time def show_default(arg=time.ctime()) print(arg) ~~~~ Pasamos como parámetro un valor mutable (muy mala idea, no lo hagais) Pasamos como valor por defecto la lista vacía, en la práctica pasar un valor por defecto equivale a crear un closure, la función mantiene su contexto a lo largo del ciclo de vida del programa con una referencia al valor por defecto del parámetro alist (que es una lista y por tanto mutable) Cada vez que llamamos a la función sin parámetros la función añade una nueva cadena 'uno' a la lista almacenada en el closure como "valor por defecto de alist" Si pasamos un parámetro, la cosa cambia, python crea una nueva lista en memoria y asigna la referencia a esa lista como parámetro de la función, con lo cual se ejecuta como esperamos. Al terminar la referencia a esa lista que pasámos como parámetro no se almacena en el closure de la función y si no hay otras referencias a ella en el programa será reclamada por el garbage colector. ~~~~python def return_list(alist=[]): alist.append('uno') return(alist) ~~~~ Solución al problema anterior (probada) ~~~~python def return_list(alist=None): if alist is None: alist = [] alist.append('uno') return(alist) ~~~~ ### Funciones pasadas como parámetros. Composición de funciones: ~~~~python def is_even(value): """Return True if *value* is even.""" return (value % 2) == 0 def count_occurrences(target_list, predicate): """Return the number of times applying the callable *predicate* to a list element returns True.""" return sum([1 for e in target_list if predicate(e)]) my_predicate = is_even my_list = [2, 4, 6, 7, 9, 11] result = count_occurrences(my_list, my_predicate) print(result) ~~~~ ### Funciones anidadas: ```python def parent(): print("Printing from the parent() function") a = 'uno' def first_child(): a = 'dos' print("Printing from the first_child() function") def second_child(): print("Printing from the second_child() function") b = 'dos' print(b) second_child() first_child() ``` ### Funciones anidadas y paso de funciones como parámetro ~~~~python def surround_with(surrounding): """Return a function that takes a single argument and.""" def surround_with_value(word): return '{}{}{}'.format(surrounding, word, surrounding) return surround_with_value def transform_words(content, targets, transform): """Return a string based on *content* but with each occurrence of words in *targets* replaced with the result of applying *transform* to it.""" result = '' for word in content.split(): if word in targets: result += ' {}'.format(transform(word)) else: result += ' {}'.format(word) return result markdown_string = 'My name is Jeff Knupp and I like Python but I do not own a Python' markdown_string_italicized = transform_words(markdown_string, ['Python', 'Jeff'], surround_with('*')) print(markdown_string_italicized) ~~~~ ### Closures (o cierres) Los closures son funciones que mantienen un "entorno" en el que "recuerdan" variables. En el ejemplo de abajo ~~~python def create_incrementer_function(increment): def incrementer (val): # Recuerda que esta función puede ver el valor `increment` por # por haber nacido en un contexto superior (en su func. padre) return val + increment return incrementer increment5 = create_incrementer(5) print(increment5(3)) # Imprimirá 8 por la pantalla ~~~ ### Decoradores ~~~python def my_decorator(func): def wrapper(*args, **kargs): print("Something is happening before the function is called.") func(*args, **kargs) print("Something is happening after the function is called.") return wrapper ~~~ ## OOP - Programación orientada a objetos La programación orientada a objetos se basa en cuatro principios básicos * Encapsulado * Abstracción * Herencia * Polimorfismo Código de la sesión 06: ~~~~python '''OOP sample code''' class Flight: '''A flight with a particular aircraft''' def __init__(self, number, aircraft): if not number[:2].isalpha(): raise ValueError(f'No airline code in {number}') if not number[:2].isupper(): raise ValueError(f'Invalid airline code in {number}') if not (number[2:].isdigit() and int(number[2:]) <= 9999): raise ValueError(f'Invalid route number in {number}') self._number = number self._aircraft = aircraft rows, seats = self._aircraft.seating_plan() self._seating = [None] \ + [{letter: None for letter in seats} for _ in rows] def number(self): return(self._number) def airline(self): return self._number[:2] def aircraft_model(self): return self._aircraft.model() def allocate_seat(self, seat, passenger): '''Allocates a seat for a passenger Args: seat: A seat designator such as '12C' or '21F' passenger: The passenger name Raises ValueError if seat is unavailable ''' rows, seat_letters = self._aircraft.seating_plan() letter = seat[-1] if letter not in seat_letters: raise ValueError(f'Invalid seat letter: {letter}') row_text = seat[:-1] try: row = int(row_text) except ValueError: raise ValueError(f'Invalid seat row: {row_text}') if row not in rows: raise ValueError(f'Invalid row number: {row}') if self._seating[row][letter] is not None: raise ValueError(f'Seat {seat} already occupied') self._seating[row][letter] = passenger class Aircraft: def __init__(self, registration, model, num_rows, num_seats_per_row): """Models an aircraft """ self._registration = registration self._model = model self._num_rows = num_rows self._num_seats_per_row = num_seats_per_row def registration(self): '''Returns aircraft registration code ''' return self._registration def model(self): '''Returns aircraft model ''' return self._model def seating_plan(self): '''Returns seating plan for aircraft, as a tuple of rows numbers (range) and seats letters (list)''' return (range(1, self._num_rows + 1), "ABCDEFGHJK"[:self._num_seats_per_row]) ~~~~ ## Docker y Python Podemos usar Docker para tener proyecto aislados en sus contenedores. Esto nos puede simplificar la vida a la hora de gestionar dependencias, versiones de Python o distintas configuraciones. ### Containers Vamos a crear un contenedor con una aplicación muy chorras. Para crear nuestro contenedor suponemos que creamos el directorio `app` en alguna parte de nuestro sistema de ficheros. ```bash mkdir -p app/src touch app/{requirements.txt,dockerfile} touch app/src/server.py tree app app ├── dockerfile ├── requirements.txt └── src └── server.py ``` El contenido del fichero `server.py` tendría esta pinta: ```python3 from flask import Flask server = Flask(__name__) @server.route("/") def hello(): return "Hello World!" if __name__ == "__main__": server.run(host='0.0.0.0') ``` Como requisito necesitamos tener `Flask` instalado así que en `requirements.txt` tedremos ```ini Flask==2.1.0 ``` Ahora vamos a definir como se construye la imagen Docker con nuestro `dockerfile`: ```dockerfile # set base image (host OS) FROM python:3.10.9 # set the working directory in the container WORKDIR /code # copy the dependencies file to the working directory COPY requirements.txt . # install dependencies RUN pip install -r requirements.txt # copy the content of the local src directory to the working directory COPY src/ . # command to run on container start CMD [ "python", "./server.py" ] ``` Nuestro `dockerfile`: - parte de la imagen oficial para Python 3.10.9 - define el directorio de trabajo `/code` (en el contenedor) - copia el fichero `requirements.txt` desde el host (nuestro ordenador) al directorio de trabajo del contenedor - copia el directorio `src/` al directorio de trabajo del contenedor - define el comando de arranque de los contenedores basados en esta imagen como `python ./server.py` Ya podemos construir nuestra imagen con el comando: `docker build -t myimage .` Y una vez construida (ver con `docker images`) podemos crear contenedores a partir de esta imagen: ```bash # Launch container mapping port 5000 in host docker run -d -p 5000:5000 myimage # Launch container and open terminal docker run -it myimage /bin/bash ``` ### Optimizando la construcción de imágenes Docker __Imagen Base__ : Por supuesto conviene usar __siempre__ imágenes oficiales : La imagen que escojamos como base determina muchas de las propiedades de la imagen final. Generalmente hay opciones para construir imágenes ligeras (casi siempre basadas en _Alpine Linux_) Para Python podemos buscar las imágenes oficiales de tipo ___slim___, por ejemplo `python:3.10.9-slim` __Orden de instrucciones en el _dockerfile___ : Para acelerar las operaciones de recontrucción de imágenes, que serán muy frecuentes durante el desarrollo, Docker implementa mecanismos de caché. Para aprovechar esta funcionalidad es muy importante que dejemos las opciones más estables (que tengan menos cambios a lo largo del desarrollo) al principio del _dockerfile_. Por ejemplo nuestro _dockerfile_ de ejemplo pone al principio los comandos de instalación de dependencias y despues copia los fuentes. Como las dependencias no cambiarán tan frecuentemente como el código fuente de nuestro proyecto se podran aprovechar las imágenes de la caché. __Construcción de imágenes por etapas__ : Esta técnica se usa más bien para preparar las imágenes para distribución, no en la fase de desarrollo. Por ejemplo veamos como contruir nuestra aplicación y preparar una imagen ligera para distribución: ```dockerfile # first stage FROM python:3.10.9 AS builder COPY requirements.txt . # install dependencies to the local user directory (eg. /root/.local) RUN pip install --user -r requirements.txt # second unnamed stage FROM python:3.10.9-slim WORKDIR /code # copy only the dependencies installation from the 1st stage image COPY --from=builder /root/.local /root/.local COPY ./src . # update PATH environment variable ENV PATH=/root/.local:$PATH CMD [ "python", "./server.py" ] ``` {{< admonition type=note title="TODO" open=true >}} Comprobar estas referencias y completar - - - - {{< /admonition >}} ## Varios experimentos con interfaces de usuario ### Tkinter ### PySimpleGUI ### NiceGUI ### Textual ## Miscelanea ### Soluciones de exercism Pistas para el ejercicio 'matrix' Una matriz o array en python se puede representar como una 'lista de listas' Asi la matriz 1 2 3 4 5 6 7 8 9 Podría representarse como una lista de python que almacenase una lista por cada fila ~~~python my_array = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] ~~~ El método clásico de transponer (que las filas se conviertan en columnas) una matriz en python era: ~~~python my_array_in_rows = [ [1, 2], [3, 4], [5, 6] ] my_array_in_cols = list(map(list, zip(*my_array_in_rows))) ~~~ Que no es muy intuitivo ¬_¬ Pero en python moderno con las list comprehension podemos escribir: ~~~python elem_by_row = len(array_in_rows[0]) my_array_in_cols = [[linea[i] for linea in my_array_in_rows] for i in range(elem_by_row)] ~~~ Hemos supuesto que el array está "bien formado", es decir que todas las filas tienen el mismo número de elementos, asi que para calcular cuantos elementos hay por fila nos vale la logitud de cualquier fila (lo calculamos con la fila cero) La 'comprehension' externa recorre los índices de los elementos de una fila (serían los números de columna) y para cada número de columna devuelve otra 'comprehension' (que llamaremos interna) La 'comprehension' interna devuelve una lista formada por el elemento i-ésimo de cada linea del array, (es decir, los elementos de la columna i-ésima. en nuestro ejemplo el contenido de my_array_in_cols, al final sería ```text [ [1, 3, 5], [2, 4, 6] ] ``` ### Jupyter (antes conocido como iPython) Por descontado, hay que instalarlo en un _virtualenv_ ~~~bash python3 -m venv venv-jupyter pip install jupyter ~~~ En la sesión del curso instalamos tambien seaborn para ver gráficos: ~~~bash # Asegurate de tener activo el virtualenv antes de instalar pip install seaborn pip install statsmodels ~~~ ### Expresiones Regulares en Python Un par de páginas web (hay muchísimas) para probar nuestras expresiones regulares * * Un buen recurso para aprender expresiones regulares con varias versiones, entre ellas castellano * Un muy buen tutorial de expresiones regulares en python: * Parte I * Parte II ### Algunas librerias y frameworks interesantes #### argparse ~~~python # -------------------- main -------------------- # Parse command line options arg_parser = argparse.ArgumentParser( description='Process files from input dir. Writes results in output dir') arg_parser.add_argument( '-i', '--idir', help='Input directory', required=True) arg_parser.add_argument( '-o', '--odir', help='Output directory', required=True) args = arg_parser.parse_args() # Check directories if not (os.path.isdir(args.idir)): print('Input dir <{}> is not a valid directory'.format(args.idir)) exit() if not (os.path.isdir(args.odir)): print('Output dir <{}> is not a valid directory'.format(args.odir)) exit() ~~~ * Toga para hacer GUI (interfaces de usuario) * Kivy Interfaces de usuario, dispositivos móviles * Dash Para hacer cuadros de mandos, se basa en plotly que a su vez se basa en d3.js. Pese a la apariencia de la página web, es software libre con licencia MIT #### Frameworks Web __Flask__ El más simple de los frameworks, siempre es bueno dedicarle algún tiempo para ver los conceptos básicos. Se puede hacer cualquier aplicación web con Flask, pero los frameworks más potentes nos ahorrarán mucho trabajo. Sigue siendo imbatible para aplicaciones sencillas Un buen tutorial de Flask: [El mega tutorial de Flask](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world). Son 23 capitulos publicados en la web. __web2py__ Un framework más potente que Flask y más sencillo que Django [El libro oficial de web2py](http://web2py.com/book) (hay versión en español) El capítulo 3 del libro es un buen punto de partida para autoformación Un buen tutorial de web2py, son siete videos de unos 20 minutos cada uno: * [Server Side programming with web2py](https://www.youtube.com/watch?v=zmSOnKkm8Y0&list=PL5E2E223FE3777851) * [Los videos de Maximo di Pierro](https://vimeo.com/user315328) (el creador de web2py) un curso de 30 videos super-completo Aseguraos de empezar por el primer video (hay que ir hacia atrás en el historial del usuario) El primero sería [este](https://vimeo.com/104801796) * Una guia (sin terminar) que puede valer para instalar web2py es [esta](https://gitlab.com/salvari_guides/web2py) #### celery {{< admonition type=info title="Referencias" open=true >}} - [Documentación Oficial](https://docs.celeryq.dev/en/stable/index.html) - [Demonizar Celery](https://docs.celeryq.dev/en/stable/userguide/daemonizing.html#daemonizing) - [Celery Python Guide: Basic, Examples and Useful Tips](https://codeburst.io/the-celery-python-guide-basics-examples-and-useful-tips-d8da1fcfaea3) - [Asyncronous Task in Python with Celery](https://medium.com/analytics-vidhya/asynchronous-tasks-in-python-with-celery-e6a9d7e3b33d) - [Full Stack Python Celery Collection](https://www.fullstackpython.com/celery.html) {{< /admonition >}} Una *cola de tareas* para Python. Una _cola de tareas_ sirve para distribuir el trabajo en _threads_ (hilos de ejecución concurrente) o entre distintas máquinas. Las entradas para la _cola de tareas_ son unidades de trabajo denominadas tareas (_tasks_). Trabajadores (_workers_) dedicados monitorizan la cola constantemente y ejecutan las tareas encoladas. _Celery_ se comunica mediante mensajes, normalmente usando un _broker_ que hace de mediador entre clientes y _workers_. Los clientes (_clients_) ofertan tareas enviando mensajes a la cola. El _broker_ facilita que esos mensajes lleguen a los _workers_. Así pues un sistema _Celery_ se compone de múltiples _workers_ y _brokers_, lo que permite obtener __alta disponibilidad__ y sistemas fáciles de __escalar horizontalmente__. _Celery_ está escrito en Python pero se ha hecho tan popular que se puede usar desde varios lenguajes de programación. _Celery_ __necesita__ un _broker_ para funcionar, tanto _RabittMQ_ como _Redis_ ofrecen soluciones completamente funcionales pero _Celery_ soporta muchos más. Adicionalmente podemos instalar un sistema que actúe como _almacén de resultados_ (_results store_). _Celery_ soporta muchos diferentes. ##### Montando un escenario sencillo con Celery Lanzamos un _RabittMQ_ dockerizado: ```bash docker run -d -p 5672:5672 rabbitmq ``` Creamos nuestro entorno virtual como de costumbre e instalamos _Celery_ ```bash cd pyenv virtualenv 3.9.7 ve_celery pyenv local ve_celery myve pip install celery ``` Creamos un fichero `tasks.py`: ```python3 from celery import Celery app = Celery('tasks', broker='pyamqp://guest@localhost//') @app.task def add(x, y): return x + y ``` Con esto podríamos ya lanzar nuestro primer _worker_ ejecutando el comando: `celery -A tasks worker --loglevel=INFO` Esto nos vale para probar, en un entorno de producción nos interesa lanzar los _workers_ como servicio (ver [Daemonization](https://docs.celeryq.dev/en/stable/userguide/daemonizing.html#daemonizing)) Ahora podríamos abrir una sesión con el intérprete de Python y lanzar una tarea al _worker_ con ```python from tasks import add add.delay(4, 4) ``` ### Django Como de costumber creamos un entorno virtual: ```bash pyenv virtualenv 3.9.7 ve_django myve pip install Django ``` Creamos un directorio para las pruebas y le asignamos el *virtualenv* local. ```bash amd Django pyenv local ve_django ``` Para empezar hacemos el [tuto oficial](https://docs.djangoproject.com/en/4.0/intro/) * `python -m django version` nos permite ver la versión de Django (la 4.0.5) * `django-admin startproject mysite` nos permite crear un nuevo directorio de proyecto con su estructura característica * `python manage.py runserver` ejecuta el server, en nuestro caso vale con `./manage.py runserver` * `python manage.py runserver 8000` ejecuta el server en el puerto 8000 * `python manage.py runserver 0:8000` ejecuta el server en el puerto 8000 en todas las IP del servidor * `python manage.py startapp polls` crea el directorio para la aplicación `polls`, las aplicaciones se crean todas en el mismo directorio donde reside el `manage.py` #### views en Django Para crear una vista: - definimos una función en el fichero `views.py` de nuestra aplicación - en el fichero `urls.py` de nuestra aplicación, asociamos la función a un `path` - en el fichero `urls.py` de nuestro site, tenemos que incluir el módulo `polls.urls` que se corresponde con el fichero `polls/urls.py`, para hacer esto siempre usamos `include` (`from django.urls import include, path`) La funcion `path` tiene cuatro parámetros: **route** : es un *string* que contiene un patrón para urls. Cuando __Django__ recibe una petición (_request_) **Django** recorrerá todos los patrones de la lista `urlpatterns` hasta encontrar una que encaje. Al hacer esta búsqueda se busca solo el `path` no se comprueba el dominio que aparece en la petición ni el tipo (`GET` o `POST`) ni los parámetros. **view** : cuando __Django__ encuentra el patrón que encaja se invoca a la función _view_ asociada a ese patrón, pasando el objeto _HttpRequest_ y cualquier otro valor que haya sido capturado **kwargs** : se pueden pasar parámetros adicionales en un diccionario **name** : siempre es práctico asignar un nombre a las URL para poder referirnos a ellas desde cualquier parte #### Establecer la base de datos En el fichero `mysite/settings.py` es un módulo de Python normal y corriente que almacena distintos valores de configuración de __Django__ en variables. Por defecto tenemos configurado usar _SQLite3_ como motor de base de datos. Si queremos usar otra de las bases de datos soportadas tenemos que instalar el _backend_ correspondiente ([ver doc](https://docs.djangoproject.com/en/4.0/topics/install/#database-installation)) Si usamos _SQLite3_ en el parámetro `NAME` especificamos el fichero de la base de datos _SQLite3_ Una vez establecida la base de datos podemos empezar a definir nuestros modelos (_models_), básicamente son las tablas de la base de dawos con sus metadatos. Los modelos se definen en el fichero `polls/models.py`, cada tabla en la base de datos se corresponde con una clase en el fichero. Cuando tenemos los modelos definidos necesitamos ejecutar dos pasos adicionales: * Dar de alta nuestra app en el fichero `mysite/settings.py` * Activar los modelos, con la operación que en Django se denomina _Migración_. Las migraciones quedan codificadas en ficheros python en el directorio `migrations`de la aplicación correspondiente. Comandos: - `python manage.py makemigrations polls` prepara las migraciones y las deja listas para ejecutarse en el directorio `migrations` de la aplicación correspondiente. - `python manage.py sqlmigrate polls 0001` nos dice que comandos sql corresponden a la migración. Son los comandos que se ejecutarán en la base de datos cuando ejecutemos la migración. - `python manage.py check` hace comprobaciones sin ejecutar la aplicación - `python manage.py migrate` ejecuta la migración (de hecho ejecuta todas las que haya pendientes) - `python manage.py shell` nos abre un shell para hacer experimentos #### Django admin `python manage.py createsuperuser` nos permite crear un usuario administrador para nuestra instancia de Django, podemos acceder al interfaz de administración en la dirección Para que nuestra applicación sea accesible desde el interfaz de administración de Django, es necesario modificar el fichero `polls/admin.py` para que incluya las siguientes lineas: ```python3 from django.contrib import admin from .models import Question admin.site.register(Question) ``` #### Nuevas vistas Para generar nuevas vistas: - Definimos la nueva función en el fichero `polls/views.py` - Añadimos las nuevas url a los `urlpatterns` definidos en el fichero `polls/urls.py` Cada vista tiene que lograr uno de dos objetivos: o bien genera un _HttpResponse_ válido o bien devuelve una excepción como por ejemplo `Http404` A parte de eso la vista puede hacer todo tipo de cosas. Leer registros de una base de datos, generar ficheros de cualquier tipo, usar el sistema de _Templates_ de Django, etc etc. ### Bibliografía * [El libro de Ekaitz](https://gitlab.com/ElenQ/Publishing/python) que hemos "seguido" en este curso * [La traducción al gallego](https://gitlab.com/brico-labs/curso_python/-/tree/gl) del mismo libro, elaborada por asistentes al curso * El libro del Dr. Chuck. Los cursos de este hombre en Coursera se podían seguir como oyente de forma gratuita y están muy bien para empezar. En todo caso [aquí](http://do1.dr-chuck.com/pythonlearn/) tenéis un libro suyo (disponible en castellano) ### Batiburrillo de enlaces * [Pilulas para o teletraballo](https://www.mancomun.gal/noticias/pilulas-para-o-teletraballo/) * [Maestro das traducións](https://pad.disroot.org/p/curso_python_memoria.md) * [Portal das palabras](https://portaldaspalabras.gal/) * [Apertium translator](https://www.apertium.org/index.glg.html?dir=spa-glg#translation) * [Python with VCS](https://code.visualstudio.com/docs/python/python-tutorial) * [Select venv with VCS](https://code.visualstudio.com/docs/python/environments) * [Decoradores ref 1](https://jeffknupp.com/blog/2013/11/29/improve-your-python-decorators-explained//) * [Decoradores ref 2](https://www.thecodeship.com/patterns/guide-to-python-function-decorators/) Para mais adiante: * [Flask megatutorial](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-now-with-python-3-support) * [Micropython IOT](https://blog.miguelgrinberg.com/post/micropython-and-the-internet-of-things-part-i-welcome) * [npyscreen TUI en python](https://medium.com/@ValTron/create-tui-on-python-71377849879d) * [Multithreading in python](https://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python) * [Dash analytical web app (sin javascript)](https://pypi.org/project/dash/) * [Estadísticas en python](https://www.learntek.org/blog/python-statistics-fundamentals/) * [Cheatsheet Python](https://gto76.github.io/python-cheatsheet/) * [Astronomia con python](https://opensource.com/article/19/10/python-astronomy-open-data) * [Mobil programs in python](https://beeware.org/) * [Tutorial de Kivy](https://www.simplifiedpython.net/python-kivy-tutorial-for-beginners-getting-started/) Problemas instalando *Kivy* o *Pygame*: * En [esta página](https://kivy.org/doc/stable/installation/installation-linux-venvs.html#installation-in-venv) en el web oficial, parece que se detallan las dependencias de kivy. * Para instalar pygame o kivy parece que hay dependencias con sdl2 En esta otra [página](https://pythonprogramming.altervista.org/how-to-install-pygame-in-python-3-8/) explican como instalar pygame en python 3.8 No se vayan todavía... * [Think DSP](https://github.com/AllenDowney/ThinkDSP) * [Task queues in python](https://www.fullstackpython.com/task-queues.html) * [SICP in Python](https://wizardforcel.gitbooks.io/sicp-in-python/content/1.html) * [Dive into python 3](https://diveintopython3.net/) * [Toga](https://toga.readthedocs.io/en/latest/) * [Kivy](https://kivy.org) * [Dash](https://dash.plotly.com/) * [Celery](https://medium.com/swlh/python-developers-celery-is-a-must-learn-technology-heres-how-to-get-started-578f5d63fab3)) * [Server Side programming with web2py](https://www.youtube.com/watch?v=zmSOnKkm8Y0&list=PL5E2E223FE3777851) * [Web2py web development](https://www.youtube.com/watch?v=dHu8O1gZOl0&list=PLRMNVXvt79o0VUcNSWAmES3c_36BTEQRr&index=2) * [Python Hacks](https://medium.com/@Nibba2018/python-life-hacks-for-everyday-use-ba6fc4b8c9ac) * [Abstract Classes](https://medium.com/techtofreedom/abstract-classes-in-python-f49cf4efdb3d) * [Smart Contract Blockchain in Python](https://medium.com/better-programming/how-to-write-smart-contracts-for-blockchain-using-python-part-2-99fc0cd43c37) * [Emacs as Python IDE](https://medium.com/analytics-vidhya/managing-a-python-development-environment-in-emacs-43897fd48c6a) * [Emacs pyenv](http://rakan.me/emacs/python-dev-with-emacs-and-pyenv/)