Apartados


B. Nombres de métodos especiales

Nivel de dificultad:5 sobre 5

``Mi especialidad es acertar cuando otras personas están equivocadas.''
--George Bernard Shaw

B.1 Inmersión

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.

B.2 Lo básico

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    

  1. Línea 1: el método __init__() se llama después de que haya creado la instancia. Si quieres controlar el proceso de creación del objeto, usa el método __new__().
  2. Línea 2: por convención, el método __repr__() debería retornar una cadena de caractres cuyo contenido sea una expresión válida en Python.
  3. Línea 3: el método __str__() se llama también cuando print(x).
  4. Línea 4: nuevo en Python 3, desde que se ha introducido el tipo bytes.
  5. Línea 5: Por convención, format_spec debería ser conforme a la especificación del mini-lenguaje de formarto. decimal.py en la librería estándar de Python proporciona su propio método __format__().

B.3 Clases que actúan como iteradores

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

  1. Línea 1: el método __iter__ se llama cada vez que creas un nuevo iterador. Es un buen lugar para inicializarlo.
  2. Línea 2: el método __next__() se llama cada vez que obtienes el siguiente valor del iterador.
  3. Línea 3: el método __reversed__() es menos común. Toma una secuencia existente y devuelve un iterador que genera (yield) sus elementos en orden inverso, del último al primero.

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.

B.4 Atributos calculados


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')

  1. Línea 1: si tu clase define un método __getattribute__(), Python la llamará en cada referencia a un nombre de método o atributo (excepto en los métodos con nombres especiales, lo que produciría un bucle infinito).
  2. Línea 2: si tu clase define un método __getattr__(), Python la llamará solamente después de buscar al atributo en todos los lugares normales. Si una instancia x define una atributo color, x.color no llamará a x.getattr('color'); solamente devolverá el valor definido ya por x.color.
  3. Línea 3: el método __setattr__() se llama siempre que asignes un valor a un atributo.
  4. Línea 4: el método __delattr__() se llama siempre que eliminas un atributo.
  5. Línea 5: __dir__() es útil si defines __getattr__() o __getattribute__(). Normalmente llamar a dir() solamente devolvería una lista con los atributos y métodos normales. Si tu método __getattr__() maneja un atributo color de forma dinámica, dir() no devolvería color como uno de los atributos disponibles. La sustitución del método __dir__() te permite listar color como uno de los atributos disponibles, lo que es útil para que otros puedan utilizar tu clase sin que tengan que investigar su código interno.

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'

  1. Línea 3: se pasa el nombre de atributo a __getattr__() como una cadena de caracteres. Si el nombre es 'color', el método devuelve un valor (en este caso, está codificado de forma fija en el propio código, pero normalmente sería algún tipo de cálculo para devolver el resultado).
  2. Línea 6: si el nombre del atributo es desconocido, el método debe elevar una excepción AttributeError, de otro modo el código fallaría de forma silenciosa cuando accediera a atributos sin definir (Técnicamente, si el método no eleva una excepción o devuelve explícitamente un valor, devuelve None, el valor nulo de Python. Esto significaría que todos los atributos que no estuvieran definidos explícitamente valdrían None, que casi seguro que no es lo que quieres).
  3. Línea 9: la instancia dyn no tiene un atributo denominado color, por eso se llama al método __getattr__() que devuelve el valor calculado.
  4. Línea 12: después de establecer de forma explícita un valor a dyn.color, no se llama ya al método __getattr__() ya que dyn.color ya está definido en el objeto de forma explícita.

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'

  1. Línea 9: se llama al método __getattribute__() para obteer un valor para dyn.color.
  2. Línea 12: incluso después de haber establecido explícitamente un valor a dyn.color, se sigue llamando al método __getattribute__() para obtener un valor para este atributo. Si está presente este método, se llama de forma incondicional para buscar a todos los métodos y atributos del objeto, incluso aquellos que se definieran explícitamente después de crear la instancia.

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

  1. Línea 3: esta clase define un método __get attribute__() que siempre eleva la excepción AttributeError. No será posible encontrar ningún atributo o método de esta clase.
  2. Línea 8: Cuando llames a hero.swim(), Python buscará un método swim() en la clase Rastan. Esta búsqueda pasa por el método __getattribute__(), todas las búsquedas de atributos y métodos pasan a través del método __getattribute__(). En este caso, este método eleva AttributeError, por lo que la búsqueda falla, así que la llamada al método también falla.

B.5 Clases que se comportan como funciones

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]))

  1. Línea 7: la clase _ZipDecriptor mantiene el estado en tres campos clave que rotan: que se van actualizando en el método _Subir adateKeys() (no se muestra aquí).
  2. Línea 13: la clase define un método __call__() que hace que se pueda llamar directamente a las instancias de la clase como si fuesen funciones. En este caso, el método __call__() desencripta un único byte del fichero zip, luego actualiza las claves basándose en el byte que se ha desencriptado.
  3. Línea 22: zd es una instancia de la clase _ZipDecriptor. La variable pwd se pasa al método __init__(), en donde se almacena y se usa para actualizar las claves que rotan por vez primera.
  4. Línea 24: Dados los 12 primeros bytes de un fichero zip, los desencripta mediante el mapeo de los bytes a zd, llamando en la práctica 12 veces a zd, que invoca al método __call__() 12 veces, que, a su vez, actualiza el estado interno y devuelve el byte resultante 12 veces.

B.6 Clases que se comportan como conjuntos

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())

  1. Línea 4: cuando creas una instancia de la clase cgi.FieldStorage, puedes utilizar el operador in para validar si se incluyó un parámetro en particular en la cadena de caracteres de consulta.
  2. Línea 12: el método __contains__() es la magia que hace el trabajo. Cuando dices if 'q' in fs, Python busca el método __contains__()en el objeto fs, que está definido en cgi.py. El valor 'q' se pasa a este método como el parámetro key.
  3. Línea 15: la función any() toma una expresión generadora para retornar True si el generador devuelve algún elemento. La función any() es lo suficientemente inteligente para parar en cuanto encuentra el primer resultado.
  4. Línea 17: la misma clase FieldStorage también sabe devolver su longitud, así que puedes utilizar len(fs) para que se llame al método __len__() de la clase FieldStorage y devuelva el número de elementos que contiene.
  5. Línea 18: el método self.key() comprueba si self.list está a None; por ello, el método __len__() no necesita duplicar esta validación.

B.7 Clases que se comportan como diccionarios

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

  1. Línea 5: El objeto fs es una instancia de cgi.FieldStorage, pero puedes evaluar expresiones como fs['q'].
  2. Línea 12: fs['q'] invoca al método __getitem__() con el parámetro key que contiene el valor 'q'. Busca in su lista interna de parámetros de consulta (self.list) por un elemento cuyo .name coincida con la clave pasada.

B.8 Clases que se comportan como números

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:

  1. Decirle a x que se divida por y, o
  2. Decirle a y que divida a x.

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á:

  1. Intentará llamar a x.__itruediv__(y). Si este método está definido y devuelve un valor diferente de NotImplemented, finaliza los intentes.
  2. Intentará llamar después a x.__truediv__(y). Si este método está definido y devuelve un valor diferente de NotImplemented, asignará el valor del resultado a la variable x, tal y como si hubieras hecho x = x / y.
  3. Intentará llamar a y.__rtruediv__(x), si este método está definido y retorna un valor diferente de NoImplementado, el valor anterior de x se descartará y se reemplazará con el valor de retorno.

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__()]

B.9 Clases que se pueden comparar entre sí

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__()

B.10 Clases que se pueden serializar

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.

B.11 Clases que se pueden utilizar en un bloque with

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()

  1. Línea 11: el objeto file define ambos métodos __enter__() y __exit__(). El primero valida que el fichero esté abierto; si no lo está, eleva una excepción.
  2. Línea 12: el método __enter__() debería devolver casi siempre self --este es el objeto que el bloque with utilizará para recuperar propiedades y métodos.
  3. Línea 16: después del bloque with, el objeto file se cierra automáticamente. ¿Cómo? En el método __exit__() se llama a 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__().

B.12 Algunas cosas más bastante esotéricas

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:

B.13 Lecturas recomendadas

Módulos mencionados en este apéndice:

Otras lecturas:

José Miguel González Aguilera 2016-08-18