¿Cómo agregar propiedad a una clase de forma dinámica?

Resuelto Anthony Kong asked hace 15 años • 26 respuestas

El objetivo es crear una clase simulada que se comporte como un conjunto de resultados de base de datos.

Entonces, por ejemplo, si una consulta de base de datos devuelve, 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.aben su lugar devuelve un objeto de propiedad.

Reemplazar la setattrlínea con k = property(lambda x: vs[i])no sirve de nada.

Entonces, ¿cuál es la forma correcta de crear una propiedad de instancia en tiempo de ejecución?

PD: Conozco una alternativa presentada en ¿Cómo se utiliza el __getattribute__método?

Anthony Kong avatar Aug 25 '09 08:08 Anthony Kong
Aceptado

Supongo que debería ampliar esta respuesta ahora que soy mayor, 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: tienes que agregarlo a la clase .

>>> class Foo(object):
...     pass
... 
>>> foo = Foo()
>>> foo.a = 3
>>> Foo.b = property(lambda self: self.a + 1)
>>> foo.b
4

A propertyes en realidad una implementación simple de algo llamado descriptor . Es un objeto que proporciona un manejo personalizado para un atributo determinado, en una clase determinada . Algo así como una forma de factorizar un ifárbol enorme __getattribute__.

Cuando pregunto foo.ben el ejemplo anterior, Python ve que lo bdefinido en la clase implementa el protocolo descriptor , lo que simplemente significa que es un objeto con un método __get__, __set__o __delete__. El descriptor asume la responsabilidad de manejar ese atributo, por lo que Python llama Foo.b.__get__(foo, Foo)y el valor de retorno se le devuelve como el valor del atributo. En el caso de property, cada uno de estos métodos simplemente llama a fget, fseto fdelusted pasó al propertyconstructor.

Los descriptores son en realidad la forma que tiene Python de exponer la plomería de toda su implementación OO. De hecho, existe otro tipo de descriptor incluso más común que 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 humilde método es sólo otro tipo de descripción. Se __get__centra en la instancia que llama como primer argumento; en efecto, hace esto:

def __get__(self, instance, owner):
    return functools.partial(self.function, instance)

De todos modos, sospecho que esta es la razón por la que los descriptores solo funcionan en clases: son una formalización de las cosas que impulsan las clases en primer lugar. Incluso son la excepción a la regla: obviamente puedes asignar descriptores a una clase, ¡y las clases son en sí mismas instancias de type! De hecho, intentar leer Foo.barllamadas fijas property.__get__; es simplemente idiomático que los descriptores regresen ellos mismos cuando se accede a ellos como atributos de clase.

Creo que es genial que prácticamente todo el sistema OO de Python se pueda expresar en Python. :)

Ah, y hace un tiempo escribí una publicación de blog extensa sobre descriptores, si estás interesado.

Eevee avatar Aug 31 '2009 01:08 Eevee

El objetivo es crear una clase simulada que se comporte como un conjunto de resultados de base de datos.

Entonces, ¿lo que quieres es un diccionario donde puedas deletrear a['b'] como ab?

Eso es fácil:

class atdict(dict):
    __getattr__= dict.__getitem__
    __setattr__= dict.__setitem__
    __delattr__= dict.__delitem__
bobince avatar Aug 25 '2009 14:08 bobince

No es necesario utilizar una propiedad para eso. Simplemente anule __setattr__para que sean de solo 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!
Ryan avatar Aug 25 '2009 02:08 Ryan