python - poo - ¿Cómo agregar propiedades a una clase de forma dinámica?
poo python 3 (17)
El objetivo es crear una clase simulada que se comporte como un conjunto de resultados db.
Entonces, por ejemplo, si una consulta de base de datos retorna, usando una expresión dict, {''ab'':100, ''cd'':200}
, entonces me gustaría ver:
>>> dummy.ab
100
Al principio pensé que tal vez podría hacerlo de esta manera:
ks = [''ab'', ''cd'']
vs = [12, 34]
class C(dict):
def __init__(self, ks, vs):
for i, k in enumerate(ks):
self[k] = vs[i]
setattr(self, k, property(lambda x: vs[i], self.fn_readyonly))
def fn_readonly(self, v)
raise "It is ready only"
if __name__ == "__main__":
c = C(ks, vs)
print c.ab
pero c.ab
devuelve un objeto de propiedad en su lugar.
Reemplazar la línea de setattr
con k = property(lambda x: vs[i])
no sirve para nada.
Entonces, ¿cuál es la forma correcta de crear una propiedad de instancia en tiempo de ejecución?
PD. Estoy al tanto de una alternativa presentada en ¿Cómo se __getattribute__
método __getattribute__
?
¿Cómo agregar propiedades a una clase python de forma dinámica?
Supongamos que tiene un objeto al que desea agregarle una propiedad. Normalmente, quiero usar propiedades cuando necesito comenzar a administrar el acceso a un atributo en el código que tiene un uso posterior, de modo que pueda mantener una API consistente. Ahora normalmente los agregaré al código fuente donde se define el objeto, pero supongamos que no tiene ese acceso, o necesita elegir dinámicamente sus funciones de forma programática.
Crear una clase
Usando un ejemplo basado en la documentación de la property
, crearemos una clase de objeto con un atributo "oculto" y crearemos una instancia de este:
class C(object):
''''''basic class''''''
_x = None
o = C()
En Python, esperamos que haya una forma obvia de hacer las cosas. Sin embargo, en este caso, voy a mostrar dos formas: con notación de decorador y sin. Primero, sin notación de decorador. Esto puede ser más útil para la asignación dinámica de getters, setters o deleters.
Dinámico (también conocido como Monkey Patching)
Vamos a crear algunos para nuestra clase:
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
Y ahora asignamos estos a la propiedad. Tenga en cuenta que podríamos elegir nuestras funciones programáticamente aquí, respondiendo la pregunta dinámica:
C.x = property(getx, setx, delx, "I''m the ''x'' property.")
Y el uso:
>>> o.x = ''foo''
>>> o.x
''foo''
>>> del o.x
>>> print(o.x)
None
>>> help(C.x)
Help on property:
I''m the ''x'' property.
Decoradores
Podríamos hacer lo mismo que hicimos anteriormente con la notación del decorador, pero en este caso, debemos nombrar los métodos con el mismo nombre (y recomendaría mantenerlo igual que el atributo), por lo que la asignación programática no es tan trivial como está usando el método anterior:
@property
def x(self):
''''''I''m the ''x'' property.''''''
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
Y asigne el objeto de propiedad con sus setters y deleters aprovisionados a la clase:
C.x = x
Y el uso:
>>> help(C.x)
Help on property:
I''m the ''x'' property.
>>> o.x
>>> o.x = ''foo''
>>> o.x
''foo''
>>> del o.x
>>> print(o.x)
None
El objetivo es crear una clase simulada que se comporte como un conjunto de resultados db.
Entonces, ¿qué quieres es un diccionario donde puedes deletrear un [''b''] como ab?
Eso es fácil:
class atdict(dict):
__getattr__= dict.__getitem__
__setattr__= dict.__setitem__
__delattr__= dict.__delitem__
Esto parece funcionar (pero ver a continuación):
class data(dict,object):
def __init__(self,*args,**argd):
dict.__init__(self,*args,**argd)
self.__dict__.update(self)
def __setattr__(self,name,value):
raise AttributeError,"Attribute ''%s'' of ''%s'' object cannot be set"%(name,self.__class__.__name__)
def __delattr__(self,name):
raise AttributeError,"Attribute ''%s'' of ''%s'' object cannot be deleted"%(name,self.__class__.__name__)
Si necesita un comportamiento más complejo, no dude en editar su respuesta.
editar
Probablemente, lo siguiente sea más eficiente con la memoria para grandes conjuntos de datos:
class data(dict,object):
def __init__(self,*args,**argd):
dict.__init__(self,*args,**argd)
def __getattr__(self,name):
return self[name]
def __setattr__(self,name,value):
raise AttributeError,"Attribute ''%s'' of ''%s'' object cannot be set"%(name,self.__class__.__name__)
def __delattr__(self,name):
raise AttributeError,"Attribute ''%s'' of ''%s'' object cannot be deleted"%(name,self.__class__.__name__)
Hice una pregunta similar en esta publicación de para crear una fábrica de clases que creara tipos simples. El resultado fue esta respuesta que tenía una versión funcional de la fábrica de la clase. Aquí hay un fragmento de la respuesta:
def Struct(*args, **kwargs):
def init(self, *iargs, **ikwargs):
for k,v in kwargs.items():
setattr(self, k, v)
for i in range(len(iargs)):
setattr(self, args[i], iargs[i])
for k,v in ikwargs.items():
setattr(self, k, v)
name = kwargs.pop("name", "MyStruct")
kwargs.update(dict((k, None) for k in args))
return type(name, (object,), {''__init__'': init, ''__slots__'': kwargs.keys()})
>>> Person = Struct(''fname'', ''age'')
>>> person1 = Person(''Kevin'', 25)
>>> person2 = Person(age=42, fname=''Terry'')
>>> person1.age += 10
>>> person2.age -= 10
>>> person1.fname, person1.age, person2.fname, person2.age
(''Kevin'', 35, ''Terry'', 32)
>>>
Puede usar alguna variación de esto para crear valores predeterminados, que es su objetivo (también hay una respuesta en esa pregunta que trata sobre esto).
La única forma de adjuntar dinámicamente una propiedad es crear una nueva clase y su instancia con su nueva propiedad.
class Holder: p = property(lambda x: vs[i], self.fn_readonly)
setattr(self, k, Holder().p)
La mejor forma de lograrlo es definiendo __slots__
. De esta manera, sus instancias no pueden tener nuevos atributos.
ks = [''ab'', ''cd'']
vs = [12, 34]
class C(dict):
__slots__ = []
def __init__(self, ks, vs): self.update(zip(ks, vs))
def __getattr__(self, key): return self[key]
if __name__ == "__main__":
c = C(ks, vs)
print c.ab
Eso imprime 12
c.ab = 33
Eso da: AttributeError: ''C'' object has no attribute ''ab''
No estoy seguro si entiendo completamente la pregunta, pero puede modificar las propiedades de instancia en tiempo de ejecución con el __dict__
de su clase:
class C(object):
def __init__(self, ks, vs):
self.__dict__ = dict(zip(ks, vs))
if __name__ == "__main__":
ks = [''ab'', ''cd'']
vs = [12, 34]
c = C(ks, vs)
print(c.ab) # 12
No necesita usar una propiedad para eso. Simplemente anule __setattr__
para que solo sean de lectura.
class C(object):
def __init__(self, keys, values):
for (key, value) in zip(keys, values):
self.__dict__[key] = value
def __setattr__(self, name, value):
raise Exception("It is read only!")
Tada.
>>> c = C(''abc'', [1,2,3])
>>> c.a
1
>>> c.b
2
>>> c.c
3
>>> c.d
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: ''C'' object has no attribute ''d''
>>> c.d = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __setattr__
Exception: It is read only!
>>> c.a = ''blah''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __setattr__
Exception: It is read only!
No puede agregar una nueva property()
a una instancia en tiempo de ejecución, porque las propiedades son descriptores de datos. En su lugar, debe crear dinámicamente una nueva clase o sobrecargar __getattribute__
para procesar descriptores de datos en instancias.
Para aquellos que vienen de los motores de búsqueda, aquí están las dos cosas que estaba buscando cuando hablo de las propiedades dinámicas :
class Foo:
def __init__(self):
# we can dynamically have access to the properties dict using __dict__
self.__dict__[''foo''] = ''bar''
assert Foo().foo == ''bar''
# or we can use __getattr__ and __setattr__ to execute code on set/get
class Bar:
def __init__(self):
self._data = {}
def __getattr__(self, key):
return self._data[key]
def __setattr__(self, key, value):
self._data[key] = value
bar = Bar()
bar.foo = ''bar''
assert bar.foo == ''bar''
__dict__
es bueno si quieres poner propiedades creadas dinámicamente. __getattr__
es bueno solo hacer algo cuando el valor es necesario, como consultar una base de datos. El conjunto combinado set / get es bueno para simplificar el acceso a los datos almacenados en la clase (como en el ejemplo anterior).
Si solo desea una propiedad dinámica, eche un vistazo a la función incorporada de property() .
Para responder al objetivo principal de su pregunta, desea un atributo de solo lectura de un dict como fuente de datos inmutable:
El objetivo es crear una clase simulada que se comporte como un conjunto de resultados db.
Entonces, por ejemplo, si una consulta de base de datos retorna, usando una expresión dict,
{''ab'':100, ''cd'':200}
, entonces yo vería
>>> dummy.ab 100
Demostraré cómo usar una namedtuple
del módulo de collections
para lograr solo esto:
import collections
data = {''ab'':100, ''cd'':200}
def maketuple(d):
''''''given a dict, return a namedtuple''''''
Tup = collections.namedtuple(''TupName'', d.keys()) # iterkeys in Python2
return Tup(**d)
dummy = maketuple(data)
dummy.ab
devuelve 100
Parece que puedes resolver este problema mucho más simplemente con una namedtuple
, ya que conoces toda la lista de campos con anticipación.
from collections import namedtuple
Foo = namedtuple(''Foo'', [''bar'', ''quux''])
foo = Foo(bar=13, quux=74)
print foo.bar, foo.quux
foo2 = Foo() # error
Si necesitas escribir tu propio setter, tendrás que hacer la metaprogramación en el nivel de clase; property()
no funciona en instancias.
Puede usar el siguiente código para actualizar los atributos de clase utilizando un objeto de diccionario:
class ExampleClass():
def __init__(self, argv):
for key, val in argv.items():
self.__dict__[key] = val
if __name__ == ''__main__'':
argv = {''intro'': ''Hello World!''}
instance = ExampleClass(argv)
print instance.intro
Recientemente me encontré con un problema similar, la solución que se me ocurrió utiliza __getattr__
y __setattr__
para las propiedades que quiero que maneje, todo lo demás pasa a los originales.
class C(object):
def __init__(self, properties):
self.existing = "Still Here"
self.properties = properties
def __getattr__(self, name):
if "properties" in self.__dict__ and name in self.properties:
return self.properties[name] # Or call a function, etc
return self.__dict__[name]
def __setattr__(self, name, value):
if "properties" in self.__dict__ and name in self.properties:
self.properties[name] = value
else:
self.__dict__[name] = value
if __name__ == "__main__":
my_properties = {''a'':1, ''b'':2, ''c'':3}
c = C(my_properties)
assert c.a == 1
assert c.existing == "Still Here"
c.b = 10
assert c.properties[''b''] == 10
Solo otro ejemplo de cómo lograr el efecto deseado
class Foo(object):
_bar = None
@property
def bar(self):
return self._bar
@bar.setter
def bar(self, value):
self._bar = value
def __init__(self, dyn_property_name):
setattr(Foo, dyn_property_name, Foo.bar)
Entonces ahora podemos hacer cosas como estas:
>>> foo = Foo(''baz'')
>>> foo.baz = 5
>>> foo.bar
5
>>> foo.baz
5
Supongo que debería ampliar esta respuesta, ahora que soy más viejo y más sabio y sé lo que está pasando. Mejor tarde que nunca.
Puede agregar una propiedad a una clase de forma dinámica. Pero ese es el problema: debes agregarlo a la clase .
>>> class Foo(object):
... pass
...
>>> foo = Foo()
>>> foo.a = 3
>>> Foo.b = property(lambda self: self.a + 1)
>>> foo.b
4
Una property
es en realidad una implementación simple de una cosa llamada descriptor . Es un objeto que proporciona un manejo personalizado para un atributo dado, en una clase dada . Algo así como una forma de factorizar un gran árbol if
__getattribute__
.
Cuando solicito foo.b
en el ejemplo anterior, Python ve que b
definido en la clase implementa el protocolo de descriptor, lo que significa que es un objeto con un __get__
, __set__
o __delete__
. El descriptor se responsabiliza por el manejo de ese atributo, por lo que Python llama a Foo.b.__get__(foo, Foo)
y el valor devuelto se le transfiere a usted como el valor del atributo. En el caso de la property
, cada uno de estos métodos simplemente llama a fget
, fset
o fdel
que pasó al constructor de la property
.
Los descriptores son en realidad la forma en que Python expone las tuberías de toda su implementación de OO. De hecho, hay otro tipo de descriptor aún más común que la property
.
>>> class Foo(object):
... def bar(self):
... pass
...
>>> Foo().bar
<bound method Foo.bar of <__main__.Foo object at 0x7f2a439d5dd0>>
>>> Foo().bar.__get__
<method-wrapper ''__get__'' of instancemethod object at 0x7f2a43a8a5a0>
El método humilde es solo otro tipo de descriptor. Su __get__
tachuelas en la instancia de llamada como primer argumento; en efecto, hace esto:
def __get__(self, instance, owner):
return functools.partial(self.function, instance)
De todos modos, sospecho que es por eso que los descriptores solo funcionan en las clases: son una formalización de las cosas que dan poder a las clases en primer lugar. Incluso son la excepción a la regla: obviamente puede asignar descriptores a una clase, ¡y las clases son en sí mismas instancias de type
! De hecho, al tratar de leer, Foo.b
aún llama property.__get__
; es simplemente idiomático que los descriptores se devuelvan a sí mismos cuando se accede como atributos de clase.
Creo que es genial que prácticamente todo el sistema OO de Python se pueda expresar en Python. :)
Oh, y hace un tiempo escribí una publicación en un blog verbo sobre descripciones, si está interesado.
class atdict(dict):
def __init__(self, value, **kwargs):
super().__init__(**kwargs)
self.__dict = value
def __getattr__(self, name):
for key in self.__dict:
if type(self.__dict[key]) is list:
for idx, item in enumerate(self.__dict[key]):
if type(item) is dict:
self.__dict[key][idx] = atdict(item)
if type(self.__dict[key]) is dict:
self.__dict[key] = atdict(self.__dict[key])
return self.__dict[name]
d1 = atdict({''a'' : {''b'': [{''c'': 1}, 2]}})
print(d1.a.b[0].c)
Y la salida es:
>> 1