Sottoclasse: è possibile sovrascrivere una proprietà con un attributo convenzionale?


14

Supponiamo di voler creare una famiglia di classi che siano diverse implementazioni o specializzazioni di un concetto generale. Supponiamo che ci sia un'implementazione di default plausibile per alcune proprietà derivate. Vorremmo metterlo in una classe base

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

Quindi una sottoclasse sarà automaticamente in grado di contare i suoi elementi in questo esempio piuttosto sciocco

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

Concrete_Math_Set(1,2,3).size
# 3

Ma cosa succede se una sottoclasse non desidera utilizzare questo valore predefinito? Questo non funziona:

import math

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

Square_Integers_Below(7)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 3, in __init__
# AttributeError: can't set attribute

Mi rendo conto che ci sono modi per scavalcare una proprietà con una proprietà, ma vorrei evitarlo. Poiché lo scopo della classe base è quello di rendere la vita il più semplice possibile per il suo utente, non aggiungere un gonfiamento imponendo un metodo di accesso contorto e superfluo (dal punto di vista ristretto della sottoclasse).

Si può fare? In caso contrario, qual è la prossima soluzione migliore?

Risposte:


6

Una proprietà è un descrittore di dati che ha la precedenza su un attributo di istanza con lo stesso nome. È possibile definire un descrittore non di dati con un __get__()metodo univoco : un attributo di istanza ha la precedenza sul descrittore di non dati con lo stesso nome, consultare i documenti . Il problema qui è che il non_data_propertydefinito di seguito è solo a scopo di calcolo (non è possibile definire un setter o un deleter) ma sembra essere il caso nel tuo esempio.

import math

class non_data_property:
    def __init__(self, fget):
        self.__doc__ = fget.__doc__
        self.fget = fget

    def __get__(self, obj, cls):
        if obj is None:
            return self
        return self.fget(obj)

class Math_Set_Base:
    @non_data_property
    def size(self, *elements):
        return len(self.elements)

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3

Tuttavia, si presuppone che si abbia accesso alla classe base per apportare queste modifiche.


Questo è abbastanza vicino alla perfezione. Quindi questo significa poperty anche se si definisce solo il getter aggiunge implicitamente un setter che non fa nulla a parte bloccare il compito? Interessante. Accetterò probabilmente questa risposta.
Paul Panzer,

Sì, secondo i documenti, una proprietà definisce sempre tutti e tre i metodi di descrittore ( __get__(), __set__()e __delete__()) ed essi sollevano una AttributeErrorse non si fornisce alcuna funzione per loro. Vedi Equivalente Python dell'implementazione della proprietà .
Arkelis,

1
Finché non ti interessa la proprietà settero, __set__la proprietà funzionerà. Tuttavia, ti avvertirei che ciò significa anche che puoi facilmente sovrascrivere la proprietà non solo a livello di classe ( self.size = ...) ma anche a livello di istanza (ad esempio, Concrete_Math_Set(1, 2, 3).size = 10sarebbe ugualmente valido). Cibo per pensieri :)
r.ook

Ed Square_Integers_Below(9).size = 1è anche valido in quanto è un attributo semplice. In questo particolare caso d'uso "size", questo può sembrare strano (usa invece una proprietà), ma nel caso generale "sostituisci facilmente un oggetto di calcolo calcolato in una sottoclasse", in alcuni casi potrebbe essere buono. Puoi anche controllare l'accesso agli attributi con, __setattr__()ma potrebbe essere travolgente.
Arkelis,

1
Non sono in disaccordo sul fatto che potrebbe esserci un caso d'uso valido per questi, volevo solo menzionare l'avvertenza in quanto può complicare gli sforzi di debug in futuro. Finché sia ​​tu che il PO siete consapevoli delle implicazioni, allora tutto va bene.
Guarda il

10

Questa sarà una risposta lunga e tortuosa che potrebbe solo essere gratuita ... ma la tua domanda mi ha portato a fare un giro nella tana del coniglio, quindi vorrei condividere anche i miei risultati (e il dolore).

Alla fine potresti trovare questa risposta non utile al tuo vero problema. In effetti, la mia conclusione è che non lo farei affatto. Detto questo, lo sfondo di questa conclusione potrebbe intrattenerti un po ', dal momento che stai cercando maggiori dettagli.


Affrontare alcuni malintesi

La prima risposta, sebbene corretta nella maggior parte dei casi, non è sempre il caso. Ad esempio, considera questa classe:

class Foo:
    def __init__(self):
        self.name = 'Foo!'
        @property
        def inst_prop():
            return f'Retrieving {self.name}'
        self.inst_prop = inst_prop

inst_prop, pur essendo a property, è irrevocabilmente un attributo di istanza:

>>> Foo.inst_prop
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'

Tutto dipende da dove il tuo propertyè definito in primo luogo. Se il tuo @propertyè definito nella classe "scope" (o davvero, il namespace), diventa un attributo di classe. Nel mio esempio, la classe stessa non ne è a conoscenza inst_propfino a quando non viene istanziata. Naturalmente, non è molto utile come proprietà qui.


Ma prima, affrontiamo il tuo commento sulla risoluzione dell'ereditarietà ...

Quindi, in che modo l'ereditarietà tiene conto di questo problema? Questo articolo che segue approfondisce un po 'l'argomento e l' Ordine di risoluzione del metodo è in qualche modo correlato, anche se discute principalmente sull'ampiezza dell'eredità anziché sulla profondità.

Insieme alla nostra scoperta, date queste impostazioni di seguito:

@property
def some_prop(self):
    return "Family property"

class Grandparent:
    culture = some_prop
    world_view = some_prop

class Parent(Grandparent):
    world_view = "Parent's new world_view"

class Child(Parent):
    def __init__(self):
        try:
            self.world_view = "Child's new world_view"
            self.culture = "Child's new culture"
        except AttributeError as exc:
            print(exc)
            self.__dict__['culture'] = "Child's desired new culture"

Immagina cosa succede quando vengono eseguite queste linee:

print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

Il risultato è quindi:

Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>

Si noti come:

  1. self.world_viewè stato possibile applicare, anche se self.culturenon riuscito
  2. culturenon esiste in Child.__dict__(il mappingproxydella classe, da non confondere con l'istanza __dict__)
  3. Anche se cultureesiste in c.__dict__, non è referenziato.

Potresti essere in grado di indovinare perché - è world_viewstato sovrascritto dalla Parentclasse come non-proprietà, quindi è Childstato anche in grado di sovrascriverlo. Nel frattempo, poiché cultureè ereditato, esiste solo all'interno mappingproxydiGrandparent :

Grandparent.__dict__ is: {
    '__module__': '__main__', 
    'culture': <property object at 0x00694C00>, 
    'world_view': <property object at 0x00694C00>, 
    ...
}

Infatti se provi a rimuovere Parent.culture:

>>> del Parent.culture
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    del Parent.culture
AttributeError: culture

Noterai che non esiste nemmeno per Parent. Perché l'oggetto fa direttamente riferimento a Grandparent.culture.


Quindi, per quanto riguarda l'ordine di risoluzione?

Quindi siamo interessati a osservare l'attuale Ordine di risoluzione, proviamo Parent.world_viewinvece a rimuovere :

del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Mi chiedo quale sia il risultato?

c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>

È tornato ai nonni world_view property, anche se siamo riusciti a assegnare il self.world_viewprima! E se cambiassimo con forza world_viewa livello di classe, come l'altra risposta? E se lo eliminassimo? Cosa succede se assegniamo l'attributo di classe corrente a una proprietà?

Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Il risultato è:

# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view

# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view

# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>

Questo è interessante perché c.world_viewviene ripristinato il suo attributo di istanza, mentre Child.world_viewè quello che abbiamo assegnato. Dopo aver rimosso l'attributo dell'istanza, ritorna all'attributo class. E dopo aver riassegnato la Child.world_viewproprietà, perdiamo immediatamente l'accesso all'attributo dell'istanza.

Pertanto, possiamo ipotizzare il seguente ordine di risoluzione :

  1. Se esiste un attributo di classe ed è un property, recuperane il valore tramite gettero fget(ne parleremo più avanti). Prima la classe corrente per ultima.
  2. Altrimenti, se esiste un attributo di istanza, recuperare il valore dell'attributo di istanza.
  3. Altrimenti, recupera l' propertyattributo non di classe. Prima la classe corrente per ultima.

In tal caso, rimuoviamo il root property:

del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

Che dà:

c.culture is: Child's desired new culture
Traceback (most recent call last):
  File "<pyshell#74>", line 1, in <module>
    print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'

Ta-dah! Childora ha il proprio culturebasato sull'inserimento forzato in c.__dict__. Child.culturenon esiste, ovviamente, poiché non è mai stato definito in Parento Childattributo di classe ed Grandparentè stato rimosso.


È questa la causa principale del mio problema?

In realtà no . L'errore che stai riscontrando, che stiamo ancora osservando durante l'assegnazione self.culture, è totalmente diverso . Ma l'ordine di eredità fa da sfondo alla risposta, che è la propertystessa.

Oltre al gettermetodo precedentemente citato , propertyhai anche qualche asso nella manica. Il più rilevante in questo caso è il metodo, settero fset, che viene attivato dalla self.culture = ...riga. Dato che propertynon hai implementato nessuna settero nessuna fgetfunzione, python non sa cosa fare e lancia AttributeErrorinvece (cioè can't set attribute).

Se tuttavia hai implementato un settermetodo:

@property
def some_prop(self):
    return "Family property"

@some_prop.setter
def some_prop(self, val):
    print(f"property setter is called!")
    # do something else...

Quando crei un'istanza della Childclasse otterrai:

Instantiating Child class...
property setter is called!

Invece di ricevere un AttributeError, ora stai effettivamente chiamando il some_prop.settermetodo. Il che ti dà un maggiore controllo sul tuo oggetto ... con i nostri risultati precedenti, sappiamo che è necessario sovrascrivere un attributo di classe prima che raggiunga la proprietà. Questo potrebbe essere implementato all'interno della classe base come trigger. Ecco un nuovo esempio:

class Grandparent:
    @property
    def culture(self):
        return "Family property"

    # add a setter method
    @culture.setter
    def culture(self, val):
        print('Fine, have your own culture')
        # overwrite the child class attribute
        type(self).culture = None
        self.culture = val

class Parent(Grandparent):
    pass

class Child(Parent):
    def __init__(self):
        self.culture = "I'm a millennial!"

c = Child()
print(c.culture)

Che si traduce in:

Fine, have your own culture
I'm a millennial!

TA-DAH! Ora puoi sovrascrivere il tuo attributo di istanza su una proprietà ereditata!


Quindi, problema risolto?

... Non proprio. Il problema con questo approccio è che ora non puoi avere un settermetodo adeguato . Ci sono casi in cui vuoi impostare valori sul tuo property. Ma ora ogni volta che si imposta self.culture = ...sarà sempre sovrascriverà qualsiasi funzione definita nella getter(che in questo caso, è in realtà solo la @propertyporzione avvolta. È possibile aggiungere misure più sfumata, ma un modo o nell'altro lo sarai sempre coinvolgere più di un semplice self.culture = .... per esempio:

class Grandparent:
    # ...
    @culture.setter
    def culture(self, val):
        if isinstance(val, tuple):
            if val[1]:
                print('Fine, have your own culture')
                type(self).culture = None
                self.culture = val[0]
        else:
            raise AttributeError("Oh no you don't")

# ...

class Child(Parent):
    def __init__(self):
        try:
            # Usual setter
            self.culture = "I'm a Gen X!"
        except AttributeError:
            # Trigger the overwrite condition
            self.culture = "I'm a Boomer!", True

È waaaaay più complicato dell'altra risposta, size = Nonea livello di classe.

Si potrebbe anche considerare scrivere il proprio descrittore , invece di gestire l' __get__e __set__, o metodi aggiuntivi. Ma alla fine della giornata, quando si self.culturefa riferimento, __get__verrà sempre innescato per primo, e quando si self.culture = ...fa riferimento, __set__sarà sempre innescato per primo. Non c'è modo di aggirarlo per quanto ho provato.


Il nocciolo della questione, IMO

Il problema che vedo qui è: non puoi avere la tua torta e mangiarla anche tu. propertysi intende come un descrittore con comodo accesso da metodi come getattro setattr. Se vuoi che anche questi metodi raggiungano uno scopo diverso, stai solo chiedendo problemi. Vorrei forse ripensare l'approccio:

  1. Ho davvero bisogno di un propertyper questo?
  2. Un metodo potrebbe servirmi in modo diverso?
  3. Se ho bisogno di un property, c'è qualche motivo per cui dovrei sovrascriverlo?
  4. La sottoclasse appartiene davvero alla stessa famiglia se questi propertynon si applicano?
  5. Se avessi bisogno di sovrascrivere qualsiasi / tutti gli propertys, un metodo separato mi servirebbe meglio della semplice riassegnazione, dal momento che la riassegnazione può annullare accidentalmente la propertys?

Per il punto 5, il mio approccio dovrebbe avere un overwrite_prop()metodo nella classe base che sovrascriva l'attributo della classe corrente in modo che il propertytestamento non venga più attivato:

class Grandparent:
    # ...
    def overwrite_props(self):
        # reassign class attributes
        type(self).size = None
        type(self).len = None
        # other properties, if necessary

# ...

# Usage
class Child(Parent):
    def __init__(self):
        self.overwrite_props()
        self.size = 5
        self.len = 10

Come puoi vedere, sebbene sia ancora un po 'inventato, è almeno più esplicito di un criptico size = None. Detto questo, in definitiva, non avrei sovrascritto la proprietà e avrei riconsiderato il mio progetto dalla radice.

Se sei arrivato così lontano, grazie per aver fatto questo viaggio con me. È stato un piccolo esercizio divertente.


2
Wow, grazie mille! Mi ci vorrà un po 'di tempo per digerire questo, ma suppongo di averlo chiesto.
Paul Panzer,

6

A @propertyè definito a livello di classe. La documentazione fornisce dettagli esaustivi su come funziona, ma è sufficiente dire che l' impostazione o il ripristino della proprietà si risolvono nel chiamare un particolare metodo. Tuttavia, l' propertyoggetto che gestisce questo processo è definito con la definizione propria della classe. Cioè, è definita come una variabile di classe ma si comporta come una variabile di istanza.

Una conseguenza di ciò è che puoi riassegnarlo liberamente a livello di classe :

print(Math_Set_Base.size)
# <property object at 0x10776d6d0>

Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4

E proprio come qualsiasi altro nome a livello di classe (ad esempio metodi), puoi sovrascriverlo in una sottoclasse semplicemente definendolo in modo esplicito in modo diverso:

class Square_Integers_Below(Math_Set_Base):
    # explicitly define size at the class level to be literally anything other than a @property
    size = None

    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Square_Integers_Below(4).size)  # 2
print(Square_Integers_Below.size)     # None

Quando creiamo un'istanza effettiva, la variabile di istanza semplicemente ombreggia la variabile di classe con lo stesso nome. L' propertyoggetto normalmente usa alcuni shenanigans per manipolare questo processo (cioè applicando getter e setter) ma quando il nome a livello di classe non è definito come una proprietà, non accade nulla di speciale, e quindi agisce come ci si aspetterebbe da qualsiasi altra variabile.


Grazie, è piacevolmente semplice. Vuol dire che anche se non ci fossero mai proprietà da nessuna parte nella mia classe e nei suoi antenati, cercherà comunque in tutto l'albero dell'albero ereditario un nome di livello prima ancora che si preoccupi di guardare __dict__? Inoltre, ci sono modi per automatizzare questo? Sì, è solo una riga ma è il tipo di cosa che è molto criptico da leggere se non si ha familiarità con i dettagli cruenti delle proprietà ecc.
Paul Panzer,

@PaulPanzer Non ho esaminato le procedure alla base di ciò, quindi non ho potuto darti una risposta soddisfacente. Puoi provare a decifrarlo dal codice sorgente di cpython se vuoi. Per quanto riguarda l'automazione del processo, non penso che ci sia un buon modo per farlo se non quello di renderlo una proprietà in primo luogo, o semplicemente aggiungere un commento / docstring per far sapere a coloro che leggono il tuo codice cosa stai facendo . Qualcosa del genere# declare size to not be a @property
Green Cloak Guy

4

Non è necessario alcun compito (a size). sizeè una proprietà nella classe base, quindi puoi sovrascrivere quella proprietà nella classe figlio:

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

    # size = property(lambda self: self.elements)


class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._cap = cap

    @property
    def size(self):
        return int(math.sqrt(self._cap))

    # size = property(lambda self: int(math.sqrt(self._cap)))

Puoi (micro) ottimizzarlo precompilando la radice quadrata:

class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._size = int(math.sqrt(self._cap))

    @property
    def size(self):
        return self._size

Questo è un ottimo punto, basta sostituire una proprietà padre con una proprietà figlio! +1
r.ook

Questo è in qualche modo la cosa semplice da fare, ma ero interessato e ho chiesto in modo specifico modi per consentire una sostituzione non di proprietà in modo da non imporre l'inconveniente di dover scrivere una proprietà di memorizzazione in cui il semplice incarico farebbe il lavoro.
Paul Panzer,

Non sono sicuro del motivo per cui vorresti complicare il tuo codice in questo modo. Per il piccolo prezzo di riconoscere che sizeè una proprietà e non un attributo di istanza, non devi fare nulla di speciale.
Chepner,

Il mio caso d'uso è costituito da molte classi secondarie con molti attributi; non dover scrivere 5 righe anziché una per tutte quelle giustifica una certa fantasia, IMO.
Paul Panzer,

2

Sembra che tu voglia definire sizenella tua classe:

class Square_Integers_Below(Math_Set_Base):
    size = None

    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

Un'altra opzione è quella di archiviare la captua classe e calcolarla con sizedefinita come proprietà (che sovrascrive la proprietà della classe base size).


2

Suggerisco di aggiungere un setter in questo modo:

class Math_Set_Base:
    @property
    def size(self):
        try:
            return self._size
        except:
            return len(self.elements)

    @size.setter
    def size(self, value):
        self._size = value

In questo modo è possibile ignorare la .sizeproprietà predefinita in questo modo:

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1,2,3).size) # 3
print(Square_Integers_Below(7).size) # 2

1

Inoltre puoi fare la prossima cosa

class Math_Set_Base:
    _size = None

    def _size_call(self):
       return len(self.elements)

    @property
    def size(self):
        return  self._size if self._size is not None else self._size_call()

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self._size = int(math.sqrt(cap))

È pulito, ma non è davvero necessario _sizeo _size_calllì. Potresti aver inserito la chiamata di funzione all'interno sizecome condizione e utilizzare try... except... per testare _sizeinvece di prendere un riferimento di classe extra che non verrà utilizzato. Indipendentemente da ciò, penso che questo sia ancora più criptico della semplice size = Nonesovrascrittura in primo luogo.
Guarda il

0

Penso che sia possibile impostare una proprietà di una classe base su un'altra proprietà di una classe derivata, all'interno della classe derivata e quindi utilizzare la proprietà della classe base con un nuovo valore.

Nel codice iniziale c'è una sorta di conflitto tra il nome sizedella classe base e l'attributo self.sizedella classe derivata. Questo può essere visibile se sostituiamo il nome self.sizedalla classe derivata con self.length. Questo produrrà:

3
<__main__.Square_Integers_Below object at 0x000001BCD56B6080>

Quindi, se sostituiamo il nome del metodo sizecon lengthin tutte le occorrenze nel programma, ciò comporterà la stessa eccezione:

Traceback (most recent call last):
  File "C:/Users/Maria/Downloads/so_1.2.py", line 24, in <module>
    Square_Integers_Below(7)
  File "C:/Users/Maria/Downloads/so_1.2.py", line 21, in __init__
    self.length = int(math.sqrt(cap))
AttributeError: can't set attribute

Il codice fisso, o comunque, una versione che funziona in qualche modo, è di mantenere il codice esattamente lo stesso, ad eccezione della classe Square_Integers_Below, che imposterà il metodo sizedalla classe di base, su un altro valore.

class Square_Integers_Below(Math_Set_Base):

    def __init__(self,cap):
        #Math_Set_Base.__init__(self)
        self.length = int(math.sqrt(cap))
        Math_Set_Base.size = self.length

    def __repr__(self):
        return str(self.size)

E poi, quando eseguiamo tutto il programma, l'output è:

3
2

Spero che questo sia stato utile in un modo o nell'altro.

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.