Nivel de dificultad:5 sobre 5
A lo largo del libro, has visto ejemplos de ``métodos especiales'' --ciertos métodos ``mágicos'' a los que Python invoca cuando utilizas cierta sintaxis. Mediante el uso de métodos especiales, tus clases pueden actuar como conjuntos, como diccionarios, como funciones, como iteradores, o incluso como números. Este apéndice sirve tanto como referencia de los métodos especiales que hemos visto ya, como una breve introducción a algunos de los más esotéricos.
Si has leído la introducción a las clases, en el capítulo 7, ya has visto el método especial más común: el método __init__(). La mayoría de las clases que escribo necesitan alguna inicialización. Hay algunos otros métodos especiales que son especialmente útiles para depurar tus clases.
Línea | Tu quieres... | Por eso escribes... | Y Python llama... |
1 | inicializar instancia | x = MiClase() | x.__init__() |
2 | representación oficial | repr(x) | x.__repr__() |
de una cadena de | |||
caracteres | |||
3 | valor informal como | str(x) | x.__str__() |
cadena de caracteres | |||
4 | valor informal como | bytes(x) | x.__bytes__() |
array de bytes | |||
5 | el valor formateado | format(x, format_spec) | x.__format__(format_spec) |
como una cadena de | |||
caracteres |
En el capítulo, 7, sobre iteradores viste cómo construir un iterador desde cero utilizando los métodos __iter__() y __next__().
Línea | Tu quieres... | Por eso escribes... | Y Python llama... |
1 | para iterar a través | iter(seq) | seq.__iter__() |
de una secuencia | |||
2 | para obtener el siguiente | next(seq) | seq.__next__() |
valor de un iterador | |||
3 | para crear un iterador | reversed(seq) | seq.__reversed__() |
con orden inverso |
Como viste en el capítulo de los iteradores un bucle for puede actuar sobre un iterador:
for x in seq:
print(x)
Python 3 llamará a seg.__iter__() para crear un iterador, luego llamará al método __next__() sobre este iterador para obtener cada uno de los valores de x. Cuando el método __next__() eleve la excepción StopIteration, el bucle for finaliza correctamente.
Línea | Tu quieres... | Por eso escribes... | Y Python llama... |
1 | para obtener un atrib. | x.mi_propiedad | x.__getattribute__( |
calculado (incondic.) | 'mi_propiedad') | ||
2 | para obtener un atrib. | x.mi_propiedad | x.__getattr__( |
calculado por defecto | 'mi_propiedad') | ||
3 | para dar valor a un | x.mi_propiedad = valor | x.__setattr__( |
atributo | 'mi_propiedad', valor) | ||
4 | para borrar un atrib. | del x.mi_propiedad | x.__delattr__( |
5 | para listar todos los | dir(x) | x.__dir__() |
atributos y métodos | 'mi_propiedad') |
La distinción entre __getattr__() y __getattribute__() es sutil pero fundamental. Puedo explicarlo con dos ejemplos:
class Dynamo:
def __getattr__(self, key):
if key == 'color':
return 'PapayaWhip'
else:
raise AttributeError
»> dyn = Dynamo() »> dyn.color 'PapayaWhip' »> dyn.color = 'LemonChiffon' »> dyn.color 'LemonChiffon'
El método __getattribute__(), sin embargo, es absoluto e incondicional:
class SuperDynamo:
def __getattribute__(self, key):
if key == 'color':
return 'PapayaWhip'
else:
raise AttributeError
»> dyn = SuperDynamo() »> dyn.color 'PapayaWhip' »> dyn.color = 'LemonChiffon' »> dyn.color 'PapayaWhip'
Si tu clase define el método __getattributes__(), probablemente también debería definir __setattr__() para coordinarse entre ellos con el fin de llevar la cuenta de los valores de los atributos. De otro modo, cualquier atributo que modifiques después de crear la instancia desaparecerá en un agujero netro.
Tienes que ser súpercuidadoso con el método __getattributes__() puesto que también se llama cuando Python busca el nombre de un método de la clase.
class Rastan:
def __getattribute__(self, key):
raise AttributeError
def swim(self):
pass
»> hero = Rastan() »> hero.swim() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in __getattribute__ AttributeError
Puedes hace que una instancia de una clase se pueda llamar directamente como si fuese una función. Para ello, se debe definir el método __call__().
Línea | Tu quieres... | Por eso escribes... | Y Python llama... |
para ``llamar'' a una instancia | miInstancia() | miInstancia.__call__() | |
como si fuera una función |
El módulo zipfile utiliza esto para definir una clase que puede desencriptar y encriptar un fichero zip. El algoritmo de desencriptación zip requiere que almacenes el estado durante la misma. Al definir el desencriptador como una clase, puedes mantener el estado dentro de la instancia de la clase. El estado se inicializa en el método __init__() y se actualiza según se va desencriptando el fichero. Pero puesto que la clase se puede ``llamar'' como una función, puedes pasar la instancia como primer argumento de la función map(), así:
# fragmento de zipfile.py
class _ZipDecrypter:
.
.
.
def __init__(self, pwd):
self.key0 = 305419896
self.key1 = 591751049
self.key2 = 878082192
for p in pwd:
self._Subir adateKeys(p)
def __call__(self, c): assert isinstance(c, int) k = self.key2 | 2 c = c ^ (((k * (k^1)) » 8) & 255) self._Subir adateKeys(c) return c . . . zd = _ZipDecrypter(pwd) bytes = zef_file.read(12) h = list(map(zd, bytes[0:12]))
Si tu clase actúa como un contenedor para conjuntos de valores --esto es, si tiene sentido si tu clase ``contiene'' un valor-- entonces, probablemente deberías definirla utilizando los métodos especiales que la hacen comportarse como si fuera un conjunto.
Línea | Tu quieres... | Por eso escribes... | Y Python llama... |
el número de elementos | len(s) | s.__len__() | |
para conocer si contiene un | x in s | s.__contains__(x) | |
valor específico |
El módulo cgi utiliza estos métodos en su clase FileStorage, que representa todos los campos de formularios o parámetros de consulta enviados por una página web dinámica.
# Un script que responde a http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()
if 'q' in fs:
do_search()
# Un fragmento de cgi.py que explica cómo funciona class FieldStorage: . . . def __contains__(self, key): if self.list is None: raise TypeError('not indexable') return any(item.name == key for item in self.list)
def __len__(self): return len(self.keys())
Si se extiende la sección anterior un poco, puedes definir clases que no solamente respondan al operador in y a la función len(), sino que también funcionen como diccionarios, devolviendo valores basados en claves.
Tu quieres... | Por eso escribes... | Y Python llama... |
obtener valor a partir de clave | x[clave] | x.__getitem__(clave) |
asignar un valor a una clave | x[clave] = valor | s.__setitem__(clave, valor) |
borra una pareja clave-valor | del x[clave] | x.__delitem__(clave) |
dar un valor por defecto a | x[claveNoExist] | x.__missing__(claveNoExist) |
las claves no incluidas |
La clase FieldStorage del módulo cgi también define estos métodos especiales, lo que significa que puedes hacer cosas como esta:
# Un script que responde a http://example.com/search?q=cgi
import cgi
fs = cgi.FieldStorage()
if 'q' in fs:
do_search(fs['q'])
# fragmento de cgi.py que muestra cómo funciona class FieldStorage: . . . def __getitem__(self, key): if self.list is None: raise TypeError('not indexable') found = [] for item in self.list: if item.name == key: found.append(item) if not found: raise KeyError(key) if len(found) == 1: return found[0] else: return found
Si se utilizan los métodos apropiados, puedes definir tus propias clases que funcionan como números. Puedes sumar, restar, y ejecutar otras operaciones matemáticas sobre los objetos de tu clase. Por ejemplo, así es cómo se implementan las facciones --La clase Fraction implementa estos métodos especiales, luego puedes hacer cosas como esta.
»> from fractions import Fraction
»> x = Fraction(1, 3)
»> x / 3
Fraction(1, 9)
Tu quieres... | Por eso escribes... | Y Python llama... |
suma | x + y | x.__add__(y) |
resta | x - y | x.__sub__(y) |
multiplicación | x * y | x.__mul__(y) |
división | x / y | x.__truediv__(y) |
división de suelo | x // y | x.__floordiv__(y) |
módulo (resto) | x % y | x.__mod__(y) |
división de suelo y módulo | divmod(x, y) | x.__divmod__(y) |
elevar a potencia | x ** y | x.__pow__(y) |
desplaz. de bit a la izda | x « y | x.__lshift__(y) |
desplaz. de bit a la dcha | x » y | x.__rshift__(y) |
``and'' bit a bit | x & y | x.__and__(y) |
``or'' bit a bit | x | y | x.__or__(y) |
``xor'' bit a bit | x ˆ y | x.__xor__(y) |
Esto funciona si x es una instancia de una clase que implementa estos métodos. Pero ¿Qué pasa si no los implementa? O peor, sí los implementa, pero no pude manejar algunos tipos de parámetros. Por ejemplo:
»> from fractions import Fraction
»> x = Fraction(1, 3)
»> 1 / x
Fraction(3, 1)
Este ejemplo no toma una fracción y la divide por un entero (como en el ejemplo anterior). Aquél ejemplo era simple: x / 3 llama a x.__truediv__(3), y este método de la clase Fraction calcula el resultado. Pero los números enteros no tienen operaciones aritméticas para manejar las fracciones. ¿Por qué funciona el ejemplo?
Existe un segundo conjunto de métodos especiales aritméticos con los operadores reflejados. Dada una operación que toma dos operandos (por ejemplo: x / y), hay dos modos de tratarla:
El conjunto de métodos que se han visto anteriormente cubren la primera aproximación: dado x / y, proporcionan una forma para que x diga ``sé cómo dividirme por y''.
El siguiente conjunto de métodos especiales cubren la segunda parte: proporcionan una forma a y para decir ``sé que soy el denominador y sé dividir a x''.
Tu quieres... | Por eso escribes... | Y Python llama... |
suma | x + y | y.__radd__(y) |
resta | x - y | y.__rsub__(y) |
multiplicación | x * y | y.__rmul__(y) |
división | x / y | y.__rtruediv__(y) |
división de suelo | x // y | y.__rfloordiv__(y) |
módulo (resto) | x % y | y.__rmod__(y) |
división de suelo y módulo | divmod(x, y) | y.__rdivmod__(y) |
elevar a potencia | x ** y | y.__rpow__(y) |
desplaz. de bit a la izda | x « y | y.__rlshift__(y) |
desplaz. de bit a la dcha | x » y | y.__rrshift__(y) |
``and'' bit a bit | x & y | y.__rand__(y) |
``or'' bit a bit | x | y | y.__ror__(y) |
``xor'' bit a bit | x ˆ y | y.__rxor__(y) |
Pero, ¡Espera! Que hay más. Si quieres hacer operaciones en la asignación, como x /= 3, hay aún más métodos especiales que puedes definir.
Tu quieres... | Por eso escribes... | Y Python llama... |
suma in-place | x + y | x.__iadd__(y) |
resta in-place | x - y | x.__isub__(y) |
multiplicación in-place | x * y | x.__imul__(y) |
división in-place | x / y | x.__itruediv__(y) |
división de suelo in-place | x // y | x.__ifloordiv__(y) |
módulo (resto) in-place | x % y | x.__imod__(y) |
elevar a potencia in-place | x ** y | x.__ipow__(y) |
desplaz. de bit a la izda in-place | x « y | x.__ilshift__(y) |
desplaz. de bit a la dcha in-place | x » y | x.__irshift__(y) |
``and'' bit a bit in-place | x & y | x.__iand__(y) |
``or'' bit a bit in-place | x | y | x.__ior__(y) |
``xor'' bit a bit in-place | x ˆ y | x.__ixor__(y) |
Nota: los métodos in-place no son requeridos. Si no defines un método de este tipo para una operación particular, Python intentará los métodos anteriores. Por ejemplo para ejecutar la expresión x /= y, Python intentará:
Solamente necesitas definir métodos in-place si quieres algún tipo de optimización para este tipo de operadores. De otro modo Python reformulará la operación para utilizar el operador habitual y realizar la asignación después.
Existen también algunas operaciones matemáticas ``unarias''.
Tu quieres... | Por eso escribes... | Y Python llama... |
número negativo | -x | x.__neg__() |
número positivo | +x | x.__pos__() |
valor absoluto | abs(x) | x.__abs__() |
inverso | ~x | x.__invert__() |
número complejo | complex(x) | x.__complex__() |
integer | int(x) | x.__int__() |
número flotante | float(x) | x.__float__() |
redondeo a entero | round(x) | x.__round__() |
redondeo a entero con decimales | round(x, n) | x.__round__(n) |
menor entero >= x | math.ceil(x) | x.__ceil__() |
mayor entero <= x | math.floor(x) | x.__floor__() |
truncar x al entero hacia 0 | math.trunc(x) | x.__trunc__() |
número como un índice de lista | unaLista[x] | unaLista[x.__index__()] |
He separado esta sección de la anterior porque las comparaciones se pueden hacer más allá de que la clase sea ``numérica''. Muchos tipos de datos pueden compararse --cadenas de caracteres, listas, e incluso diccionarios. Si has creado tu propia clase y tiene sentido que se comparen sus objetos, puedes utilizar los siguientes métodos para implementar las comparaciones.
Tu quieres... | Por eso escribes... | Y Python llama... |
igualdad | x == y | x.__eq__(y) |
desigualdad | x != y | x.__ne__(y) |
menor que | x < y | x.__lt__(y) |
menor o igual que | x <= y | x.__le__(y) |
mayor que | x > y | x.__gt__(y) |
mayor o igual que | x >= y | x.__ge__(y) |
verdadero o falso en contexto booleano | if x: | x.__bool__() |
Si defines un método __lt__(), pero no el método __gt__(), Python usará el primero con los operandos intercambiados. Sin embargo, Python no combinará métodos. Por ejemplo, si defines __lt__() y __eq__() e intentas x <= y, Python no llamará a los anteriores métodos en secuencia, solamente llamará al método __le__()
Python permite serializar y deserializar objetos arbitrarios (La mayoría de referencias a este proceso en Python lo llaman ``pickling'' y ``unplicking'', respectivamente). Esto es útil para almacenar el estado en un fichero para recuperarlo en otro momento. Todos los tipos de dato nativos implementan su serialización. Si creas una clase que quieres que se pueda serializar, lee el protocolo de serialización para ver cuándo y cómo se llaman los siguientes métodos especiales.
Tu quieres... | Por eso escribes... | Y Python llama... |
una copia de tu objeto | copy.copy(x) | x.__copy__() |
una copia profunda del objeto | copy.deepcopy(x) | x.__deepcopy__() |
obtener el estado del objeto | pickle.dump(x, fichero) | x.__getstate__() |
antes de la serialización | ||
para serializar un objeto | pickle.dump(x, fichero) | x.__reduce__() |
para serializar un objeto | pickle.dump(x, fichero, | x.__reduce_ex__( |
(nuevo protocolo) | versionProtocolo) | versionProtocolo) |
para controlar cómo | x = pickle.load(fichero) | x.__getnewargs__() |
se crea un objeto | ||
durante la deserializ. | ||
para recuperar el estado | x = pickle.load(fichero) | x.__setstate__() |
de un objeto despues | ||
de deserializ. |
Para recuperar un objeto serializado, Python necesita crear un nuevo objeto que sea como el serializado para, luego, establecer los valores de todos los atributos del nuevo objeto. El método __getnewargs__() controla cómo se crea el objeto; después, el método __setstate__() controla cómo se recuperan los atributos.
with define que un bloque tenga un contexto en tiempo de ejecución: entras en el contexto cuando ejecutas la sentencia, y sales cuando ejecutas la última sentencia del bloque.
Tu quieres... | Por eso escribes... | Y Python llama... |
para hacer algo especial | with x: | x.__enter__() |
cuando entras en el bloque | ||
para hacer algo especial al | with x: | x.__exit__(exc_tipo, |
salir del bloque | exc_valor, traza) |
Así es cómo funciona el idiomatismo with fichero(ver capítulo 11).
# fragmento de io.py:
def _checkClosed(self, msg=None):
'''Internal: raise an ValueError if file is closed
'''
if self.closed:
raise ValueError('I/O operation on closed file.'
if msg is None else msg)
def __enter__(self): '''Context management protocol. Returns self.''' self._checkClosed() return self
def __exit__(self, *args): '''Context management protocol. Calls close()''' self.close()
El método __exit__() se llama siempre, incluso si se eleva una excepción en bloque. De hecho, si se eleva una excepción, la información de la misma se pasa al método __exit__().
Si sabes lo que estás haciendo, puedes tomar el control casi completo sobre cómo se comparan las clases, cómo se definen los atributos, y qué clases se consideran subclases de tu clase.
Tu quieres... | Por eso escribes... | Y Python llama... |
un constructor de clase | x = MiClase() | x.__new__() |
un destructor de clase | del x | x.__del__() |
definir solo un específico | x.__slots__() | |
conjunto de atributos | ||
un valor de has a medida | hash(x) | x.__hash__() |
obtener el valor | x.color | type(x).__dict__['color'].__get__( |
de una propiedad | x, type(x)) | |
establecer el valor | x.color = 'PapayaWhip' | type(x).__dict__['color'].__set__( |
de una propiedad | x, 'PapayaWhip') | |
borrar una propiedad | del x.color | type(x).__dict__['color'].__del__() |
controlar si un objeto | isinstance(x, MiClase) | MiClase.__instancecheck__(x) |
es instancia de tu clase | ||
controlar si una clase | issubclass(C, MiClase) | MiClase.__subclasscheck__(x) |
es subclase de tu clase | ||
controlar si una clase | issubclass(C, MiClase) | MiClase.__subclasshook__(x) |
es subclase de tu clase | ||
abstracta |
Conocer cuándo llama Python al método __del__() es extremadamente complejo. Para comprenderlo es necesario conocer cómo Python mantiene a los objetos en memoria, especialmente debido a que el recolector de basura se ejecuta de forma asíncrona. Es interesante leer sobre lo siguiente:
Módulos mencionados en este apéndice:
Otras lecturas:
José Miguel González Aguilera 2016-08-18