web-dev-qa-db-fra.com

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

L'objectif est de créer une classe fictive qui se comporte comme un ensemble de résultats de base de données.

Ainsi, par exemple, si une requête de base de données renvoie, à l'aide d'une expression dict, {'ab':100, 'cd':200}, j'aimerais voir: 

>>> dummy.ab
100

Au début, j'ai pensé 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 __== "__main__":
    c = C(ks, vs)
    print c.ab

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

Remplacer la ligne setattr par k = property(lambda x: vs[i]) n’est d'aucune utilité.

Alors, quel est le bon moyen de créer une propriété d'instance au moment de l'exécution?

P.S. Je connais une alternative présentée dans Comment utilise-t-on la méthode __getattribute__?

167
Anthony Kong

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

Vous pouvez ajouter une propriété à une classe de manière dynamique. Mais c'est le piège: vous devez l'ajouter à la classe .

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

property est en réalité une simple implémentation d'une chose appelée descripteur . C'est un objet qui fournit un traitement personnalisé pour un attribut donné, sur une classe donnée. Un peu comme un moyen de factoriser un énorme arbre if à partir 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 descriptor, ce qui signifie simplement que c'est un objet avec une méthode __get__, __set__ ou __delete__. Le descripteur revendique le traitement de cet attribut. Python appelle donc 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 que vous avez transmis au constructeur property.

Les descripteurs sont vraiment la manière dont Python expose 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 n'est qu'un autre type de descripteur. Son __get__ pointe sur l'instance appelante en tant que premier argument; en effet, il fait ceci:

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

Quoi qu’il en soit, je suppose que c’est la raison pour laquelle les descripteurs ne fonctionnent que sur les classes: c’est une formalisation de la matière qui alimente les classes en premier lieu. Ils font même exception à la règle: vous pouvez évidemment affecter des descripteurs à une classe, et les classes sont elles-mêmes des instances de type! En fait, essayer de lire Foo.b appelle toujours property.__get__; il est juste idiomatique que les descripteurs se retournent lorsqu'ils sont accédés en tant qu'attributs de classe.

Je pense que le fait que presque tout le système OO de Python puisse être exprimé en Python est vraiment génial. :)

Oh, et j’ai écrit un commentaire de blog) sur les descripteurs il ya quelque temps si vous êtes intéressé.

274
Eevee

L'objectif est de créer une classe fictive qui se comporte comme un ensemble de résultats de base de données.

Vous voulez donc un dictionnaire dans lequel vous pouvez épeler un ['b'] comme un.b?

C'est facile:

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

Il semble que vous puissiez 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 compositeur, vous devrez faire la métaprogrammation au niveau de la classe. property() ne fonctionne pas sur les instances.

33
Eevee

Vous n'avez pas besoin d'utiliser une propriété pour cela. Remplacez simplement __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!
28
Ryan

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

Supposons que vous souhaitiez ajouter une propriété à un objet. En règle générale, je souhaite utiliser des propriétés lorsque je dois commencer à gérer l'accès à un attribut dans un code utilisé en aval, afin de pouvoir conserver 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 ne disposez pas de cet accès, ou que vous deviez choisir réellement de manière dynamique vos fonctions par programme.

Créer une classe

En utilisant un exemple basé sur la documentation pour property , créons une classe d'objet avec un attribut "hidden" et créons une instance de celle-ci:

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

o = C()

En Python, nous nous attendons à ce qu'il y ait une façon é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 décorative. Cela peut être plus utile pour l'affectation dynamique de getters, setters ou deleters.

Dynamique (correction des singes)

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 attribuons à 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 utilisation:

>>> 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 ci-dessus avec la notation décorateur, mais dans ce cas, nous devons nommer les méthodes de la même façon (et je recommanderais de le conserver de la même manière que l'attribut). pas aussi trivial que d'utiliser 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 setters et deleters fournis à la classe:

C.x = x

Et utilisation:

>>> 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
5
Aaron Hall

J'ai posé une question similaire sur cet article de Stack Overflow pour créer une fabrique de classes créant des types simples. Le résultat était cette réponse qui avait une version de travail de la classe factory . 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 une variante de ceci pour créer des valeurs par défaut qui sont votre objectif (il y a aussi une réponse dans cette question qui traite de cela).

5
kjfletch

Vous ne pouvez pas ajouter une nouvelle property() à une instance au moment de 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 surcharger __getattribute__ afin de traiter les descripteurs de données sur les instances.

5
Alex Gaynor

Pour ceux qui viennent de moteurs de recherche, voici les deux choses que je cherchais quand je parlais de dynamic properties:

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 utile si vous souhaitez définir des propriétés créées dynamiquement. __getattr__ est utile pour ne faire quelque chose que lorsque la valeur est requise, comme interroger une base de données. Le combo set/get est bien pour simplifier l'accès aux données stockées dans la classe (comme dans l'exemple ci-dessus).

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

3
tleb

Je ne suis pas sûr de bien comprendre la question, mais vous pouvez modifier les propriétés de l'instance lors de l'exécution à l'aide du __dict__ intégré de votre classe:

class C(object):
    def __init__(self, ks, vs):
        self.__dict__ = dict(Zip(ks, vs))


if __== "__main__":
    ks = ['ab', 'cd']
    vs = [12, 34]
    c = C(ks, vs)
    print(c.ab) # 12
3
Crescent Fresh

Le meilleur moyen d’y parvenir est de définir __slots__. Ainsi, 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 __== "__main__":
    c = C(ks, vs)
    print c.ab

Cela imprime 12

    c.ab = 33

Cela donne: AttributeError: 'C' object has no attribute 'ab'

2
nosklo

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
lehins

C'est un peu différent de ce que voulait OP, mais j'ai secoué mon cerveau jusqu'à ce que je trouve une solution efficace, alors je le mets ici pour le prochain gars/fille

J'avais besoin d'un moyen de spécifier des setters et des getters dynamiques.

class X:
    def __init__(self, a=0, b=0, c=0):
        self.a = a
        self.b = b
        self.c = c

    @classmethod
    def _make_properties(cls, field_name, inc):
        _inc = inc

        def _get_properties(self):
            if not hasattr(self, '_%s_inc' % field_name):
                setattr(self, '_%s_inc' % field_name, _inc)
                inc = _inc
            else:
                inc = getattr(self, '_%s_inc' % field_name)

            return getattr(self, field_name) + inc

        def _set_properties(self, value):
            setattr(self, '_%s_inc' % field_name, value)

        return property(_get_properties, _set_properties)

Je connais mes champs à l'avance, alors je vais créer mes propriétés. NOTE: vous ne pouvez pas faire cette instance PER, ces propriétés existeront sur la classe !!!

for inc, field in enumerate(['a', 'b', 'c']):
    setattr(X, '%s_summed' % field, X._make_properties(field, inc))

Essayons tout cela maintenant ..

x = X()
assert x.a == 0
assert x.b == 0
assert x.c == 0

assert x.a_summed == 0  # enumerate() set inc to 0 + 0 = 0
assert x.b_summed == 1  # enumerate() set inc to 1 + 0 = 1
assert x.c_summed == 2  # enumerate() set inc to 2 + 0 = 2

# we set the variables to something
x.a = 1
x.b = 2
x.c = 3

assert x.a_summed == 1  # enumerate() set inc to 0 + 1 = 1
assert x.b_summed == 3  # enumerate() set inc to 1 + 2 = 3
assert x.c_summed == 5  # enumerate() set inc to 2 + 3 = 5

# we're changing the inc now
x.a_summed = 1 
x.b_summed = 3 
x.c_summed = 5

assert x.a_summed == 2  # we set inc to 1 + the property was 1 = 2
assert x.b_summed == 5  # we set inc to 3 + the property was 2 = 5
assert x.c_summed == 8  # we set inc to 5 + the property was 3 = 8

Est-ce déroutant? Oui, désolé, je ne pourrais pas vous donner d'exemples significatifs du monde réel. En outre, ce n'est pas pour le cœur léger.

0
Javier Buzzi

Cela semble fonctionner (mais voir ci-dessous):

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 vous avez besoin d'un comportement plus complexe, n'hésitez pas à modifier votre réponse.

modifier

Les éléments suivants seraient probablement plus efficaces en termes de mémoire pour les grands ensembles de données:

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__)
0
David X

Pour répondre à l'essentiel de votre question, vous voulez un attribut en lecture seule à partir d'un dict en tant que source de données immuable:

L'objectif est de créer une classe fictive qui se comporte comme un ensemble de résultats de base de données.

Ainsi, par exemple, si une requête de base de données renvoie, à l'aide d'une expression dict, {'ab':100, 'cd':200}, alors je verrais

>>> dummy.ab
100

Je vais vous montrer comment utiliser une namedtuple du module collections pour accomplir ceci:

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

renvoie 100

0
Aaron Hall

Vous pouvez utiliser le code suivant pour mettre à jour les attributs de classe à l'aide d'un objet dictionnaire:

class ExampleClass():
    def __init__(self, argv):
        for key, val in argv.items():
            self.__dict__[key] = val

if __== '__main__':
    argv = {'intro': 'Hello World!'}
    instance = ExampleClass(argv)
    print instance.intro
0
Anthony Holloman

Bien que de nombreuses réponses soient données, je ne pouvais pas en trouver une qui me satisfasse. J'ai imaginé ma propre solution qui permet à property de fonctionner pour le cas dynamique. La source pour répondre à la question initiale:

#!/usr/local/bin/python3

INITS = { 'ab': 100, 'cd': 200 }

class DP(dict):
  def __init__(self):
    super().__init__()
    for k,v in INITS.items():
        self[k] = v 

def _dict_set(dp, key, value):
  dp[key] = value

for item in INITS.keys():
  setattr(
    DP,
    item,
    lambda key: property(
      lambda self: self[key], lambda self, value: _dict_set(self, key, value)
    )(item)
  )

a = DP()
print(a)  # {'ab': 100, 'cd': 200}
a.ab = 'ab100'
a.cd = False
print(a.ab, a.cd) # ab100 False
0
Rob L

Quelque chose qui fonctionne pour moi est la suivante:

class C:
    def __init__(self):
        self._x=None

    def g(self):
        return self._x

    def s(self, x):
        self._x = x

    def d(self):
        del self._x

    def s2(self,x):
        self._x=x+x

    x=property(g,s,d)


c = C()
c.x="a"
print(c.x)

C.x=property(C.g, C.s2)
C.x=C.x.deleter(C.d)
c2 = C()
c2.x="a"
print(c2.x)

Sortie

a
aa
0
prosti

Extension de l'idée de kjfletch

# This is my humble contribution, extending the idea to serialize
# data from and to tuples, comparison operations and allowing functions
# as default values.

def Struct(*args, **kwargs):
    FUNCTIONS = (types.BuiltinFunctionType, types.BuiltinMethodType, \
                 types.FunctionType, types.MethodType)
    def init(self, *iargs, **ikwargs):
        """Asume that unamed args are placed in the same order than
        astuple() yields (currently alphabetic order)
        """
        kw = list(self.__slots__)

        # set the unnamed args
        for i in range(len(iargs)):
            k = kw.pop(0)
            setattr(self, k, iargs[i])

        # set the named args
        for k, v in ikwargs.items():
            setattr(self, k, v)
            kw.remove(k)

        # set default values
        for k in kw:
            v = kwargs[k]
            if isinstance(v, FUNCTIONS):
                v = v()
            setattr(self, k, v)

    def astuple(self):
        return Tuple([getattr(self, k) for k in self.__slots__])

    def __str__(self):
        data = ['{}={}'.format(k, getattr(self, k)) for k in self.__slots__]
        return '<{}: {}>'.format(self.__class__.__name__, ', '.join(data))

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return self.astuple() == other.astuple()

    name = kwargs.pop("__name__", "MyStruct")
    slots = list(args)
    slots.extend(kwargs.keys())
    # set non-specific default values to None
    kwargs.update(dict((k, None) for k in args))

    return type(name, (object,), {
        '__init__': init,
        '__slots__': Tuple(slots),
        'astuple': astuple,
        '__str__': __str__,
        '__repr__': __repr__,
        '__eq__': __eq__,
    })


Event = Struct('user', 'cmd', \
               'arg1', 'arg2',  \
               date=time.time, \
               __name__='Event')

aa = Event('pepe', 77)
print(aa)
raw = aa.astuple()

bb = Event(*raw)
print(bb)

if aa == bb:
    print('Are equals')

cc = Event(cmd='foo')
print(cc)

Sortie:

<Event: user=pepe, cmd=77, arg1=None, arg2=None, date=1550051398.3651814>
<Event: user=pepe, cmd=77, arg1=None, arg2=None, date=1550051398.3651814>
Are equals
<Event: user=None, cmd=foo, arg1=None, arg2=None, date=1550051403.7938335>
0
asterio gonzalez
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)

Et le résultat est:

>> 1
0
Serhii Khachko