functions - python ¿Cómo crear variables privadas de clase usando setattr o exec?
python object attributes (3)
Aquí está el truco que tengo hasta ahora. Sugerencias para mejorar son bienvenidas.
class T(object):
def __init__(self, **kwds):
for k, v in kwds.items():
d = {}
cls_name = self.__class__.__name__
eval(compile(
''class dummy: pass/n''
''class {0}: __{1} = 0''.format(cls_name, k), '''', ''exec''), d)
d1, d2 = d[''dummy''].__dict__, d[cls_name].__dict__
k = next(k for k in d2 if k not in d1)
setattr(self, k, v)
>>> t = T(x=1, y=2, z=3)
>>> t._T__x, t._T__y, t._T__z
(1, 2, 3)
Acabo de encontrarme en una situación en la que los nombres de los miembros de la clase pseudoprivada no se setattr
al usar setattr
o exec
.
In [1]: class T:
...: def __init__(self, **kwargs):
...: self.__x = 1
...: for k, v in kwargs.items():
...: setattr(self, "__%s" % k, v)
...:
In [2]: T(y=2).__dict__
Out[2]: {''_T__x'': 1, ''__y'': 2}
He intentado con exec("self.__%s = %s" % (k, v))
también con el mismo resultado:
In [1]: class T:
...: def __init__(self, **kwargs):
...: self.__x = 1
...: for k, v in kwargs.items():
...: exec("self.__%s = %s" % (k, v))
...:
In [2]: T(z=3).__dict__
Out[2]: {''_T__x'': 1, ''__z'': 3}
Hacer self.__dict__["_%s__%s" % (self.__class__.__name__, k)] = v
funcionaría, pero __dict__
es un atributo de solo lectura.
¿Hay alguna otra forma en que pueda crear dinámicamente estos miembros de la clase psuedo -private (sin hard-coding en el manejo de nombres)?
Una mejor manera de expresar mi pregunta:
¿Qué hace Python "debajo del capó" cuando se encuentra con un atributo de doble guión bajo ( self.__x
)? ¿Hay una función mágica que se utiliza para hacer el cambio?
Dirigiéndose a esto:
¿Qué hace Python "debajo del capó" cuando se encuentra con un atributo de doble guión bajo (
self.__x
)? ¿Hay una función mágica que se utiliza para hacer el cambio?
AFAIK, es básicamente especial en el compilador. Entonces, una vez que está en bytecode, el nombre ya está destrozado; el intérprete nunca ve el nombre no manchado, y no tenía idea de ningún manejo especial necesario. Esta es la razón por la que las referencias a través de setattr
, exec
o al buscar una cadena en __dict__
no funcionan; el compilador los ve todos como cadenas y no sabe que tienen algo que ver con el acceso a los atributos, por lo que los pasa sin cambios. El intérprete no sabe nada sobre el nombre de manipulación, por lo que simplemente los usa directamente.
Las veces que he necesitado para solucionar esto, he hecho manualmente el mismo nombre, hacky como eso es. Descubrí que usar estos nombres ''privados'' generalmente es una mala idea, a menos que sea un caso en el que sepa que los necesita para su propósito previsto: permitir que una jerarquía de clases heredadas use el mismo nombre pero tenga una copia por clase. Peppering los nombres de atributos con dobles guiones bajos simplemente porque se supone que son detalles privados de implementación parece causar más daño que beneficio; Empecé usando solo un guión bajo como una pista de que el código externo no debería tocarlo.
Creo que Python hace que los atributos privados se manipulen durante la compilación ... en particular, ocurre en la etapa en la que acaba de analizar la fuente en un árbol sintáctico abstracto, y lo está convirtiendo en código de bytes. Este es el único momento durante la ejecución en que la VM conoce el nombre de la clase dentro de cuyo ámbito (léxico) se define la función. A continuación, desactiva los atributos y variables psuedo-privados, y deja todo lo demás sin cambios. Esto tiene un par de implicaciones ...
Las constantes de cadena, en particular, no se alteran, por lo que su
setattr(self, "__X", x)
se deja en paz.Dado que el cambio depende del alcance léxico de la función dentro de la fuente, las funciones definidas fuera de la clase y luego "insertadas" no tienen ningún cambio, ya que la información sobre la clase a la que "pertenecen" no se conocía en tiempo de compilación .
Por lo que yo sé, no hay una manera fácil de determinar (en tiempo de ejecución) en qué clase se definió una función ... Al menos no sin una gran cantidad de llamadas
inspect
que se basan en la reflexión fuente para comparar números de línea entre la función y fuentes de clase. Incluso ese enfoque no es 100% confiable, existen casos fronterizos que pueden causar resultados erróneos.El proceso es bastante endeble en cuanto a la modificación: si intenta acceder al atributo
__X
en un objeto que no es una instancia de la clase en la que la función está definida léxicamente, aún lo destruirá para esa clase ... dejando usted almacena atributos de clase privados en instancias de otros objetos! (Casi diría que este último punto es una característica, no un error)
Por lo tanto, el cambio variable tendrá que hacerse manualmente, de modo que se calcule cuál debe ser el attr destrozado para llamar a setattr
.
Respecto al mangle mismo, lo hace la función _Py_Mangle , que usa la siguiente lógica:
-
__X
obtiene un guión bajo y el nombre de la clase es antepuesto. Por ejemplo, si esTest
, el attr destrozado es_Test__X
. - La única excepción es si el nombre de la clase comienza con cualquier guión bajo, estos se eliminan. Por ejemplo, si la clase es
__Test
, el attr destrozado todavía es_Test__X
. - Los guiones bajos posteriores en un nombre de clase no se eliminan.
Para envolver todo esto en una función ...
def mangle_attr(source, attr):
# return public attrs unchanged
if not attr.startswith("__") or attr.endswith("__") or ''.'' in attr:
return attr
# if source is an object, get the class
if not hasattr(source, "__bases__"):
source = source.__class__
# mangle attr
return "_%s%s" % (source.__name__.lstrip("_"), attr)
Sé que esto es algo así como "códigos rígidos", pero el nombre está asociado a una sola función. A continuación, se puede usar para setattr
cadenas para setattr
:
# you should then be able to use this w/in the code...
setattr(self, mangle_attr(self, "__X"), value)
# note that would set the private attr for type(self),
# if you wanted to set the private attr of a specific class,
# you''d have to choose it explicitly...
setattr(self, mangle_attr(somecls, "__X"), value)
Alternativamente, la siguiente implementación de mangle_attr
usa una evaluación para que siempre use la lógica de mangle_attr
actual de Python (aunque no creo que la lógica expuesta anteriormente haya cambiado) ...
_mangle_template = """
class {cls}:
@staticmethod
def mangle():
{attr} = 1
cls = {cls}
"""
def mangle_attr(source, attr):
# if source is an object, get the class
if not hasattr(source, "__bases__"):
source = source.__class__
# mangle attr
tmp = {}
code = _mangle_template.format(cls=source.__name__, attr=attr)
eval(compile(code, '''', ''exec''), {}, tmp);
return tmp[''cls''].mangle.__code__.co_varnames[0]
# NOTE: the ''__code__'' attr above needs to be ''func_code'' for python 2.5 and older