Hay unas cuantas situaciones en las la que el uso aparentemente inocua de una referencia prestada puede causar quebraderos de cabeza. Todas tienen que ver con llamadas implícitas al intérprete, que pueden causar que el propietario de una referencia se deshaga de ella.
La primera y más importante causa que hay que conocer es el uso de Py_DECREF() sobre un objeto independiente mientras se toma prestada una referencia a un elemento de lista. Por ejemplo:
bug(PyObject *lista) { PyObject *elemento = PyList_GetItem(list, 0); PyList_SetItem(lista, 1, PyInt_FromLong(0L)); PyObject_Print(elemento, stdout, 0); /* ¡BUG! */ }
Esta función toma prestada una referencia a list[0]
, luego reemplaza
list[1]
con el valor 0
y, por último, presenta la referencia
prestada. Parece inofensivo, ¿no? Pues no.
Sigamos el flujo de control de PyList_SetItem(). La lista posee referencias a todos sus elementos, por lo que cuando se reemplaza el elemento 1, se ha de deshacer del elemento 1 original. Supongamos ahora que dicho elemento fuera una instancia de una clase definida por el usuario y que dicha clase definiera un método __del__(). Si esta instancia de clase tuviera un saldo de referencias igual a 1, deshacerse de ella hará que se llame a su método __del__().
Al estar escrito en Python, el método __del__() puede ejecutar
código Python arbitrario. ¿Podría hacer algo para invalidar
la referencia a elemento
en bug()? ¡Por supuesto!
Suponiendo que la lista pasada a bug() es accesible por el método
__del__() podría ejecutar una sentencia análoga a
"del list[0]". Suponiendo que ésta fuera la última referencia al objeto,
se liberaría la memoria asociada, invalidando elemento
.
La solución, conocida la fuente del problema, es sencilla: incrementar temporalmente el saldo de referencias. La versión correcta de la función es:
no_bug(PyObject *lista) { PyObject *elemento = PyList_GetItem(lista, 0); Py_INCREF(elemento); PyList_SetItem(lista, 1, PyInt_FromLong(0L)); PyObject_Print(elemento, stdout, 0); Py_DECREF(elemento); }
Esto está basado en hechos reales. Una versión anterior de Python contenía variantes de este bug y alguien se pasó una cantidad considerable de tiempo para descubrir por qué fracasaban sus métodos __del__()...
El segundo caso de problemas de referencias prestadas afecta a las
hebras de ejecución. Normalmente, las múltiples hebras de ejecución no se estorban,
porque hay un bloque global que protege el espacio nominal de Python completo.
Sin embargo, es posible liberar este bloqueo usando la macro
Py_BEGIN_ALLOW_THREADS
y reactivarlo con
Py_END_ALLOW_THREADS
. Esto es común en los bloques de llamadas E/S
bloqueantes, para permitir que otras hebras usen la CPU mientras se espera que
se complete la E/S. Obviamente, la función siguiente presenta el mismo problema
que la anterior:
bug(PyObject *lista) { PyObject *elemento = PyList_GetItem(lista, 0); Py_BEGIN_ALLOW_THREADS ...llamada a E/S bloqueante... Py_END_ALLOW_THREADS PyObject_Print(elemento, stdout, 0); /* ¡BUG! */ }