Notas de Python

Work in progress
Apuntes de Python, muy incompletos.

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.

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 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).

Módulos Python que si que instalo
Hay algunos paquetes como veremos en el siguiente punto que si que nos conviene tener siempre instalados. Son justo los que nos facilitan la gestión de los virtualenvs, como por ejemplo venv o virtualenv

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:

1
2
3
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:

1
2
3
4
# 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) 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#-------------------
# virtualenv
virtualenv <nombreEntorno>   # 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  <nombreEntorno>                  # Crear un entorno virtual y activarlo
mkvirtualenv -p /usr/bin/python3  -a <dirPry> <nombreEntorno>      # Crear un entorno virtual y asociar el directorio de proyecto
deactivate                                                         # Salir del entorno virtual
lsvirtualenv                                                       # ver la lista de entornos virtuales
workon <nombreEntorno>                                             # activar un entorno ya definido
rmvirtualenv <nombreEntorno>                                       # borrar un entorno virtual
cpvirtualenv <origen> <destino>                                    # copiar un entorno virtual
lssitepackages                                                     # listar paquetes instalados en el entorno activo

pyenv

Importante: No confundir con pyvenv

pyenv 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
  2. Crear entornos virtuales basados en esas versiones, con el plugin virtualenv de pyenv
  3. Asignar los entornos virtuales a proyectos (normalmente con pyenv local <entorno>

Instalamos las dependencias en el sistema:

1
2
3
4
5
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í tienes una alternativa (que no he probado) o puedes currarte una instalación siguiendo las instrucciones oficiales en el github del proyecto pyenv

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):

1
2
3
4
5
6
# 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:

1
2
3
4
# 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)

1
2
3
4
5
6
7
8
9
pyenv commands                            # La lista de comandos disponibles (¡son muchos!)
pyenv <cmd> --help                        # Ayuda para un comando
pyenv install --list                      # Versiones de python disponibles
pyenv versions                            # Versiones de python instaladas en nuestro sistema

pyenv global <version>                    # Establece la version de python global
pyenv local <version>                     # Establece la versión de python local
pyenv shell <version>                     # Establece la versión para el shell
pyenv virtualenv <version> <env_name>     # 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
1
2
3
4
5
6
7
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
Peligro

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

Para poder activar el virtualenvwrapper correctamente via el shimm, añadimos el siguiente alias:

1
2
# 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

En Emacs usamos LSPLanguage Server Protocol. Para Python necesitamos instalar python-lsp-server Mucho ojo, no hay que instalar Palantir (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)

1
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:

1
2
3
4
myve(){
    pip install --upgrade pip setuptools pipx virtualenv virtualenvwrapper
    pip install 'python-lsp-server[all]'
}

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).

Supongamos que tenemos instalada la versión anaconda3-2021.11. Ejecutaríamos:

1
2
3
4
5
6
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

Modulos en Python

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

1
2
3
4
5
6
7
    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

1
y = x[::-1]

Minidom

  • Acceder a un atributo
1
2
3
4
5
6
7
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
1
2
3
4
5
6
7
8
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

Un ejemplo rápido:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import tkinter as tk

border_effects = {
    "flat": tk.FLAT,
    "sunken": tk.SUNKEN,
    "raised": tk.RAISED,
    "groove": tk.GROOVE,
    "ridge": tk.RIDGE,
}

window = tk.Tk()

for relief_name, relief in border_effects.items():

    frame = tk.Frame(master=window, relief=relief, borderwidth=5)

    frame.pack(side=tk.LEFT)

    label = tk.Label(master=frame, text=relief_name)

    label.pack()

window.mainloop()

Ojo al borderwidth en la linea 15, si no lo ponemos no funcionan los efectos.

Aprendiendo tkinter

Creamos un virtualenv:

1
mkvirtualenv tkinter

Probamos que todo va bien

1
2
3
4
#!/usr/bin/env python

import tkinter
tkinter._test()

Vamos a echar un ojo a un programa completo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/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("<Return>", calculate)

root.mainloop()

Lo mismito pero con OOP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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("<Return>", 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
  • 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

1
2
3
4
5
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

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
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    from tkinter import *
    from tkinter import ttk
    root = Tk()
    l =ttk.Label(root, text="Starting...")
    l.grid()
    l.bind('<Enter>', lambda e: l.configure(text='Moved mouse inside'))
    l.bind('<Leave>', lambda e: l.configure(text='Moved mouse outside'))
    l.bind('<ButtonPress-1>', lambda e: l.configure(text='Clicked left mouse button'))
    l.bind('<3>', lambda e: l.configure(text='Clicked right mouse button'))
    l.bind('<Double-1>', lambda e: l.configure(text='Double clicked'))
    l.bind('<B3-Motion>', lambda e: l.configure(text='right button drag to %d,%d' % (e.x, e.y)))
    root.mainloop()
    

Ejemplos

Un campo entrada de texto
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/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
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/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

Create http server

Comparte los ficheros de la carpeta

1
2
3
4
5
6
7
8
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

Static method vs Classmethod

Referencias

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
  • Iremos incorporando ao libro exemplos e notas que vexamos útiles

Bricolabs

Bricolabs

Documentación do curso

Outros enlaces de interese

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
    1
    
    #!/ruta al intérprete
    
    Ou tamén
    1
    
    #!/usr/bin/env python3
    
  • Ver “posición da memoria” (en realidade id devolve un identificativo único) da variable
    1
    
    hex(id(a))
    
  • saber o tipo dunha variable: type(variable)

Mutabilidad:

  • A tupla e inmutable
  • A lista e mutable

Estilo de programación

Tipos de datos

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
TIPOS
        |
        |- Numéricos
        |            |- Enteros (int)
        |            |- Coma flotante (float)
        |            |- Complejos
        |
        |- Texto
        |
        |- Booleanos
                    |- True (Verdadero)
                    |- Falso (Falso)

Operadores

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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()

1
2
for i in range(10):
  print(i)

En los bucles for y while se pueden usar las palabras clave continue, break y else:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/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

1
2
3
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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

1
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.

1
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

1
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

1
2
3
4
5
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

1
2
3
4
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.

1
2
3
def return_list(alist=[]):
    alist.append('uno')
    return(alist)

Solución al problema anterior (probada)

1
2
3
4
5
def return_list(alist=None):
    if alist is None:
        alist = []
    alist.append('uno')
    return(alist)

Funciones pasadas como parámetros. Composición de funciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
'''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])

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

1
2
3
4
5
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:

1
2
3
4
5
6
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:

1
2
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

1
2
3
4
[
 [1, 3, 5],
 [2, 4, 6]
]

Jupyter (antes conocido como iPython)

Por descontado, hay que instalarlo en un virtualenv

1
2
python3 -m venv venv-jupyter
pip install jupyter

En la sesión del curso instalamos tambien seaborn para ver gráficos:

1
2
3
# 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:

Algunas librerias y frameworks interesantes

argparse

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# -------------------- 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()

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. 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 (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:

celery

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:

1
docker run -d -p 5672:5672 rabbitmq

Creamos nuestro entorno virtual como de costumbre e instalamos Celery

1
2
3
4
5
cd <pry_dir>
pyenv virtualenv 3.9.7 ve_celery
pyenv local ve_celery
myve
pip install celery

Creamos un fichero tasks.py:

1
2
3
4
5
6
7
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)

Ahora podríamos abrir una sesión con el intérprete de Python y lanzar una tarea al worker con

1
2
from tasks import add
add.delay(4, 4)

Bibliografía

  • El libro de Ekaitz que hemos “seguido” en este curso
  • La traducción al gallego 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í tenéis un libro suyo (disponible en castellano)

Batiburrillo de enlaces

Para mais adiante:

Problemas instalando Kivy o Pygame:

  • En esta página 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 explican como instalar pygame en python 3.8

No se vayan todavía…