~adeodato/ code/ minirok/ csl2.blog/ entries/ 2008/ 02/ 20/ Playlist (2): estructura interna de los datos (y moraleja)

Playlist (2): estructura interna de los datos (y moraleja)

La lista de reproducción es, a todas todas, un simple array de ítems. Con Qt3, el orden de esta lista se guardaba en las estructuras internas (y no directamente accesibles) de la clase QListView. Esta clase ofrecía iteradores para acceder a los ítems en orden, y estos ítems tenían métodos como itemAbove() e itemBelow() para, dado por ejemplo la pista actual en reproducción, poder saltar a la pista siguiente o la anterior.

Ahora, con Qt4, la responsibilidad de estas estructuras internas corresponde al modelo, es decir, a nosotros mismos. Como implementadores, tenemos que almacenar la lista en una estructura o estructuras que, por una parte, proporcionen toda la funcionalidad necesaria y, por otra, sean razonablemente eficientes. Para ello, hay que saber primero qué tipo de acceso a la estructura vamos a necesitar.

Se necesita, en primer lugar, un acceso por índice numérico, y se necesita que sea rápido. Como se vio en la entrada anterior, el método data() del modelo recibe una fila y una columna; y dicho método se invoca muchas muchas veces por la vista, así que debe de ser realmente rápido. Asimismo, todo el resto de interacciones entre la vista y el modelo se basan en número de fila. Por esto, el almacén principal de los ítems es una lista de Python, llamada _itemlist, y que se debe modificar únicamente mediante dos métodos insert_items() y remove_items():

  class Playlist:

      def insert_items(self, position, items):
          amount = len(items)
          self.beginInsertRows(QtCore.QModelIndex(),
                               position, position + amount - 1)
          self._itemlist[position:0] = items
          self._row_count += amount
          self.endInsertRows()

      def remove_items(self, position, amount):
          self.beginRemoveRows(QtCore.QModelIndex(),
                               position, position + amount - 1)
          self._itemlist[position:position+amount] = []
          self._row_count -= amount
          self.endRemoveRows()

          return items

(Las funciones beginRemoveRows() y endRemoveRows() son funciones heredadas de la clase base, y han de ser llamadas para indicar a la vista que va a haber modificaciones en la lista de ítems.)


Por otra parte, en distintas partes el modelo también necesita la operación inversa: a partir de un determinado ítem, averiguar su posición. En principio esto se podría hacer con el método index() de las listas de Python, pero tendría complejidad O(n); y a nosotros nos conviene O(1), sobre todo con vistas a listas grandes (siendo n el tamaño de la lista).

Mi primera idea, que implementé y mantuve hasta ayer mismo, fue acoplar un diccionario a la lista de ítems. Es decir, mantener una estructura adicional que mapeara ítems a posiciones: la obtención de la posición de cualquier ítem sería O(1), a cambio de incrementar la complejidad temporal de las dos funciones explicadas anteriormente, para mantener el diccionario actualizado. (Este incremento, lamentablemente, es O(m+n) tanto en el caso peor como en el mejor, ya que hay que iterar sobre todos los elementos del diccionario y mirar si hay que actualizarlos uno a uno.)


Sin embargo, al comenzar a preparar esta entrada, lo miré todo con nuevos ojos, y se me ocurrió una manera más sencilla y eficiente: mantener un atributo con la posición en los ítems mismos.

Al principio de comenzar a migrar la lista, me planteé los ítems como un simple contenedor para los tags del fichero, y que todo el resto de información debería almacenarse en el modelo en sí. Y, por tanto, implementé el diccionario acoplado. Pero algo más tarde cambié de idea y comencé a incluir algunos otros atributos en los ítems, de cuya actualización el encargado era el modelo. Sin embargo, se me olvidó considerar este nuevo paradigma para almacenar la posición de cada ítem en la lista.

Una vez hecho, para mantener el atributo position actualizado sólo hay que añadir un poco de código a los dos métodos anteriores, así:

  class Playlist:

      def insert_items(self, position, items):
          [...]
          for item in self._itemlist[position:]:
              item.position += amount

          for i, item in enumerate(items):
              item.position = position + i
          [...]

      def remove_items(self, position, amount):
          [...]
          for item in items:
              item.position = None

          for item in self._itemlist[position+amount:]:
              item.position -= amount
          [...]

Este nuevo cambio tiene complejidad O(n+m) en el caso peor, pero O(m) en el caso mejor (siendo m el número de items a insertar o eliminar). El cambió está implementado en la revisión 549 (diff).


Moraleja: siempre es bueno examinar tras un tiempo las soluciones que hemos dado a un problema, por ejemplo describiendo por escrito sus detalles. Muy a menudo encontraremos que pasado un tiempo nuestro cerebro es capaz de ver maneras más eficientes o elegantes de hacer las cosas.