5.6. Métodos de clase especiales

Además de los métodos normales, existe un cierto número de métodos especiales que pueden definir las clases de Python. En lugar de llamarlos directamente desde su código (como los métodos normales), los especiales los invoca Python por usted en circunstancias particulares o cuando se use una sintaxis específica.

Como pudo ver en la sección anterior, los métodos normales van mucho más allá de simplemente actuar como cápsula de un diccionario en una clase. Pero los métodos normales por sí solos no son suficientes, porque hay muchas cosas que puede hacer con diccionarios aparte de llamar a sus métodos. Para empezar, en lugar de usar get y set para trabajar con los elementos, puede hacerlo con una sintaxis que no incluya una invocación explícita a métodos. Aquí es donde entran los métodos especiales de clase: proporcionan una manera de convertir la sintaxis-que-no-llama-a-métodos en llamadas a métodos.

5.6.1. Consultar y modificar elementos

Ejemplo 5.12. El método especial __getitem__

    def __getitem__(self, key): return self.data[key]
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__getitem__("name") 1
'/music/_singles/kairo.mp3'
>>> f["name"]             2
'/music/_singles/kairo.mp3'
1 El método especial __getitem__ parece bastante sencillo. Igual que los métodos normales clear, keys y values, simplemente se dirige al diccionario para devolver su valor. Pero, ¿cómo se le invoca? Bien, puede llamar a __getitem__ directamente, pero en la práctica no es lo que hará; lo hago aquí para mostrarle cómo trabaja. La manera adecuada de usar __getitem__ es hacer que Python lo llame por usted.
2 Se parece a la sintaxis que usaríamos para obtener un valor del diccionario, y de hecho devuelve el valor esperado. Pero he aquí el eslabón perdido: internamente, Python ha convertido esta sintaxis en una llamada al método f.__getitem__("name"). Por eso es __getitem__ un método especial; no sólo puede invocarlo usted, sino que puede hacer que Python lo invoque usando la sintaxis adecuada.

Por supuesto, Python tiene un método especial __setitem__ que acompaña a __getitem__, como mostramos en el siguiente ejemplo.

Ejemplo 5.13. El método especial __setitem__

    def __setitem__(self, key, item): self.data[key] = item
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__setitem__("genre", 31) 1
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':31}
>>> f["genre"] = 32            2
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':32}
1 Igual que con el método __getitem__, __setitem__ simplemente delega en el diccionario real self.data para hacer su trabajo. E igual que __getitem__, normalmente no lo invocará de forma ordinaria de esta manera; Python llama a __setitem__ cuando usa la sintaxis correcta.
2 Esto parece sintaxis normal de diccionario, excepto que por supuesto f realmente es una clase que intenta por todos los medios aparentar ser un diccionario, y __setitem__ es parte esencial de la mascarada. Esta línea de código en realidad invoca a f.__setitem__("genre", 32) tras las cortinas.

__setitem__ es un método especial de clase debido a que lo invocan por usted, pero sigue siendo un método de clase. Con la misma facilidad con que definió el método __setitem__ en UserDict, puede redefinirlo en la clase descendiente para sustituir el método del ancestro. Esto le permite definir clases que actúan como diccionarios de cierta manera pero con su propio comportamiento más allá del de un diccionario estándar.

Este concepto es la base de todo el framework que está estudiando en este capítulo. Cada tipo de fichero puede tener una clase de manejo que sepa cómo acceder a los metadatos de ese tipo particular de fichero. Una vez se conocen ciertos atributos (como el nombre del fichero y su emplazamiento), la clase manejadora sabe cómo derivar otros atributos de forma automática. Esto se hace reemplazando el método __setitem__, buscando ciertas claves, y añadiendo procesamiento adicional cuando se las encuentra.

Por ejemplo, MP3FileInfo desciende de FileInfo. Cuando se asigna el name de una MP3FileInfo, no nos limitamos a asignar el valor de la clave name (como hace el ancestro FileInfo); también se busca en el propio fichero etiquetas MP3 y da valor a todo un juego de claves. El siguiente ejemplo le muestra cómo funciona esto.

Ejemplo 5.14. Reemplazo de __setitem__ en MP3FileInfo

    def __setitem__(self, key, item):         1
        if key == "name" and item:            2
            self.__parse(item)                3
        FileInfo.__setitem__(self, key, item) 4
1 Observe que este método __setitem__ se define exactamente de la misma manera que en el método ancestro. Esto es importante, ya que Python llamará al método por usted, y espera que esté definido con un cierto número de argumentos (técnicamente, los nombres de los argumentos no importan; sólo su número).
2 Aquí está el quid de toda la clase MP3FileInfo: si asignamos un valor a la clave name, queremos que se hagan algunas cosas extra.
3 El procesamiento extra que se hace para los name se encapsula en el método __parse. Éste es otro método de clase definido en MP3FileInfo, y cuando lo invoca, lo hace calificándolo con self. Una llamada simplemente a __parse hará que se busque una función normal definida fuera de la clase, que no es lo que deseamos. Invocar a self.__parse buscará un método definido en la clase. Esto no es nuevo; hemos hecho referencia a atributos de datos de la misma manera.
4 Tras hacer este procesamiento extra, querremos llamar al método del ancestro. Recuerde que esto no lo hace Python por usted nunca; debe hacerlo manualmente. Fíjese en que está llamado al ancestro inmediato, FileInfo, incluso aunque éste no define un método __setitem__. Esto es correcto, ya que Python escalará en el árbol genealógico hasta que encuentre una clase con el método que busca, de manera que esta línea de código acabará encontrando e invocando al __setitem__ definido en UserDict.
nota
Cuando se accede a atributos de datos dentro de una clase, necesitamos calificar el nombre del atributo: self.atributo. Cuando llamamos a otros métodos dentro de una clase, necesitamos calificar el nombre del método: self.método.

Ejemplo 5.15. Dar valor al name de una MP3FileInfo

>>> import fileinfo
>>> mp3file = fileinfo.MP3FileInfo()                   1
>>> mp3file
{'name':None}
>>> mp3file["name"] = "/music/_singles/kairo.mp3"      2
>>> mp3file
{'album': 'Rave Mix', 'artist': '***DJ MARY-JANE***', 'genre': 31,
'title': 'KAIRO****THE BEST GOA', 'name': '/music/_singles/kairo.mp3',
'year': '2000', 'comment': 'http://mp3.com/DJMARYJANE'}
>>> mp3file["name"] = "/music/_singles/sidewinder.mp3" 3
>>> mp3file
{'album': '', 'artist': 'The Cynic Project', 'genre': 18, 'title': 'Sidewinder', 
'name': '/music/_singles/sidewinder.mp3', 'year': '2000', 
'comment': 'http://mp3.com/cynicproject'}
1 Primero, creamos una instancia de MP3FileInfo, sin pasarle el nombre de un fichero (podemos hacerlo así porque el argumento filename del método __init__ es opcional). Como MP3FileInfo no tiene método __init__ propio, Python escala en el árbol genealógico y encuentra el método __init__ de FileInfo. Este método __init__ llama manualmente al __init__ de UserDict y establece el valor de la clave name como filename, que es None, ya que no pasó un nombre de fichero. Por tanto, mp3file aparenta ser un diccionario con una clave, name, cuyo valor es None.
2 Ahora empieza lo divertido. Poner un valor a la clave name de mp3file dispara el método __setitem__ de MP3FileInfo (no UserDict), lo que se da cuenta de que está dándole a name un valor real e invoca a self.__parse. Aunque no hemos mirado aún el método __parse, puede ver por la salida que establece varias otras claves: album, artist, genre, title, year, y comment.
3 Modificar la clave name reproducirá este proceso: Python llama a __setitem__, que a su vez llama a self.__parse, que pone valor a las otras claves.