Skip to content

Latest commit

 

History

History
executable file
·
580 lines (432 loc) · 20 KB

File metadata and controls

executable file
·
580 lines (432 loc) · 20 KB

Otros tipos ampliamente utilizados

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.

Tuplas con nombres

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

Diccionarios ordenados

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.

Como ejercicio, compara las dos versiones de la documentación.

Listas doblemente enlazadas

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.

Contadores

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]

Diccionarios con valor por defecto

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?

Expresiones regulares

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:

  1. Empieza importando el módulo:

    import re
  2. 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.
  3. 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.

  4. En principio, las expresiones regulares reconocen cualquier secuencia de caracteres literalmente:

    assert re.fullmatch(r'abc', 'abc')
  5. El carácter especial . reconoce cualquier carácter:

    assert re.fullmatch(r'.', 'a')
    assert not re.fullmatch(r'.', 'aa')
  6. 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')
  7. 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')
  8. 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 modificador r, 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')
  9. 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".

  10. 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')
  11. 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.

  12. 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.
  13. 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.
  14. 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>'))

Agrupamientos

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.

  1. 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 grupo 0 se reserva para el reconocimiento completo.

  2. 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'))
  3. 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))
  4. 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')
  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 de match(), puesto que este tipo de grupos no se reconocen al comienzo de las cadenas.

  6. 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 grupo id, trata de reconocer el yes-pattern; si no, el no-pattern.

Fechas

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.

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.