Colecciones no secuenciales

Ricardo Pérez López

IES Doñana, curso 2025/2026

1 Conjuntos (set y frozenset)

1.1 Definición

  • Un conjunto es una colección no ordenada de elementos hashables.

  • Se usan frecuentemente para comprobar si un elemento pertenece a un grupo, para eliminar duplicados en una secuencia y para realizar operaciones matemáticas como la unión, la intersección y la diferencia simétrica.

  • Como cualquier otra colección, los conjuntos permiten el uso de:

    • x\ in \ c

    • len(c)

    • for \ x\ in \ c

  • Como son colecciones no ordenadas, los conjuntos no almacenan la posición de los elementos o el orden en el que se insertaron.

  • Por tanto, tampoco admiten la indexación, las rodajas ni cualquier otro comportamiento propio de las secuencias.

  • Cuando decimos que un conjunto no está ordenado, queremos decir que los elementos que contiene no se encuentran situados en una posición concreta.

    Es lo contrario de lo que ocurre con las sencuencias, donde cada elemento se encuentra en una posición indicada por su índice y podemos acceder a él usando la indexación.

  • Además, en un conjunto no puede haber elementos repetidos (un elemento concreto sólo puede estar una vez dentro de un conjunto, es decir, o está una vez o no está).

  • En resumen:

    En un conjunto:

    Un elemento concreto, o está una vez, o no está.

    Si está, no podemos saber en qué posición (no tiene sentido preguntárselo).

  • Existen dos tipos predefinidos de conjuntos: set y frozenset.

  • El tipo set es mutable, es decir, que su contenido puede cambiar usando métodos como add y remove.

    Como es mutable, no es hashable y, por tanto, no puede usarse como clave de un diccionario o como elemento de otro conjunto.

  • El tipo frozenset es inmutable y hashable. Por tanto, su contenido no se puede cambiar una vez creado y puede usarse como clave de un diccionario o como elemento de otro conjunto.

  • Los dos tipos de conjuntos se crean con las funciones set([iterable]) y frozenset([iterable]):

    • Si se llaman sin argumentos, devuelven un conjunto vacío:

      • set() devuelve un conjunto vacío de tipo set.

      • frozenset() devuelve un conjunto vacío de tipo frozenset.

      >>> set()
      set()
      >>> frozenset()
      frozenset()

      Como se ve, esas son, precisamente, las formas normales de un conjunto vacío de tipo set y frozenset.

    • Si se les pasa un iterable (como por ejemplo, una lista), devuelve un conjunto formado por los elementos del iterable:

      >>> set([4, 3, 2, 2, 4])
      {2, 3, 4}
      >>> frozenset([4, 3, 2, 2, 4])
      frozenset({2, 3, 4})
  • Además, existe una sintaxis especial para escribir literales de conjuntos no vacíos de tipo set, que consiste en encerrar sus elementos entre llaves y separados por comas: {'pepe', 'juan'}.

    >>> x = {'pepe', 'juan'}  # un literal de tipo set, como set(['pepe', 'juan'])
    >>> x
    {'pepe', 'juan'}
    >>> type(x)
    <class 'set'>

    Esa es, precisamente, la forma normal de un conjunto no vacío y, por tanto, la que se usa cuando se visualiza desde el intérprete o se imprime con print.

  • Por tanto, para crear conjuntos congelados usando frozenset podemos usar esa sintaxis en lugar de usar listas como hicimos antes:

    >>> frozenset({4, 3, 2, 2, 4})
    frozenset({2, 3, 4})
  • También podría usarse con la función set, pero entonces estaríamos creando un nuevo conjunto igual que el anterior, aunque no idéntico (es decir, sería una copia del original):

    >>> s = {4, 3, 2, 2, 4}
    >>> s
    {2, 3, 4}
    >>> t = set(s)
    >>> t
    {2, 3, 4}
    >>> s == t
    True
    >>> s is t
    False
  • En memoria, los conjuntos se almacenan mediante una estructura de datos donde sus elementos no se identifican mediante ningún índice o clave especial.

  • Por ejemplo, el siguiente conjunto:

    d = {1, 2, 3}

    se almacenaría de la siguiente forma según lo representa la herramienta Pythontutor:

    Conjunto almacenado en memoria

1.2 Conjuntos por comprensión

  • También se pueden crear conjuntos por comprensión usando la misma sintaxis de las expresiones generadoras y las listas por comprensión, pero esta vez encerrando la expresión entre llaves.

  • Su sintaxis es:

    conj_comp ::= {expresión (for identificador in secuencia [if condición])^+}
  • Por ejemplo:

    >>> {x ** 2 for x in [1, 2, 3]}
    {1, 4, 9}
  • El resultado es directamente un valor de tipo set, no un iterador.

  • Los conjuntos por comprensión, al igual que las expresiones generadoras y las listas por comprensión, determinan su propio ámbito.

  • Ese ámbito abarca todo el conjunto por comprensión, de principio a fin.

  • Al recorrer el iterable, las variables van almacenando en cada iteración del bucle el valor del elemento que en ese momento se está visitando.

  • Debido a ello, podemos afirmar que las variables que aparecen en en cada cláusula for del conjunto por comprensión son identificadores cuantificados, ya que toman sus valores automáticamente y éstos están restringido a los valores que devuelva el iterable.

  • Además, estos identificadores cuantificados son locales al conjunto por comprensión, y sólo existen dentro de él.

  • Debido a lo anterior, esos identificadores cumplen estas dos propiedades:

    1. Se pueden renombrar (siempre de forma consistente) sin que el conjunto por comprensión cambie su significado.

      Por ejemplo, los dos conjuntos por comprensión siguientes son equivalentes, puesto que producen el mismo resultado:

      {x for x in (1, 2, 3)}
      {y for y in (1, 2, 3)}
    2. No se pueden usar fuera del conjunto por comprensión, ya que estarían fuera de su ámbito y no serían visibles.

      Por ejemplo, lo siguiente daría un error de nombre:

      >>> e = {x for x in (1, 2, 3)}
      >>> x       # Intento acceder a la 'x' del conjunto por comprensión
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      NameError: name 'x' is not defined

1.3 Operaciones

  • \underline{s} y \underline{o} son conjuntos, y \underline{x} es un valor cualquiera:

    Operación Resultado
    len(s) Número de elementos de \underline{s} (su cardinalidad)
    x\ in \ s True si \underline{x} pertenece a \underline{s}
    x\ not  in \ s True si \underline{x} no pertenece a \underline{s}
    s.isdisjoint(o) True si \underline{s} no tiene ningún elemento en común con \underline{o}
    s.issubset(o)
    s <= o
    True si \underline{s} es un subconjunto de \underline{o}
    s < o True si \underline{s} es un subconjunto propio de \underline{o}
    s.issuperset(o)
    s >= o
    True si \underline{s} es un superconjunto de \underline{o}
    s > o True si \underline{s} es un superconjunto propio de \underline{o}
Operación Resultado
s.union(o)
s | o
Unión de \underline{s} y \underline{o} (s \cup o)
s.intersection(o)
s & o
Intersección de \underline{s} y \underline{o} (s \cap o)
s.difference(o)
s - o
Diferencia de \underline{s} y \underline{o} (s \setminus o)
s.symmetric_difference(o)
s ^ o
Diferencia simétrica de \underline{s} y \underline{o} (s \vartriangle o)
s.copy() Devuelve una copia superficial de \underline{s}
  • Tanto set como frozenset admiten comparaciones entre conjuntos.

  • Suponiendo que a y b son conjuntos:

    • a == b si y sólo si cada elemento de a pertenece también a b, y viceversa; es decir, si cada uno es un subconjunto del otro.

    • a <= b si y sólo si a es un subconjunto de b (es decir, si cada elemento de a está también en b).

    • a < b si y sólo si a es un subconjunto propio de b (es decir, si a es un subconjunto de b, pero no es igual a b).

    • a >= b si y sólo si a es un superconjunto de b (es decir, si cada elemento de b está también en a).

    • a > b si y sólo si a es un superconjunto propio de b (es decir, si a es un superconjunto de b, pero no es igual a b).

1.4 Operaciones sobre conjuntos mutables

  • Estas tablas sólo se aplica a conjuntos mutables (o sea, al tipo set y no al frozenset):
Operación Resultado
s.update(o)
s |= o
Actualiza \underline{s} añadiendo los elementos de \underline{o}
s.intersection_update(o)
s &= o
Actualiza \underline{s} manteniendo sólo los elementos que están en \underline{s} y \underline{o}
s.difference_update(o)
s -= o
Actualiza \underline{s} eliminando los elementos que están en \underline{o}
s.symmetric_difference_update(o)
s ^= o
Actualiza \underline{s} manteniendo sólo los elementos que están en \underline{s} y \underline{o} pero no en ambos
Operación Resultado
s.add(x) Añade \underline{x} a \underline{s}
s.remove(x) Elimina \underline{x} de \underline{s} (produce KeyError si \underline{x} no está en \underline{s})
s.discard(x) Elimina \underline{x} de \underline{s} si está en \underline{s}
s.pop() Elimina y devuelve un valor cualquiera de \underline{s} (produce KeyError si \underline{s} está vacío)
s.clear() Elimina todos los elementos de \underline{s}

1.5 Recorrido de conjuntos

  • Como cualquier otro dato iterable, los conjuntos se pueden recorrer usando iteradores.

  • El orden en el que se recorren los elementos del conjunto no está determinado de antemano, es decir, que el iterador puede entregar los elementos del conjunto en cualquier orden:

  • Con un iterador:

    >>> s = {3, 1, 2}
    >>> it = iter(s)
    >>> next(it)
    1
    >>> next(it)
    2
    >>> next(it)
    3
  • Con un bucle for:

    >>> s = {3, 1, 2}
    >>> for e in s:
    ...     print(e)
    ...
    1
    2
    3
  • Aunque, a la vista de este ejemplo, pudiera parecer que el conjunto siempre se va a recorrer como si estuviese ordenado, no hay que confiar nunca en que eso se vaya a cumplir siempre.

2 Diccionarios (dict)

2.1 Definición

  • Un diccionario es una colección que almacena correspondencias (o asociaciones) entre valores.

  • Por tanto, los elementos de un diccionario son parejas de valores llamados clave y valor, y lo que hace el diccionario es almacenar las claves y el valor que le corresponde a cada clave.

  • Además, los elementos de un diccionario son datos mutables y, por tanto, los diccionarios también son mutables.

  • En consecuencia, los diccionarios NO son hashables.

  • Los diccionarios se pueden crear:

    • Con una pareja de llaves:

      {}

      que representan el diccionario vacío.

    • Encerrando entre llaves una lista de parejas clave:valor separadas por comas:

      {'juan': 4098, 'pepe': 4127}

      Esa es precisamente la forma normal de un diccionario y, por tanto, la que se usa cuando se visualiza desde el intérprete o se imprime con print.

    • Usando la función dict.

  • Por ejemplo:

    >>> v1 = {}                                          # diccionario vacío
    >>> v2 = dict()                                      # también diccionario vacío
    >>> v1 == v2
    True                                                 # son iguales
    >>> a = {'uno': 1, 'dos': 2, 'tres': 3}              # literal
    >>> b = dict(uno=1, dos=2, tres=3)                   # argumentos con nombre
    >>> c = dict([('dos', 2), ('uno', 1), ('tres', 3)])  # lista de tuplas
    >>> d = dict({'tres': 3, 'uno': 1, 'dos': 2})        # crea una copia
    >>> e = dict(zip(['uno', 'dos', 'tres'], [1, 2, 3])) # con dos iterables
    >>> a == b and b == c and c == d and d == e          # todos son iguales
    True
  • En memoria, los diccionarios se almacenan como tablas de dos columnas, la clave y el valor.

  • Por ejemplo, el siguiente diccionario:

    d = {'a': 1, 'b': 2, 'c': 3}

    se almacenaría de la siguiente forma según lo representa la herramienta Pythontutor:

    Diccionario almacenado en memoria
  • Las claves de un diccionario deben cumplir dos restricciones:

    1. Deben ser únicas en ese diccionario.

    2. Deben ser hashables.

Claves únicas
  • En un diccionario dado, cada clave sólo puede asociarse con un único valor.

  • Por tanto, en un diccionario no puede haber claves repetidas, es decir, que no puede haber dos elementos distintos con la misma clave.

  • Esto es así porque los elementos de un diccionario se identifican mediante su clave.

    Así que, para acceder a un elemento dentro de un diccionario, debemos indicar la clave del elemento.

  • Los tipos numéricos que se usen como claves obedecen las reglas normales de comparación numérica.

  • Por tanto, si dos números son considerados iguales (como 1 y 1.0) entonces se consideran la misma clave dentro del diccionario.

  • Si se intenta crear un diccionario con claves repetidas, sólo se almacenará uno de los elementos que tengan la misma clave (los demás se ignoran):

    >>> a = {'perro': 'dog', 'gato': 'cat', 'perro': 'doggy'}
    >>> a
    {'perro': 'doggy', 'gato': 'cat'}

    Como se ve, la clave 'perro' está repetida y, por tanto, sólo se almacena uno de los dos elementos con clave repetida, que siempre es el último que se va a insertar en el diccionario.

    En este caso, se almacena el elemento 'perro': 'doggy' y se ignora el 'perro': 'dog'.

Claves únicas
  • Por otra parte, las claves de un diccionario deben ser datos hashables.

  • Por tanto, no se pueden usar como clave una lista, un conjunto set, otro diccionario o cualquier otro dato mutable.

  • Si se intenta crear un diccionario con una clave no hashable, se produce un error TypeError:

    >>> {[1, 2]: 'a', [3, 4]: 'b'}         # Las listas no son hashables
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unhashable type: 'list'
    >>> {{1, 2}: 'a', {3, 4}: 'b'}         # Los conjuntos set tampoco
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unhashable type: 'set'
  • En cambio, sí se puede usar un frozenset, al ser hashable:

    >>> {frozenset({1, 2}): 'a', frozenset({3, 4}): 'b'}
    {frozenset({1, 2}): 'a', frozenset({3, 4}): 'b'}
  • Desde la versión 3.7 de Python, los elementos dentro de un diccionario se almacenan en el orden en el que se van insertando dentro del diccionario, aunque ese orden sólo tiene importancia en determinadas situaciones concretas.

  • Dos diccionarios se consideran iguales si ambos contienen los mismos elementos, es decir, si tienen las mismas parejas clave:valor, sin importar el orden en el que aparezcan los elementos en el diccionario:

    >>> {'a': 1, 'b': 2} == {'b': 2, 'a': 1}
    True
  • Para acceder a un elemento del diccionario se usa una sintaxis idéntica a la de la indexación, salvo que, en este caso, en lugar de usar el índice o posición del elemento, se usa la clave:

    >>> a = {'perro': 'dog', 'gato': 'cat'}
    >>> a['perro']
    'dog'
  • Si se intenta acceder a un elemento usando una clave que no existe, se lanza una excepción de tipo KeyError:

    >>> a['caballo']
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    KeyError: 'caballo'

2.2 Diccionarios por comprensión

  • También se pueden crear diccionarios por comprensión usando una sintaxis análoga a la de los conjuntos por comprensión (encerrando la expresión entre llaves), pero de forma que los elementos estén formados por parejas de clave y valor separados por :.

  • Su sintaxis es:

    dicc_comp ::= {clave:valor (for identificador in secuencia [if condición])^+}

    donde clave debe ser una expresión que devuelva valores hashables, y valor puede ser una expresión cualquiera.

  • Por ejemplo:

    >>> {x: x ** 2 for x in [1, 2, 3]}
    {1: 1, 2: 4, 3: 9}

    devuelve el diccionario que asocia a cada número 1, 2 y 3 con su correspondiente cuadrado.

  • El resultado es directamente un valor de tipo dict, no un iterador.

  • Los diccionarios por comprensión, al igual que los conjuntos por comprensión, las expresiones generadoras y las listas por comprensión, determinan su propio ámbito.

  • Ese ámbito abarca todo el diccionario por comprensión, de principio a fin.

  • Al recorrer el iterable, las variables van almacenando en cada iteración del bucle el valor del elemento que en ese momento se está visitando.

  • Debido a ello, podemos afirmar que las variables que aparecen en en cada cláusula for del diccionario por comprensión son identificadores cuantificados, ya que toman sus valores automáticamente y éstos están restringido a los valores que devuelva el iterable.

  • Además, estos identificadores cuantificados son locales al diccionario por comprensión, y sólo existen dentro de él.

  • Debido a lo anterior, esos identificadores cumplen estas dos propiedades:

    1. Se pueden renombrar (siempre de forma consistente) sin que el diccionario por comprensión cambie su significado.

      Por ejemplo, los dos diccionarios por comprensión siguientes son equivalentes, puesto que producen el mismo resultado:

      {x: x ** 2 for x in (1, 2, 3)}
      {y: y ** 2 for y in (1, 2, 3)}
    2. No se pueden usar fuera del diccionario por comprensión, ya que estarían fuera de su ámbito y no serían visibles.

      Por ejemplo, lo siguiente daría un error de nombre:

      >>> e = {x: x ** 2 for x in (1, 2, 3)}
      >>> x       # Intento acceder a la 'x' del diccinario por comprensión
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      NameError: name 'x' is not defined

2.3 Operaciones

  • \underline{d} y \underline{o} son diccionarios, \underline{c} es una clave válida y \underline{v} es un valor cualquiera:

    Operación Resultado
    d[c] Devuelve el valor asociado a \underline{c} en \underline{d} (lanza KeyError si \underline{c} no está en \underline{d})
    d[c] = v Asocia a la clave \underline{c} el valor \underline{v} en \underline{d} (crea el elemento dentro de \underline{d} si la clave \underline{c} no estaba ya en \underline{d})
    del \ d[c] Borra de \underline{d} el elemento cuya clave es \underline{c} (lanza KeyError si \underline{c} no está en \underline{d})
    len(d) Número de elementos de \underline{d}
    c\ in \ d True si \underline{d} contiene un elemento con clave \underline{c}
    c\ not  in \ d True si \underline{d} no contiene un elemento con clave \underline{c}
    d.clear() Elimina todos los elementos de \underline{d}
    d.copy() Devuelve una copia superficial de \underline{d}
Operación Resultado
d.get(c[, def]) Si la clave \underline{c} está en \underline{d}, devuelve d[c]; si no está, devuelve \underline{def}, que por defecto es None
d.pop(c[, def]) Si la clave \underline{c} está en \underline{d}, devuelve d[c] y elimina de \underline{d} el elemento con clave \underline{c}; si no está, devuelve \underline{def} (si no se pasa \underline{def} y la clave \underline{c} no está en \underline{d}, lanza un KeyError)
d.popitem() Selecciona un elemento de \underline{d} siguiendo un orden LIFO, lo elimina de \underline{d} y lo devuelve en forma de tupla (clave,valor) (lanza un KeyError si \underline{d} está vacío)
d.setdefault(c[, def]) Si la clave \underline{c} está en \underline{d}, devuelve d[c]; si no está, inserta en \underline{d} un elemento con clave \underline{c} y valor \underline{def}, y devuelve \underline{def} (que por defecto es None)
d.update(o) Actualiza \underline{d} con las parejas (clave, valor) de \underline{o}, sobreescribiendo las claves ya existentes en \underline{d}, y devuelve None

2.4 Recorrido de diccionarios

  • Como cualquier otro dato iterable, los diccionarios se pueden recorrer usando iteradores.

  • El orden en el que se recorren los elementos del diccionario es el orden en el que están almacenados los elementos dentro del diccionario que, como ya sabemos, desde la versión 3.7 de Python coincide con el orden en el que se han ido insertando los elementos en el diccionario.

  • Los iteradores creados sobre un diccionario, en realidad, recorren sus claves:

  • Con un iterador:

    >>> d = {'a': 1, 'b': 2}
    >>> it = iter(d)
    >>> next(it)
    'a'
    >>> next(it)
    'b'
  • Con un bucle for:

    >>> d = {'a': 1, 'b': 2}
    >>> for k in d:
    ...     print(k)
    ...
    a
    b
  • Si, además de acceder a las claves, necesitamos también acceder a los valores del diccionario mientras lo recorremos, podemos:

    • Acceder al valor a partir de la clave usando indexación:

      >>> for k in d:
      ...     print(k, d[k])
      ...
      a 1
      b 2
    • Usar el método items sobre el diccionario (el cual devuelve un objeto que, al iterar sobre él, genera una secuencia de tuplas (clave, valor)), y combinarlo con el desempaquetado de tuplas:

      >>> d.items()
      dict_items([('a', 1), ('b', 2)]
      >>> for k, v in d.items():
      ...     print(k, v)
      ...
      a 1
      b 2
  • Otros métodos útiles para recorrer un diccionario son keys y values.
  • keys devuelve un objeto que, al iterar sobre él, va generando las claves del diccionario sobre el que se invoca:

    >>> d.keys()
    dict_keys(['a', 'b'])
    >>> for k in d.keys():
    ...     print(k)
    ...
    a
    b
  • values devuelve un objeto que, al iterar sobre él, va generando los valores del diccionario sobre el que se invoca:

    >>> d.values()
    dict_values([1, 2])
    >>> for v in d.values():
    ...     print(v)
    ...
    1
    2
  • En la práctica, no resulta muy útil usar keys, ya que se puede hacer lo mismo recorriendo directamente el propio diccionario, como ya sabemos:

    >>> for k in d:
    ...     print(k)
    ...
    a
    b

3 Documentos XML

3.1 Definición

  • Los documentos XML se pueden considerar datos estructurados en forma de árbol (es decir, con una estructura jerárquica y, por tanto, no secuencial).

  • Por ejemplo, supongamos el siguiente documento XML:

<?xml version="1.0"?>
<raiz>
    <alumno numero="111">
        <dni>12312312A</dni>
        <nombre>
            <propio>Juan</propio>
            <apellidos>García González</apellidos>
        </nombre>
        <telefono>666555444</telefono>
        <nota>7</nota>
    </alumno>
    <alumno numero="222">
        <dni>44455566B</dni>
        <nombre>
            <propio>María</propio>
            <apellidos>Pérez Rodríguez</apellidos>
        </nombre>
        <telefono>696969696</telefono>
        <nota>9</nota>
    </alumno>
    <madre>
        <dni>22222222C</dni>
    </madre>
</raiz>
  • Podemos encontrarnos lo siguiente:

    • Lo que hay entre ángulos (como <raiz>) es una etiqueta.

    • Lo que hay entre dos etiquetas (como el 7 en <nota>7</nota>) es el texto de la etiqueta.

    • Lo que hay dentro de la etiqueta (como numero="111") es un atributo de la etiqueta.

      numero="111" es un atributo de la primera etiqueta <alumno>.

      numero es el nombre del atributo.

      111 es el valor del atributo numero.

    • Puede haber etiquetas con uno o varios atributos y etiquetas sin atributos.

  • Ese documento representaría la siguiente estructura jerárquica:

  • Se observa que:

    • Cada nodo representa una etiqueta del documento XML.

    • Si una etiqueta contiene a otra, su correspondiente nodo en el árbol tendrá un hijo que representa a la etiqueta que contiene.

    • Los hijos de un nodo están ordenados por la posición que ocupan dentro de su etiqueta padre.

3.2 Acceso

  • El módulo xml.etree.ElementTree (documentado en https://docs.python.org/3/library/xml.etree.elementtree.html) implementa una interfaz sencilla y eficiente para interpretar y crear datos XML.

  • Para importar los datos de un archivo XML, podemos hacer:

    import xml.etree.ElementTree as ET
    arbol = ET.parse('archivo.xml')
    raiz = arbol.getroot()
  • Si los datos XML se encuentran ya en una cadena, se puede hacer directamente:

    raiz = ET.fromstring(datos_en_una_cadena)
  • Los nodos del árbol se representan internamente mediante objetos de tipo Element, los cuales disponen de ciertos atributos y responden a ciertos métodos.

    ¡Cuidado! Aquí, cuando hablamos de atributos, nos referimos a información que contiene el objeto (una cualidad del objeto según el paradigma orientado a objetos) y a la cual se puede acceder usando el operador punto (.), no a los atributos que pueda tener una etiqueta según consten en el documento XML.

  • Los objetos de tipo Element disponen de los siguientes atributos:

    • tag: una cadena que representa a la etiqueta del nodo (por ejemplo: si la etiqueta es <alumno>, entonces tag contendrá 'alumno').

    • attrib: un diccionario que representa a los atributos de esa etiqueta en el documento XML.

    • text: una cadena que representa el contenido del nodo, es decir, el texto que hay entre <etiqueta> y </etiqueta>.

  • Por ejemplo, si tenemos la siguiente etiqueta en el documento XML:

    <telefono tipo="movil">666555444</telefono>

    y la variable nodo contiene el nodo (es decir, el objeto Element) que representa a dicha etiqueta en el árbol, entonces:

    • nodo.tag valdrá 'telefono'.

    • nodo.attrib valdrá {'tipo': 'movil'}.

    • nodo.text valdrá '666555444'.

  • En nuestro caso, raiz es un objeto de tipo Element que, además, representa al nodo raíz del árbol XML.

  • Por tanto, tendríamos lo siguiente:

    >>> raiz.tag
    'raiz'
    >>> raiz.attrib
    {}
    >>> raiz.text
    '\n    '              # ¿Por qué?
  • Los objetos Element son iterables. Por ejemplo, el nodo raíz tiene nodos hijos (nodos que «cuelgan» directamente del nodo raíz) sobre los cuales se puede iterar desde el objeto raiz:

    >>> for hijo in raiz:
    ...     print(hijo.tag, hijo.attrib)
    ...
    alumno {'numero': '111'}
    alumno {'numero': '222'}
    madre {}
  • Los hijos están anidados, y podemos acceder a nodos concretos a través de su índice (es decir, que los objetos Element son indexables):

    >>> raiz[0]       # el primer hijo directo de raiz
    <Element 'alumno' at 0x7f929c29cf90>
    >>> raiz[0][2]    # el tercer hijo directo del primer hijo directo de raiz
    <Element 'telefono' at 0x7f929c29d180>
    >>> raiz[0][2].text
    '666555444'
  • Los objetos de tipo Element disponen de métodos útiles para iterar recursivamente sobre todos los subárboles situados debajo de él (sus hijos, los hijos de sus hijos, y así sucesivamente).

  • Por ejemplo, el método iter devuelve un iterador que recorre todos los nodos del árbol desde el nodo actual (el nodo sobre el que se invoca al método) en un orden primero en profundidad.

  • Eso quiere decir que va visitando los nodos en el mismo orden en el que se encuentran escritos en el documento XML, incluyendo el propio nodo sobre el que se invoca.

  • Por ejemplo:

    >>> for nodo in raiz.iter():
    ...     print(nodo.tag)
    ...
    raiz
    alumno
    dni
    nombre
    propio
    apellidos
    telefono
    nota
    alumno
    dni
    nombre
    propio
    apellidos
    telefono
    nota
    madre
    dni
  • Si se le pasa una etiqueta como argumento, devolverá únicamente los nodos que tengan esa etiqueta:

    >>> for dni in raiz.iter('dni'):
    ...     print(dni.text)
    ...
    12312312A
    44455566B
    22222222C
  • El método findall devuelve una lista con los nodos que tengan una cierta etiqueta y que sean hijos directos del nodo sobre el que se invoca.

    Puede devolver una lista vacía si no hay nodos que cumplan la condición.

  • El método find devuelve el primer hijo directo del nodo sobre el que se invoca, siempre que tenga una cierta etiqueta indicada como argumento.

    Puede devolver None si el nodo no tiene ningún hijo con esa etiqueta.

  • El método get devuelve el valor de algún atributo de la etiqueta asociada a ese nodo:

    >>> for alumno in raiz.findall('alumno'):
    ...     numero = alumno.get('numero')
    ...     dni = alumno.find('dni').text
    ...     print(numero, dni)
    ...
    111 12312312A
    222 44455566B
  • Si la etiqueta no tiene el atributo indicado en el argumento de get, éste devuelve None o el valor que se haya indicado en el segundo argumento:

    >>> alumno = raiz[0]
    >>> alumno.get('numero')
    111
    >>> alumno.get('altura')
    None
    >>> alumno.get('altura', 0)
    0
    >>> alumno.get('numero', 0)
    111
  • También disponemos de la función dump que devuelve la cadena correspondiente al nodo que se le pase como argumento:

    >>> ET.dump(raiz[0][2])
    <telefono>666555444</telefono>
  • Para una especificación más sofisticada de los elementos a encontrar, se pueden usar las expresiones XPath con los métodos find y findall:

    Sintaxis Significado
    etiqueta Selecciona todos los nodos hijo con la etiqueta etiqueta. Por ejemplo, pepe selecciona todos los nodos hijo llamados pepe, y pepe/juan selecciona todos los nietos llamados juan en todos los hijos llamados pepe.
    * Selecciona todos los nodos hijo inmediatos. Por ejemplo, */pepe selecciona todos los nietos llamados pepe.
    . Selecciona el nodo actual. Se usa, sobre todo, al principio de la ruta para indicar que es una ruta relativa.
    // Selecciona todos los subnodos en cualquier nivel por debajo de un nodo. Por ejemplo, .//pepe selecciona todos los nodos pepe que haya en todo el árbol.
    .. Selecciona el nodo padre. Devuelve None si se intenta acceder a un ancestro del nodo de inicio (aquel sobre el que se ha llamado al método find).
  • Continuación de las expresiones XPath:

    Sintaxis Significado
    [@atrib] Selecciona todos los nodos que tienen el atributo atrib.
    [@atrib='valor'] Selecciona todos los nodos que tienen el valor valor en el atributo atrib. El valor no puede contener apóstrofes.
    [@atrib!='valor'] Selecciona todos los nodos que tienen el valor valor en el atributo atrib. El valor no puede contener apóstrofes.
    [etiqueta] Selecciona todos los nodos que tienen un hijo inmediato llamado etiqueta.
    [posición] Selecciona todos los nodos situados en cierta posición. Ésta puede ser un entero (1 es la primera posición), la expresión last() (la última posición) o una posición relativa a la última posición (por ejemplo, last() - 1).
Ejemplos
# Los nodos de nivel más alto:
>>> raiz.findall(".")
[<Element 'raiz' at 0x7f929c29cf40>]

# Los nietos 'dni' de los hijos 'alumno' de los nodos de nivel más alto:
>>> raiz.findall("./alumno/dni")
[<Element 'dni' at 0x7f929c29d040>,
 <Element 'dni' at 0x7f929c29d270>]

# Lo de antes equivale a hacer (porque el nodo actual es el raíz):
>>> raiz.findall("alumno/dni")
[<Element 'dni' at 0x7f929c29d040>,
 <Element 'dni' at 0x7f929c29d270>]

# Los nodos con numero='111' que tienen un hijo 'dni':
>>> raiz.findall(".//dni/..[@numero='111']")
[<Element 'alumno' at 0x7f929c29cf90>]

# Antes de // hay que poner algo que indique el nodo debajo del
# cual se va a buscar:
>>> raiz.findall("//dni/..[@numero='111']")
SyntaxError: cannot use absolute path on element
Ejemplos
# Todos los nodos 'dni' del árbol completo:
>>> raiz.findall('.//dni')
[<Element 'dni' at 0x7f547a0e9950>,
 <Element 'dni' at 0x7f547a0e9b80>,
 <Element 'dni' at 0x7f547a0e9e00>]

# Sólo los DNIs que estén por debajo de un nodo 'madre'
# en cualquier nivel:
>>> raiz.findall('madre//dni')
[<Element 'dni' at 0x7f547a0e9e00>]

# Los nodos 'dni' que son hijos de los nodos con numero='111':
>>> raiz.findall(".//*[@numero='111']/dni")
[<Element 'dni' at 0x7f929c29d040>]

# Los nodos 'alumno' que son hijos segundos de sus padres:
>>> raiz.findall(".//alumno[2]")
[<Element 'alumno' at 0x7f929c29d220>]

# Los nodos 'alumno' hijos directos del actual que tienen un hijo 'nota':
>>> raiz.findall("./alumno[nota]")
[<Element 'alumno' at 0x7f929c29cf90>,
 <Element 'alumno' at 0x7f929c29d220>]

# Lo de antes equivale a hacer (porque el nodo actual es el raíz):
>>> raiz.findall("alumno[nota]")
[<Element 'alumno' at 0x7f929c29cf90>,
 <Element 'alumno' at 0x7f929c29d220>]

3.3 Modificación

  • ElementTree proporciona una forma sencilla de crear documentos XML y escribirlos en archivos.

  • Para ello, usamos el método write.

  • Una vez creado, un objeto Element puede manipularse directamente:

    • Cambiando los atributos del objeto, como text o attrib.

    • Cambiando los atributos de la etiqueta a la que representa el objeto, con el método set.

    • Añadiendo nuevos hijos con los métodos append o insert.

    • Eliminando hijos con el método remove.

  • Por ejemplo, supongamos que queremos sumarle 1 a la nota de cada alumno y añadir un atributo modificado a la etiqueta nota:

    >>> for nota in raiz.iter('nota'):
    ...     nueva_nota = int(nota.text) + 1
    ...     nota.text = str(nueva_nota)
    ...     nota.set('modificado', 'si')
    ...
    >>> arbol.write('salida.xml')
  • Nuestro XML tendría ahora el siguiente aspecto:

    <?xml version="1.0"?>
    <raiz>
        <alumno numero="111">
            <dni>12312312A</dni>
            <nombre>
                <propio>Juan</propio>
                <apellidos>García González</apellidos>
            </nombre>
            <telefono>666555444</telefono>
            <nota modificado="si">8</nota>
        </alumno>
        <alumno numero="222">
            <dni>44455566B</dni>
            <nombre>
                <propio>María</propio>
                <apellidos>Pérez Rodríguez</apellidos>
            </nombre>
            <telefono>696969696</telefono>
            <nota modificado="si">10</nota>
        </alumno>
        <madre>
            <dni>22222222C</dni>
        </madre>
    </raiz>
  • Podemos insertar nuevos nodos hijos de un determinado nodo con los métodos append o insert, como si el nodo fuese una lista.

  • Para ello, primero normalmente crearemos el nodo que vamos a insertar usando Element(etiqueta).

  • Por ejemplo, añadimos el teléfono a la única madre que tenemos en el documento XML:

    telefono = ET.Element('telefono')
    telefono.text = '956366666'
    madre = raiz.find('madre')
    madre.append(telefono)
  • Tras estas operaciones, ahora tendríamos:

  • Podríamos haber usado insert en lugar de append para cambiar la posición donde situar el nodo hijo.

  • Por ejemplo, si queremos situar el teléfono antes que el DNI, podríamos hacer:

    telefono = ET.Element('telefono')
    telefono.text = '956366666'
    madre = raiz.find('madre')
    madre.insert(0, telefono)
  • Tras estas operaciones, ahora tendríamos:

  • Podemos eliminar elementos con el método remove. Por ejemplo, supongamos que queremos eliminar todos los alumnos con una nota inferior a 9:

    >>> for alumno in raiz.findall('alumno'):
    ...     # se usa findall para que no afecte el borrado durante el recorrido
    ...     nota = int(alumno.find('nota').text)
    ...     if nota < 9:
    ...         raiz.remove(alumno)
    ...
    >>> arbol.write('salida.xml')
  • Tener en cuenta que la modificación concurrente mientras se hace una iteración puede dar problemas, lo mismo que ocurre cuando se modifican listas o diccionarios mientras se itera sobre ellos.

  • Por ello, el ejemplo primero recoge todos los elementos con findall y sólo entonces itera sobre la lista que devuelve.

  • Si usáramos iter en lugar de findall se podrían dar problemas debido a que iter va devolviendo perezosamente los nodos (es un iterador) y el conjunto de nodos que devuelve podría verse afectado por los borrados.

  • Nuestro XML tendría ahora el siguiente aspecto:

    <?xml version="1.0"?>
    <raiz>
        <alumno numero="222">
            <dni>44455566B</dni>
            <nombre>
                <propio>María</propio>
                <apellidos>Pérez Rodríguez</apellidos>
            </nombre>
            <telefono>696969696</telefono>
            <nota modificado="si">10</nota>
        </alumno>
        <madre>
            <dni>22222222C</dni>
        </madre>
    </raiz>
  • Si lo que queremos es mover un nodo (es decir, cambiar un nodo de sitio), podemos combinar los efectos de append e insert con remove.

  • Por ejemplo, si queremos mover la etiqueta <madre> dentro de la etiqueta <alumno>, podríamos hacer:

    madre = raiz.find('madre')
    raiz.remove(madre)
    alumno = raiz.find('alumno')
    alumno.append(madre)
  • Nuestro XML tendría ahora el siguiente aspecto:

    <?xml version="1.0"?>
    <raiz>
        <alumno numero="222">
            <dni>44455566B</dni>
            <nombre>
                <propio>María</propio>
                <apellidos>Pérez Rodríguez</apellidos>
            </nombre>
            <telefono>696969696</telefono>
            <nota modificado="si">10</nota>
            <madre>
                <dni>22222222C</dni>
            </madre>
        </alumno>
    </raiz>
  • La función SubElement también proporciona una forma muy conveniente de crear sub-elementos de un elemento dado:

    >>> a = ET.Element('a')
    >>> b = ET.SubElement(a, 'b')
    >>> c = ET.SubElement(a, 'c')
    >>> d = ET.SubElement(c, 'd')
    >>> ET.dump(a)
    <a><b /><c><d /></c></a>

Bibliografía

Python Software Foundation. n.d. “Sitio Web de Documentación de Python.” https://docs.python.org/3.