Uso di property () su classmethods


174

Ho una classe con due metodi di classe (usando la funzione classmethod ()) per ottenere e impostare quella che è essenzialmente una variabile statica. Ho provato a usare la funzione property () con questi, ma risulta in un errore. Sono stato in grado di riprodurre l'errore con l'interprete seguente:

class Foo(object):
    _var = 5
    @classmethod
    def getvar(cls):
        return cls._var
    @classmethod
    def setvar(cls, value):
        cls._var = value
    var = property(getvar, setvar)

Posso dimostrare i metodi di classe, ma non funzionano come proprietà:

>>> f = Foo()
>>> f.getvar()
5
>>> f.setvar(4)
>>> f.getvar()
4
>>> f.var
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable
>>> f.var=5
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable

È possibile utilizzare la funzione property () con funzioni decorate classmethod?

Risposte:


90

Una proprietà viene creata su una classe ma influisce su un'istanza. Quindi, se si desidera una proprietà classmethod, creare la proprietà sulla metaclasse.

>>> class foo(object):
...     _var = 5
...     class __metaclass__(type):  # Python 2 syntax for metaclasses
...         pass
...     @classmethod
...     def getvar(cls):
...         return cls._var
...     @classmethod
...     def setvar(cls, value):
...         cls._var = value
...     
>>> foo.__metaclass__.var = property(foo.getvar.im_func, foo.setvar.im_func)
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

Ma dal momento che stai usando una metaclasse, leggerà meglio se sposti solo i metodi di classe.

>>> class foo(object):
...     _var = 5
...     class __metaclass__(type):  # Python 2 syntax for metaclasses
...         @property
...         def var(cls):
...             return cls._var
...         @var.setter
...         def var(cls, value):
...             cls._var = value
... 
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

oppure, usando la metaclass=...sintassi di Python 3 , la metaclasse definita al di fuori del foocorpo della classe e la metaclasse responsabile dell'impostazione del valore iniziale di _var:

>>> class foo_meta(type):
...     def __init__(cls, *args, **kwargs):
...         cls._var = 5
...     @property
...     def var(cls):
...         return cls._var
...     @var.setter
...     def var(cls, value):
...         cls._var = value
...
>>> class foo(metaclass=foo_meta):
...     pass
...
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

1
Questo non sembra funzionare per me in Python 3.2. Se cambio foo .__ metaclass __. Var = proprietà (foo.getvar.im_func, foo.setvar.im_func) in foo .__ metaclass __. Var = proprietà (foo.getvar .__ func__, foo.setvar .__ func__) ottengo "AttributeError: type l'oggetto 'pippo' non ha alcun attributo 'var' "quando si esegue" pippo.var ".
Michael Kelley,

Doppia correzione SIGH : funziona in Python 2.7, ma non in Python 3.2.
Michael Kelley,

@MichaelKelley - Questo perché la sintassi dei metaclassi è cambiata in Python 3.x
mac

1
Non sono sicuro di capire, quale sarebbe il modo Python 3.x per scrivere questo allora?
SylvainD,

8
@Josay: dovresti prima definire la metaclasse, quindi definire la classe usando la nuova class Foo(metaclass=...)sintassi.
Kevin,

69

Leggendo le note di rilascio di Python 2.2 , trovo quanto segue.

Il metodo get [di una proprietà] non verrà chiamato quando si accede alla proprietà come attributo di classe (Cx) anziché come attributo di istanza (C (). X). Se si desidera sovrascrivere l'operazione __get__ per le proprietà quando utilizzato come attributo di classe, è possibile sottoclassare la proprietà - è un tipo di nuovo stile stesso - per estendere il suo metodo __get__ oppure è possibile definire un tipo di descrittore da zero creando un nuovo classe-style che definisce i metodi __get__, __set__ e __delete__.

NOTA: il metodo seguente non funziona in realtà per i setter, solo per i getter.

Pertanto, credo che la soluzione prescritta sia quella di creare una ClassProperty come sottoclasse di proprietà.

class ClassProperty(property):
    def __get__(self, cls, owner):
        return self.fget.__get__(None, owner)()

class foo(object):
    _var=5
    def getvar(cls):
        return cls._var
    getvar=classmethod(getvar)
    def setvar(cls,value):
        cls._var=value
    setvar=classmethod(setvar)
    var=ClassProperty(getvar,setvar)

assert foo.getvar() == 5
foo.setvar(4)
assert foo.getvar() == 4
assert foo.var == 4
foo.var = 3
assert foo.var == 3

Tuttavia, i setter in realtà non funzionano:

foo.var = 4
assert foo.var == foo._var # raises AssertionError

foo._var è invariato, hai semplicemente sovrascritto la proprietà con un nuovo valore.

Puoi anche usare ClassPropertycome decoratore:

class foo(object):
    _var = 5

    @ClassProperty
    @classmethod
    def var(cls):
        return cls._var

    @var.setter
    @classmethod
    def var(cls, value):
        cls._var = value

assert foo.var == 5

20
Non penso che la parte setter di ClassProperty funzioni effettivamente come descritto: mentre tutte le affermazioni dell'esempio passano, alla fine foo._var == 4 (non 3, come implicito). L'impostazione della proprietà blocca la proprietà stessa. Quando le proprietà delle classi sono state discusse su Python-Dev, è stato sottolineato che, mentre i getter sono banali, i setter sono difficili (impossibili?) Senza una metaclasse
Gabriel Grant,

4
@Gabriel Totalmente corretto. Non posso credere che nessuno lo abbia sottolineato per due anni.
AGF

Inoltre, non sono sicuro del motivo per cui non si utilizza self.fget(owner)e non si rimuove semplicemente la necessità di utilizzare @classmethoda qui? (questo è ciò che classmethod fa , tradurre .__get__(instance, owner)(*args, **kwargs)in function(owner, *args, **kwargs)chiamate, tramite un intermediario; le proprietà non hanno bisogno dell'intermediario).
Martijn Pieters

Nella tua dimostrazione manca qualsiasi trasformazione effettiva nel getter o nel setter che dimostrerebbe chiaramente che il tuo foo.var = 3compito in realtà non passa attraverso la proprietà e invece ha semplicemente sostituito l'oggetto della proprietà foocon un numero intero. Se hai aggiunto assert isinstance(foo.__dict__['var'], ClassProperty)chiamate tra le tue affermazioni vedresti che falliscono dopo che foo.var = 3è stata eseguita.
Martijn Pieters

1
Classi Python non lo fanno descrittore di sostegno vincolante per l'impostazione sulla classe stessa , ma solo su come ottenere (così instance.attr, instance.attr = valuee del instance.attrsaranno tutti legano il descrittore trovato su type(instance), ma mentre classobj.attrsi lega, classobj.attr = valuee del classobj.attrnon non e invece sostituire o eliminare l'oggetto descrittore stesso). È necessaria una metaclasse per supportare l'impostazione e l'eliminazione (rendendo l'oggetto della classe l'istanza e la metaclasse il tipo).
Martijn Pieters

56

Spero che questo semplice @classpropertydecoratore di sola lettura possa aiutare qualcuno alla ricerca di proprietà di classe.

class classproperty(object):

    def __init__(self, fget):
        self.fget = fget

    def __get__(self, owner_self, owner_cls):
        return self.fget(owner_cls)

class C(object):

    @classproperty
    def x(cls):
        return 1

assert C.x == 1
assert C().x == 1

2
Funziona con le sottoclassi? (può una sottoclasse scavalcare i beni di classe?)
zakdances

1
Umm si? class D(C): x = 2; assert D.x == 2
Dtheodor,

Vorrei che funzionasse quando lo uso in modo .formatsimile "{x}".format(**dict(self.__class__.__dict__, **self.__dict__)):(
2rs2ts

@Nathan Non solo ... quando lo imposti sovrascrivi tutti gli xaccessi di 10. Mi piace questo approccio perché è pulito e semplice ma suona come un antipasto
Michele d'Amico,

Facilmente risolto: aggiungi un __set__che solleva un ValueErrorper evitare l'override.
Kiran Jonnalagadda,

30

È possibile utilizzare la funzione property () con funzioni decorate classmethod?

No.

Tuttavia, un metodo di classe è semplicemente un metodo associato (una funzione parziale) su una classe accessibile dalle istanze di quella classe.

Poiché l'istanza è una funzione della classe e puoi derivare la classe dall'istanza, puoi ottenere qualsiasi comportamento desiderato da una proprietà di classe con property:

class Example(object):
    _class_property = None
    @property
    def class_property(self):
        return self._class_property
    @class_property.setter
    def class_property(self, value):
        type(self)._class_property = value
    @class_property.deleter
    def class_property(self):
        del type(self)._class_property

Questo codice può essere utilizzato per testare: dovrebbe passare senza generare errori:

ex1 = Example()
ex2 = Example()
ex1.class_property = None
ex2.class_property = 'Example'
assert ex1.class_property is ex2.class_property
del ex2.class_property
assert not hasattr(ex1, 'class_property')

E nota che non avevamo affatto bisogno di metaclasse e che comunque non accedi direttamente a una metaclasse attraverso le istanze delle sue classi.

scrivendo a @classproperty decoratore

Puoi effettivamente creare un classpropertydecoratore in poche righe di codice eseguendo la sottoclasse property(è implementato in C, ma puoi vedere Python equivalente qui ):

class classproperty(property):
    def __get__(self, obj, objtype=None):
        return super(classproperty, self).__get__(objtype)
    def __set__(self, obj, value):
        super(classproperty, self).__set__(type(obj), value)
    def __delete__(self, obj):
        super(classproperty, self).__delete__(type(obj))

Quindi tratta il decoratore come se fosse un metodo di classe combinato con la proprietà:

class Foo(object):
    _bar = 5
    @classproperty
    def bar(cls):
        """this is the bar attribute - each subclass of Foo gets its own.
        Lookups should follow the method resolution order.
        """
        return cls._bar
    @bar.setter
    def bar(cls, value):
        cls._bar = value
    @bar.deleter
    def bar(cls):
        del cls._bar

E questo codice dovrebbe funzionare senza errori:

def main():
    f = Foo()
    print(f.bar)
    f.bar = 4
    print(f.bar)
    del f.bar
    try:
        f.bar
    except AttributeError:
        pass
    else:
        raise RuntimeError('f.bar must have worked - inconceivable!')
    help(f)  # includes the Foo.bar help.
    f.bar = 5

    class Bar(Foo):
        "a subclass of Foo, nothing more"
    help(Bar) # includes the Foo.bar help!
    b = Bar()
    b.bar = 'baz'
    print(b.bar) # prints baz
    del b.bar
    print(b.bar) # prints 5 - looked up from Foo!

    
if __name__ == '__main__':
    main()

Ma non sono sicuro di quanto ben informato sarebbe. Un vecchio articolo della mailing list suggerisce che non dovrebbe funzionare.

Far funzionare la proprietà sulla classe:

L'aspetto negativo di quanto sopra è che la "proprietà di classe" non è accessibile dalla classe, perché semplicemente sovrascriverebbe il descrittore di dati dalla classe __dict__ .

Tuttavia, possiamo ignorarlo con una proprietà definita nella metaclasse __dict__. Per esempio:

class MetaWithFooClassProperty(type):
    @property
    def foo(cls):
        """The foo property is a function of the class -
        in this case, the trivial case of the identity function.
        """
        return cls

E quindi un'istanza di classe della metaclasse potrebbe avere una proprietà che accede alla proprietà della classe usando il principio già dimostrato nelle sezioni precedenti:

class FooClassProperty(metaclass=MetaWithFooClassProperty):
    @property
    def foo(self):
        """access the class's property"""
        return type(self).foo

E ora vediamo entrambi l'istanza

>>> FooClassProperty().foo
<class '__main__.FooClassProperty'>

e la classe

>>> FooClassProperty.foo
<class '__main__.FooClassProperty'>

avere accesso alla proprietà della classe.


3
Chiaro, conciso, approfondito: questa dovrebbe essere la risposta accettata.
pfabri,

25

Python 3!

Vecchia domanda, molti punti di vista, assolutamente bisognoso di un vero Python 3 way.

Fortunatamente, è facile con il metaclasskwarg:

class FooProperties(type):

    @property
    def var(cls):
        return cls._var

class Foo(object, metaclass=FooProperties):
    _var = 'FOO!'

Poi, >>> Foo.var

'FOO!'


1
vale a dire che non esiste una via d'uscita semplice
mehmet

@mehmet Non è semplice? Fooè un'istanza della sua metaclasse e @propertypuò essere utilizzata per i suoi metodi così come per quelli di istanze di Foo.
OJFord,

2
dovevi definire un'altra classe per una classe, che è il doppio della complessità ipotizzando che la metaclasse non sia riutilizzabile.
Mehmet,

Un metodo di classe funziona sia dalla classe che dall'istanza. Questa proprietà funziona solo dalla classe. Non credo sia questo ciò che viene chiesto.
Aaron Hall

1
@AaronHall Se questo è importante, può essere facilmente aggiunto Foo.__new__. Anche se a quel punto potrebbe valere la pena usare getattribute o chiedersi se pretendere che esista una caratteristica del linguaggio è davvero l'approccio che si desidera adottare.
OJFord,

16

Non esiste un modo ragionevole per far funzionare questo sistema "class property" in Python.

Ecco un modo irragionevole per farlo funzionare. Puoi sicuramente renderlo più fluido con quantità crescenti di magia metaclasse.

class ClassProperty(object):
    def __init__(self, getter, setter):
        self.getter = getter
        self.setter = setter
    def __get__(self, cls, owner):
        return getattr(cls, self.getter)()
    def __set__(self, cls, value):
        getattr(cls, self.setter)(value)

class MetaFoo(type):
    var = ClassProperty('getvar', 'setvar')

class Foo(object):
    __metaclass__ = MetaFoo
    _var = 5
    @classmethod
    def getvar(cls):
        print "Getting var =", cls._var
        return cls._var
    @classmethod
    def setvar(cls, value):
        print "Setting var =", value
        cls._var = value

x = Foo.var
print "Foo.var = ", x
Foo.var = 42
x = Foo.var
print "Foo.var = ", x

Il nodo del problema è che le proprietà sono quelle che Python chiama "descrittori". Non esiste un modo semplice e breve per spiegare come funziona questo tipo di metaprogrammazione, quindi devo indicarti il descrittore howto .

Hai solo bisogno di capire questo genere di cose se stai implementando un framework abbastanza avanzato. Come una persistenza dell'oggetto trasparente o un sistema RPC o una specie di linguaggio specifico del dominio.

Tuttavia, in un commento a una risposta precedente, dici di si

è necessario modificare un attributo che sia visto da tutte le istanze di una classe e nell'ambito da cui vengono chiamati questi metodi di classe non ha riferimenti a tutte le istanze della classe.

Mi sembra che quello che vuoi davvero sia un modello di progettazione di Observer .


Mi piace l'idea dell'esempio di codice, ma sembra che in pratica sarebbe un po 'goffo.
Mark Roddy,

Quello che sto cercando di realizzare è un'impostazione piuttosto semplice e l'ottenimento di un singolo attributo che viene usato come flag per modificare il comportamento di tutte le istanze, quindi penso che Observer sarebbe esagerato per quello che sto cercando di fare. Se ci fossero più attributi in questione, sarei più propenso.
Mark Roddy,

Sembra che semplicemente rendere pubbliche le funzioni e chiamarle direttamente fosse la soluzione più semplice. Ero curioso di sapere se stavo facendo qualcosa di sbagliato o se stavo cercando di farlo era impossibile. Mi dispiace per i molteplici commenti tra l'altro. Un limite di 300 caratteri fa schifo.
Mark Roddy,

La cosa bella dell'esempio di codice è che puoi implementare tutti i bit goffi una volta e poi ereditarli. Basta spostare _var nella classe derivata. classe D1 (Foo): _var = 21 classe D2 (Foo) _var = "Hello" D1.var 21 D2.var Hello
Thomas L Holaday

6

L'impostazione solo sulla meta classe non aiuta se si desidera accedere alla proprietà della classe tramite un oggetto istanziato, in questo caso è necessario installare anche una proprietà normale sull'oggetto (che viene inviato alla proprietà della classe). Penso che quanto segue sia un po 'più chiaro:

#!/usr/bin/python

class classproperty(property):
    def __get__(self, obj, type_):
        return self.fget.__get__(None, type_)()

    def __set__(self, obj, value):
        cls = type(obj)
        return self.fset.__get__(None, cls)(value)

class A (object):

    _foo = 1

    @classproperty
    @classmethod
    def foo(cls):
        return cls._foo

    @foo.setter
    @classmethod
    def foo(cls, value):
        cls.foo = value

a = A()

print a.foo

b = A()

print b.foo

b.foo = 5

print a.foo

A.foo = 10

print b.foo

print A.foo

3

Mezza soluzione, __set__ sulla classe non funziona, comunque. La soluzione è una classe di proprietà personalizzata che implementa sia una proprietà che un metodo statico

class ClassProperty(object):
    def __init__(self, fget, fset):
        self.fget = fget
        self.fset = fset

    def __get__(self, instance, owner):
        return self.fget()

    def __set__(self, instance, value):
        self.fset(value)

class Foo(object):
    _bar = 1
    def get_bar():
        print 'getting'
        return Foo._bar

    def set_bar(value):
        print 'setting'
        Foo._bar = value

    bar = ClassProperty(get_bar, set_bar)

f = Foo()
#__get__ works
f.bar
Foo.bar

f.bar = 2
Foo.bar = 3 #__set__ does not

3

Perché ho bisogno di modificare un attributo che sia visto da tutte le istanze di una classe e nell'ambito da cui vengono chiamati questi metodi di classe non ha riferimenti a tutte le istanze della classe.

Hai accesso ad almeno un'istanza della classe? Mi viene in mente un modo per farlo allora:

class MyClass (object):
    __var = None

    def _set_var (self, value):
        type (self).__var = value

    def _get_var (self):
        return self.__var

    var = property (_get_var, _set_var)

a = MyClass ()
b = MyClass ()
a.var = "foo"
print b.var

2

Fai una prova, fa il lavoro senza dover cambiare / aggiungere molto codice esistente.

>>> class foo(object):
...     _var = 5
...     def getvar(cls):
...         return cls._var
...     getvar = classmethod(getvar)
...     def setvar(cls, value):
...         cls._var = value
...     setvar = classmethod(setvar)
...     var = property(lambda self: self.getvar(), lambda self, val: self.setvar(val))
...
>>> f = foo()
>>> f.var
5
>>> f.var = 3
>>> f.var
3

La propertyfunzione richiede due callableargomenti. dai loro wrapper lambda (che passa l'istanza come primo argomento) e tutto va bene.


Come sottolinea Florian Bösch, la sintassi richiesta (da librerie di terze parti o codice legacy) è foo.var.
Thomas L Holaday,

2

Ecco una soluzione che dovrebbe funzionare sia per l'accesso tramite la classe che per l'accesso tramite un'istanza che utilizza una metaclasse.

In [1]: class ClassPropertyMeta(type):
   ...:     @property
   ...:     def prop(cls):
   ...:         return cls._prop
   ...:     def __new__(cls, name, parents, dct):
   ...:         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
   ...:         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
   ...:         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
   ...:         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)
   ...:

In [2]: class ClassProperty(object):
   ...:     __metaclass__ = ClassPropertyMeta
   ...:     _prop = 42
   ...:     def __getattr__(self, attr):
   ...:         raise Exception('Never gets called')
   ...:

In [3]: ClassProperty.prop
Out[3]: 42

In [4]: ClassProperty.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-e2e8b423818a> in <module>()
----> 1 ClassProperty.prop = 1

AttributeError: can't set attribute

In [5]: cp = ClassProperty()

In [6]: cp.prop
Out[6]: 42

In [7]: cp.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-e8284a3ee950> in <module>()
----> 1 cp.prop = 1

<ipython-input-1-16b7c320d521> in <lambda>(cls, attr, val)
      6         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
      7         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
----> 8         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
      9         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)

AttributeError: can't set attribute

Questo funziona anche con un setter definito nella metaclasse.


1

Dopo aver cercato luoghi diversi, ho trovato un metodo per definire una proprietà di classe valida con Python 2 e 3.

from future.utils import with_metaclass

class BuilderMetaClass(type):
    @property
    def load_namespaces(self):
        return (self.__sourcepath__)

class BuilderMixin(with_metaclass(BuilderMetaClass, object)):
    __sourcepath__ = 'sp'        

print(BuilderMixin.load_namespaces)

Spero che questo possa aiutare qualcuno :)


1
Se questo metodo è qualcosa che hai trovato da qualche parte, sarebbe bene dare un link (vedi Come fare riferimento al materiale scritto da altri )
Andrew Myers,

-27

Ecco il mio suggerimento. Non usare metodi di classe.

Sul serio.

Qual è la ragione per usare i metodi di classe in questo caso? Perché non avere un oggetto ordinario di una classe ordinaria?


Se vuoi semplicemente cambiare il valore, una proprietà non è davvero molto utile, vero? Basta impostare il valore dell'attributo e completarlo.

Una proprietà dovrebbe essere utilizzata solo se c'è qualcosa da nascondere, qualcosa che potrebbe cambiare in una futura implementazione.

Forse il tuo esempio è molto ridotto e c'è qualche calcolo infernale che hai interrotto. Ma non sembra che la proprietà abbia un valore significativo.

Le tecniche di "privacy" influenzate da Java (in Python, nomi di attributi che iniziano con _) non sono molto utili. Privato da chi? Il punto privato è un po 'nebuloso quando hai la fonte (come fai in Python).

I setter e setter in stile EJB influenzati da Java (spesso fatti come proprietà in Python) sono lì per facilitare l'introspezione primitiva di Java e per passare il raduno con il compilatore di linguaggio statico. Tutti quei getter e setter non sono altrettanto utili in Python.


14
Perché ho bisogno di modificare un attributo che sia visto da tutte le istanze di una classe e nell'ambito da cui vengono chiamati questi metodi di classe non ha riferimenti a tutte le istanze della classe.
Mark Roddy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.