Addendum: Cómo acceder al "code object" de una clase Python

En Cómo acceder al "code object" de una clase Python explico cómo acceder al code object de una clase Python y comprobar así que estamos deserializando datos con la versión correcta del código.

Dejando al margen la conveniencia o no de recurrir a estos chanchullos, hay un problema grave para mi caso de uso: el code object de una clase Python cambia no solo cuando cambia el código fuente de la clase, sino también cuando cambia su entorno.

Veamos un ejemplo:

 #!/usr/bin/env python3

 import dis, marshal, hashlib

 def wrapper():
     class prueba:
         pass

 code = wrapper.__code__
 dis.dis(code)
 code_dump = marshal.dumps(code)
 print()
 print(hashlib.sha256(code_dump).hexdigest())

Si ejecutamos el código varias veces, siempre obtendremos el mismo resultado:

6             0 LOAD_BUILD_CLASS
              2 LOAD_CONST               1 (<code object prueba at 0x7f7dcba98930, file "./z.py", line 6>)
              4 LOAD_CONST               2 ('prueba')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               2 ('prueba')
             10 CALL_FUNCTION            2
             12 STORE_FAST               0 (prueba)
             14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

d2945184e581612f4f30458a3e70afa92382356e83422c9c8f95bc28fd83fd8c

Aquí vemos el contenido de la función y el hash del code object.

Observemos qué ocurre si hacemos un cambio mínimo en el programa. Añadamos simplemente una línea en blanco antes de la función que nos interesa:

 #!/usr/bin/env python3


 import dis, marshal, hashlib

 def wrapper():
     class prueba:
         pass

 code = wrapper.__code__
 dis.dis(code)
 code_dump = marshal.dumps(code)
 print()
 print(hashlib.sha256(code_dump).hexdigest())

Hemos añadido una línea en blanco antes de importar los módulos. Ejecutar este código produce lo siguiente:

7             0 LOAD_BUILD_CLASS
              2 LOAD_CONST               1 (<code object prueba at 0x7fe54e688930, file "./z.py", line 7>)
              4 LOAD_CONST               2 ('prueba')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               2 ('prueba')
             10 CALL_FUNCTION            2
             12 STORE_FAST               0 (prueba)
             14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

e0298eb04b1d22a786bbac588204830ecec87310294c1308d3dbde820fa90a03

La función es idéntica, pero su hash ha cambiado.

El problema fundamental es que un dato incluído en el code object es el número de línea donde empieza la función:

>>> import z
>>> z.wrapper.__code__
>>> dir(z.wrapper.__code__)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> z.wrapper.__code__.co_firstlineno
6

Por tanto, el code object puede variar no solo porque hemos modificado el código fuente relevante, sino también porque hemos cambiado la posición relativa del código en el fichero.

Dada esta situación y mi caso de uso, he movido los objetos relevantes a su propio fichero de código fuente. De esta forma evito que cambios periféricos alteren el hash de lo que me interesa.