Question Comment ajouter une propriété à une classe de manière dynamique?


Le but est de créer une classe simulée qui se comporte comme un résultat de base de données.

Ainsi, par exemple, si une requête de base de données retourne, en utilisant une expression dict, {'ab':100, 'cd':200}, alors j'aimerais voir:

>>> dummy.ab
100

Au début, je pensais que je pourrais peut-être le faire de cette façon:

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

mais c.ab renvoie à la place un objet de propriété

Remplacer le setattr aligner avec k = property(lambda x: vs[i]) n'est d'aucune utilité.

Alors, quelle est la bonne façon de créer une propriété d'instance à l'exécution?

P.S. Je suis au courant d'une alternative présentée dans Comment est la __getattribute__ méthode utilisée?


150
2017-08-25 01:53


origine


Réponses:


Je suppose que je devrais élargir cette réponse, maintenant que je suis plus âgé et plus sage et que je sais ce qui se passe. Mieux vaut tard que jamais.

Toi pouvez Ajouter une propriété à une classe de manière dynamique. Mais c’est le piège: il faut l’ajouter à la classe.

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

UNE property est en fait une simple implémentation d'une chose appelée descripteur. C'est un objet qui fournit une gestion personnalisée pour un attribut donné, sur une classe donnée. Un peu comme un moyen de prendre en compte un énorme if arbre hors de __getattribute__.

Quand je demande foo.b dans l'exemple ci-dessus, Python voit que le b défini sur la classe implémente le protocole descripteur-Qui signifie simplement qu'il s'agit d'un objet avec __get__, __set__, ou __delete__ méthode. Le descripteur revendique la responsabilité de gérer cet attribut, donc Python appelle Foo.b.__get__(foo, Foo), et la valeur de retour vous est renvoyée en tant que valeur de l'attribut. Dans le cas de property, chacune de ces méthodes appelle simplement le fget, fset, ou fdel vous êtes passé à la property constructeur.

Les descripteurs sont vraiment une manière pour Python d'exposer la plomberie de toute son implémentation OO. En fait, il existe un autre type de descripteur encore plus commun 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>

La méthode humble est juste un autre type de descripteur. Ses __get__ tacks sur l'instance d'appel comme premier argument; en effet, il le fait:

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

Quoi qu’il en soit, je soupçonne que c’est la raison pour laquelle les descripteurs ne fonctionnent que sur les classes: ils sont une formalisation de l’élément moteur des classes. Ils sont même l'exception à la règle: vous pouvez évidemment assigner des descripteurs à une classe, et les classes sont elles-mêmes des instances de type! En fait, essayant de lire Foo.b appelle encore property.__get__; il est juste idiomatique que les descripteurs se renvoient lorsqu'ils sont accédés en tant qu'attributs de classe.

Je pense que c'est plutôt cool que pratiquement tout le système OO de Python puisse être exprimé en Python. :)

Oh, et j'ai écrit un Wordy blog post sur les descripteurs il y a longtemps si vous êtes intéressé.


244
2017-08-31 01:30



Le but est de créer une classe simulée qui se comporte comme un résultat de base de données.

Donc, ce que vous voulez, c'est un dictionnaire où vous pouvez épeler un [b] comme a.b?

C'est facile:

class atdict(dict):
    __getattr__= dict.__getitem__
    __setattr__= dict.__setitem__
    __delattr__= dict.__delitem__

50
2017-08-25 14:33



Il semble que vous pourriez résoudre ce problème beaucoup plus simplement avec un namedtuple, puisque vous connaissez la liste complète des champs à l'avance.

from collections import namedtuple

Foo = namedtuple('Foo', ['bar', 'quux'])

foo = Foo(bar=13, quux=74)
print foo.bar, foo.quux

foo2 = Foo()  # error

Si vous devez absolument écrire votre propre setter, vous devrez effectuer la métaprogrammation au niveau de la classe; property()ne fonctionne pas sur les instances.


32
2017-08-25 02:28



Vous n'avez pas besoin d'utiliser une propriété pour cela. Il suffit de remplacer __setattr__ pour les rendre en lecture seule.

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!

26
2017-08-25 02:41



Vous ne pouvez pas ajouter un nouveau property() à une instance à l'exécution, car les propriétés sont des descripteurs de données. Au lieu de cela, vous devez créer dynamiquement une nouvelle classe ou une surcharge __getattribute__ afin de traiter les descripteurs de données sur les instances.


5
2017-08-25 02:23



J'ai posé une question similaire sur ce post Stack Overflow pour créer une fabrique de classes qui crée des types simples. Le résultat était cette réponse qui avait une version de travail de l'usine de classe. Voici un extrait de la réponse:

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)
>>>

Vous pouvez utiliser certaines variantes pour créer des valeurs par défaut, ce qui est votre objectif (il y a également une réponse à cette question).


4
2017-08-26 09:15



Je ne sais pas si je comprends parfaitement la question, mais vous pouvez modifier les propriétés de l'instance à l'exécution avec le __dict__ de votre classe:

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

3
2017-08-25 02:19



Comment ajouter une propriété à une classe python de manière dynamique?

Disons que vous avez un objet auquel vous souhaitez ajouter une propriété. En règle générale, je souhaite utiliser les propriétés lorsque je dois commencer à gérer l'accès à un attribut dans le code qui a une utilisation en aval, afin de pouvoir maintenir une API cohérente. Maintenant, je vais généralement les ajouter au code source où l'objet est défini, mais supposons que vous n'ayez pas cet accès, ou que vous ayez besoin de choisir de manière dynamique vos fonctions par programme.

Créer une classe

En utilisant un exemple basé sur le documentation pour property, créons une classe d'objet avec un attribut "hidden" et en créons une instance:

class C(object):
    '''basic class'''
    _x = None

o = C()

En Python, nous nous attendons à une manière évidente de faire les choses. Cependant, dans ce cas, je vais montrer deux manières: avec la notation de décorateur, et sans. Tout d'abord, sans notation de décorateur. Cela peut être plus utile pour l'affectation dynamique de getters, setters ou deleters.

Dynamique (a.k. Monkey Patching)

Créons-en pour notre classe:

def getx(self):
    return self._x

def setx(self, value):
    self._x = value

def delx(self):
    del self._x

Et maintenant, nous les assignons à la propriété. Notez que nous pourrions choisir nos fonctions par programmation ici, en répondant à la question dynamique:

C.x = property(getx, setx, delx, "I'm the 'x' property.")

Et usage:

>>> o.x = 'foo'
>>> o.x
'foo'
>>> del o.x
>>> print(o.x)
None
>>> help(C.x)
Help on property:

    I'm the 'x' property.

Décorateurs

Nous pourrions faire la même chose que nous avons fait ci-dessus avec la notation de décorateur, mais dans ce cas, nous doit nommez les méthodes avec le même nom (et je vous recommande de le garder comme l'attribut), l'assignation programmatique n'est donc pas si simple qu'elle utilise la méthode ci-dessus:

@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

Et affectez l'objet de propriété avec ses paramètres et ses paramètres provisionnés à la classe:

C.x = x

Et usage:

>>> 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

3
2018-02-05 23:12



La meilleure façon de le faire est de définir __slots__. De cette façon, vos instances ne peuvent pas avoir de nouveaux attributs.

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

Qui imprime 12

    c.ab = 33

Ça donne: AttributeError: 'C' object has no attribute 'ab'


2
2017-08-25 03:01



Juste un autre exemple pour obtenir l'effet désiré

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)

Alors maintenant, nous pouvons faire des choses comme:

>>> foo = Foo('baz')
>>> foo.baz = 5
>>> foo.bar
5
>>> foo.baz
5

2
2018-05-09 21:41



Pour ceux qui viennent des moteurs de recherche, voici les deux choses que je cherchais en parlant de dynamique Propriétés:

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__ est bon si vous voulez mettre des propriétés créées dynamiquement. __getattr__est bon de ne faire que quelque chose lorsque la valeur est nécessaire, comme interroger une base de données. Le combo set / get permet de simplifier l'accès aux données stockées dans la classe (comme dans l'exemple ci-dessus).

Si vous voulez seulement une propriété dynamique, jetez un coup d’œil à la propriété() fonction intégrée.


2
2017-08-10 12:58