Además de la jerarquía estándar de tipos, Python incluye más estructuras de datos en la librería estándar. Estos tipos no se encuentran en el espacio de nombres por defecto y requieren de la importación explícita de algún módulo para funcionar.
Una tupla con nombres es aquella en la que cada posición tiene un identificador asociado por lo que podemos hacer uso de sus elementos mediante la notación con índices que ya conocemos o mediante nombres. Por ejemplo:
from collections import namedtuple
Point2D = namedtuple('Point2D', ['x', 'y'])
offset = Point2D(x=-10, y=-10)
location = Point2D(10, 10)
print(f'position is x={position.x}, y={position.y}')
d = position._asdict()
new_position = position._replace(x=100)
new_position is position
Con anterioridad a Python 3.7, los diccionarios no retenían el orden de inserción de los pares por lo que los desarrolladores necesitaban algún tipo de tipo secuencia auxiliar para recordar el orden cuando éste importaba.
En Python 2.7, el código:
d = {3:3, 2:2, 1:1}
d.items()
Da como resultado:
[(1, 1), (2, 2), (3, 3)]
Se introdujo pues el "diccionario ordenado" (tipo OrderedDict
en el módulo
collections
). De esta forma, el código:
from collections import OrderedDict
od = OrderedDict([(3,3), (2, 2), (1, 1)])
od.items()
Da como resultado:
[(3, 3), (2, 2), (1, 1)]
A partir de Python 3.7, se garantiza que el comportamiento del diccionario
por defecto (tipo dict
) es el de retener el orden por lo que el tipo
OrderedDict
pierde importancia.
Nota: es importante conocer el significado histórico de OrderedDict
por la
cantidad de código aun dependiente de versiones pre-3.6. Es Python 3.6 la
versión que cambia la implementación de los diccionarios para que retengan el
orden pero no es hasta Python 3.7 que se garantiza que los diccionarios
retienen el orden.
- Documentación del tipo
OrderedDict
anterior a Python 3.7. - Documentación del tipo
OrderedDict
actual.
Como ejercicio, compara las dos versiones de la documentación.
Las listas de Python (tipo list
) están optimizadas para operaciones que no
alteran la longitud de la lista. Sin embargo, una inserción o la extracción
de un elemento incurre en una penalización al tener que reubicar los elementos
de la lista.
Las listas doblemente enlazadas eliminan este problema y permiten inserciones y eliminaciones eficientes. Además contienen métodos especializados en la inserción y eliminación por ambas partes, lo que las convierte también en colas:
from collections import deque
q = deque()
q.appendleft(3)
q.appendleft(2)
q.appendleft(1)
q.popleft()
q.popleft()
q.popleft()
Reemplaza los métodos appendleft
y popleft
por append
y pop
respectivamente y compara los resultados.
Los contadores son diccionarios donde las claves son elementos de una colección y los valores son las veces que se repiten esos elementos. O, dicho de otra forma, un contador es un histograma.
Con un contador, el ejercicio de encontrar las repeticiones en una lista se resolvería así:
from collections import Counter
input = [1, 1, 2, 3, 4, 3, 2, 1]
repetitions = [value for value, count in Counter(input).items() if count > 1]
Imagina que estás implementando una lista de los subscriptores a distintos temas en tu blog, quizá para notificarles de nuevas entradas en las categorías de interés de cada usuario. Podrías usar un diccionario para este fin:
def subscribe(subscriptions, user, topic):
if topic not in subscriptions:
subscriptions[topic] = set()
subscriptions[topic].add(user)
subs = {}
if 'python' not in subs:
subs['python'] = set()
subs['python'].add('@salva')
if 'animals' not in subs:
subs['animals'] = set()
subs['animals'].add('@bea')
if 'dance' not in subs:
subs['dance'] = set()
subs['dance'].add('@paula')
if 'python' not in subs:
subs['python'] = set()
subs['python'].add('@diego')
Fíjate en los condicionales if
que lidian con el caso en el que la clave aún
no está en el diccionario. Este patrón es altamente común. Para estos casos
existe el tipo defaultdict
que admite una función (un invocable, para
ser exactos) que devuelva el valor por defecto si se accede a una clave que
no existe. Por ejemplo, el código anterior quedaría:
from collections import defaultdict
def empty_set():
return set()
subs = defaultdict(empty_set)
subs['python'].add('@salva')
subs['animals'].add('@bea')
subs['dance'].add('@paula')
subs['python'].add('@diego')
Investiga lo que es una lambda
y trata de ofrecer una versión más "pythonica" del código anterior.
¿Podrías utilizar la iteración con for ... in
y un diccionario normal y corriente para construir un contador? ¿Y utilizando un
diccionario con valor por defecto?
En computación, una expresión regular es una forma de definir una gramática regular y permite reconocer expresiones de un lenguaje regular.
Los lenguajes regulares son aquellos que se forman por la adición de nuevos caracteres al final de otro lenguaje regular y que no dependen de las propiedades del lenguaje anterior. Parece una definición sencilla pero, con algo de creatividad y práctica, es increiblemente potente. Además, las expresiones regulares en Python son algo más potentes que las estrícatmente académicas.
En el contexto de las expresiones regulares se habla mucho de si "una expresión regular reconoce (match) una cadena". De esto precisamente va el uso de expresiones regulares, de definir la forma de algunas cadenas, reconocerlas y extraer información de las mismas.
En Python, las expresiones regulares se encuentran en el módulo re
:
-
Empieza importando el módulo:
import re
-
Una expresión regular necesita ser compilada, lo que generará una máquina de estados (o autómata finito) para reconocerla:
e = re.compile(r'ab+')
Una vez compilada podemos ver si reconoce una o más cadenas:
assert e.match('ab') assert e.match('abbbb') assert not e.match('a') assert e.match('abc') assert not e.fullmatch('abc')
Existen muchos métodos para ver si una expresión regular reconoce una cadena. Aquí tienes un resumen:
Método Significado match() Se reconoce el comienzo de la cadena. fullmatch() Se reconoce toda la cadena encaja. search() Se reconoce alguna subcadena. findall() Devuelve todas subcadenas reconocidas en una lista. finditer() Igual que antes, pero en un iterador. -
También existen algunas funciones a nivel de módulo que hacen los mismo pero aceptan en un primer parámetro una cadena como expresión regular:
assert re.fullmatch(r'ab*', 'abb')
Utilizaremos estas formas por motivos didácticos pero conviene hacer notar que la compilación de una expresión regular es algo lento y si vamos a realizar muchas comprobaciones contra la misma expresión regular conviene compilarla sólo una vez y entonces llamar a los métodos de la misma.
-
En principio, las expresiones regulares reconocen cualquier secuencia de caracteres literalmente:
assert re.fullmatch(r'abc', 'abc')
-
El carácter especial
.
reconoce cualquier carácter:assert re.fullmatch(r'.', 'a') assert not re.fullmatch(r'.', 'aa')
-
También admiten repeticiones de cero o más, o uno o más:
assert re.fullmatch(r'ab*', 'a') assert re.fullmatch(r'ab*', 'ab') assert re.fullmatch(r'ab*', 'abb') assert not re.fullmatch(r'ab+', 'a') assert re.fullmatch(r'ab+', 'ab') assert re.fullmatch(r'ab+', 'abb')
-
Podemos controlar el número exacto de repeticiones:
assert not re.fullmatch(r'ab{3,5}', 'ab') assert re.fullmatch(r'ab{3,5}', 'abbb') assert re.fullmatch(r'ab{3,5}', 'abbbb') assert re.fullmatch(r'ab?', 'a') assert re.fullmatch(r'ab?', 'ab') assert not re.fullmatch(r'ab?', 'abb')
-
Como ves, hay caracteres que no se interpretan literalmente, sino que tienen un significado especial. Si quisiéramos reconocer alguno de ellos, tendríamos que usar una barra de "escape":
assert re.fullmatch(r'ab\?', 'ab?')
Puesto que el carácter especial
\
para "escapar" caracteres en expresiones regulares colisiona con aquel para "escapar" caracteres en las cadenas normales, si no usáramos el modificadorr
, escribir estos caracteres sería demasiado tedioso:assert re.fullmatch('ab\\?', 'ab?')
Puestos a reconocer la cadena
\title
, tendríamos que escribir:assert re.fullmatch('\\\\title', '\\title')
Es preferible usar una cadena "cruda" y escribir:
assert re.fullmatch(r'\\title', r'\title')
-
Una expresión regular puede indicar si reconoce al principio o al final de una cadena:
assert re.fullmatch(r'From A to Z', 'From A to Z')
Es equivalente a:
assert re.match(r'^From A to Z$', 'From A to Z') assert not re.match(r'^From A to Z$', 'From A to Z and more') assert not re.match(r'^From A to Z$', '* From A to Z')
El carácter
^
indica "al comienzo de la cadena" y el carácter "$" indica "al final de la cadena". -
Una expresión regular puede elegir de entre un cojunto de caracteres:
assert re.fullmatch(r'a[bcd]z', 'abz') assert re.fullmatch(r'a[bcd]z', 'acz') assert re.fullmatch(r'a[bcd]z', 'adz')
-
O también puede elegir de entre un conjunto de expresiones regulares:
assert re.fullmatch(r'cat|dog', 'cat') assert re.fullmatch(r'cat|dog', 'dog')
Que no es lo mismo que:
assert re.fullmatch(r'ca(t|d)og', 'catog') assert re.fullmatch(r'ca(t|d)og', 'cadog')
Los paréntesis pueden alterar la precedencia de otros operadores.
-
También podemos expresar que no esté en un cojunto:
assert re.fullmatch(r'[^aeiou]\w+', 'pod') assert not re.fullmatch(r'[^aeiou]\w+', 'ipod')
Las expresiones regulares incluyen algunos símbolos especiales que denotan conjuntos de caracteres como:
Símbolo Significado \d
Cualquier dígito Unicode. \D
Cualquier carácter que no sea un dígito. \s
Cualquier carácter Unicode que represente espacio en blanco. \S
Cualquier carácter que no represente espacio en blanco. \w
Cualquier carácter que pueda pertenecer a una palabra. \W
Cualquier carácter que no pertenezca a una palabra. Algunos de ellos no reconocen nada sino que indican posiciones en la cadena:
Símbolo Significado \b
En un extremo de una palabra. \B
En mitad de una palabra. \A
Al comienzo de la cadena. \Z
Al final de la cadena. -
Podemos pasar algunas opciones (flags) a la compilación, por ejemplo, para ignorar la capitalización:
assert re.fullmatch(r'[^aeiou]\w+', 'Ipod') assert not re.fullmatch(r'[^aeiou]\w+', 'Ipod', re.IGNORECASE)
Existen otras opciones posibles. Más de una opción puede indicarse con el operador de tubería:
re.IGNORECASE | re.ASCII
.Flag Forma larga Significado re.A
re.ASCII
Sólo caracteres ASCII (no Unicode). - re.DEBUG
Muestra información sobre la compilación. re.I
re.IGNORECASE
Ignora la capitalización. re.M
re.MULTILINE
Trata cada nueva línea como una cadena distinta. re.S
re.DOTALL
Hace que el carácter comodín .
reconozca los saltos de línea.re.X
re.VERBOSE
Cambia la sintaxis de las expresiones regulares a una más clara. -
Los cuantificadores
*
,+
y{n,m}
pueden acompañarse de?
para formar un nuevo cuantificador "no codicioso":print(re.findall(r'<.*>', '<b>Important notice</b>')) print(re.findall(r'<.*?>', '<b>Important notice</b>'))
El resultado de una operación match
o fullmatch
no es un valor "booleano",
sino un objeto re.Match
. Este objeto permite extraer información de la
expresión regular. La información se captura en grupos.
-
Los grupos se delimitan con paréntesis:
m = re.match(r'(\d+),(\d+)', '3,5') print(m.group(0)) print(m.group(1)) print(m.group(2))
Los grupos se cuentan por el paréntesis de apertura, comenzando en
1
. El grupo0
se reserva para el reconocimiento completo. -
Los grupos se pueden nombrar, para que sea más semántico acceder a ellos:
m = re.match(r'(?P<x>\d+),(?P<y>\d+)', '3,5') print(m.group(0)) print(m.group('x')) print(m.group('y'))
-
También se pueden ignorar para que no pertenezcan al conteo:
m = re.fullmatch(r'ca(?:t|d)og', 'catog') print(m.group(0)) print(m.group(1))
-
Un grupo puede ser referenciado como parte de una expresión regular:
assert re.fullmatch(r'(foo) \1', 'foo foo') assert not re.fullmatch(r'(foo) \1', 'foofoo')
También podemos hacer una referencia por nombre:
assert re.match(r'(?P<x>\d+),(?P=x)', '3,3') assert not re.match(r'(?P<x>\d+),(?P=x)', '3,5')
-
Algunos grupos toman decisiones respecto a algunos caracteres de la cadena, por delante de la posición actual:
assert re.match(r'Isaac (?=Asimov)', 'Isaac Asimov') assert not re.match(r'Isaac (?=Asimov)', 'Isaac Newton') assert not re.match(r'Isaac (?!Asimov)', 'Isaac Asimov') assert re.match(r'Isaac (?!Asimov)', 'Isaac Newton')
O por detrás de la posición actual:
assert not re.search(r'(?<=--)\w+', '-v') assert re.search(r'(?<=--)\w+', '--verbose')
Si utilizamos la referencia hacia atrás, combiene usar
search()
en lugar dematch()
, puesto que este tipo de grupos no se reconocen al comienzo de las cadenas. -
Un último grupo permite tomar decisiones respector de otros grupos:
assert re.match(r'(<)?(\w+@\w+\.\w+)(?(1)>|$)', '[email protected]') assert re.match(r'(<)?(\w+@\w+\.\w+)(?(1)>|$)', '<[email protected]>') assert not re.match(r'(<)?(\w+@\w+\.\w+)(?(1)>|$)', '[email protected]>') assert not re.match(r'(<)?(\w+@\w+\.\w+)(?(1)>|$)', '<[email protected]')
Se trata del último grupo de cada expresión y la sintáxis es
(?(id)yes-pattern|no-pattern)
. Se traduce como "si se ha reconocido el grupoid
, trata de reconocer elyes-pattern
; si no, elno-pattern
.
- Sintáxis de las expresiones regulares de Python.*
- Regex 101 es un compañero indispensable para todo aquel que utilice expresiones regulares.*
- Documentación del módulo
re
- No, no se puede parsear HTML con una expresión regular.
- Pero se puede parsear un subconjunto del mismo.
El tipo fecha en Python viene en dos sabores. Por un lado, una modalidad naive (ingenua), que descarta la información de la zona horaria. La interpretación del valor de una fecha naive se deja a la aplicación (que podría asumir, por ejemplo, que son momentos en la zona horaria local). Por otro lado, una modalidad aware (consciente), que tiene en cuenta la zona horaria.
La creación de una fecha naive consiste en utilizar el tipo datetime
ignorando el parámetro tzinfo
:
import datetime
christmas = datetime.datetime(2019, 12, 25)
past_year_christmas = christmas - datetime.timedelta(days=365)
delta = christmas - past_year_christmas
christmas.strftime('Christmas %Y')
past_year_christmas.strftime('Christmas %Y')
past_year_christmas < christmas
Para la creación de una fecha aware tenemos que suplir alguna información
de la zona horaria. Por ejemplo, una instancia de la clase timezone
:
import datetime
madrid_timezone = datetime.timezone(datetime.timedelta(hours=1))
madrid_christmas = datetime.datetime(2019, 12, 25, tzinfo=madrid_timezone)
ukraine_timezone = datetime.timezone(datetime.timedelta(hours=2))
ukraine_christmas = datetime.datetime(2019, 12, 25, tzinfo=ukraine_timezone)
madrid_christmas - ukraine_christmas
ukraine_christmas < madrid_christmas
Las fechas pueden operarse y compararse. El orden de dos fechas viene dado por el momento que ocurre antes en el tiempo. Así, la fecha A será menor que la fecha B si la fecha A ocurre antes que la fecha B.
Prueba a comparar (igualdad, menor que, mayor o igual que...) fechas naive y fechas aware, ¿qué ocurre? ¿Y al operar aritméticamente fechas de distinta modalidad?
Construye un pequeño programa que imprima el día de la semana (lunes, martes...)
de una fecha dada en formato día/mes/año
.
- Documentación del módulo
datetime
- Documentación para el formateado de fechas
- Documentación del módulo
calendar
Considera también explorar otras bibliotecas relacionadas con fechas como dateutil o Arrow que aportan utilidades para crear fechas aware, para interpretar fechas a partir de cadenas o para formatear momentos de una manera más natural.
Vale la pena echar un vistazo a la sección Data Types de la documentación de Python para completar el conjunto de estructuras de datos y algoritmos que provee Python.