2015-09-24 5 views
7

Attualmente sovrascrivo classe __setattr__() verso la fine della classe __init__() per evitare che nuova creazione attributo -Come scrivere metaclassi che impedirebbe di creare nuovi attributi dopo __init __()?

class Point(object): 
    def __init__(self): 
     self.x = 0 
     self.y = 0 
     Point.__setattr__ = self._setattr 

    def _setattr(self, name, value): 
     if not hasattr(self, name): 
      raise AttributeError("'" + name + "' not an attribute of Point object.") 
     else: 
      super(Point, self).__setattr__(name, value) 

C'è un modo per evitare manualmente l'override __setattr__() e farlo automaticamente con l'aiuto di metaclassi?

Il più vicino mi è venuto è stato -

class attr_block_meta(type): 
    def __new__(meta, cname, bases, dctry): 
     def _setattr(self, name, value): 
      if not hasattr(self, name): 
       raise AttributeError("'" + name + "' not an attribute of " + cname + " object.") 
      object.__setattr__(self, name, value) 

     dctry.update({'x': 0, 'y': 0}) 
     cls = type.__new__(meta, cname, bases, dctry) 
     cls.__setattr__ = _setattr 
     return cls 

class ImPoint(object): 
    __metaclass__ = attr_block_meta 

Esiste un modo più generico di fare questo in modo tale che a priori la conoscenza della sottoclasse attributi non è necessario?
In pratica, come evitare la riga dctry.update({'x': 0, 'y': 0}) e farlo funzionare indipendentemente da quali siano i nomi degli attributi di classe?

P.S. - FWIW Ho già valutato le opzioni __slots__ e namedtuple e le ho trovate carenti per le mie esigenze. Per favore non restringere la tua attenzione all'esempio Punti() ridotto che ho usato per illustrare la domanda; il caso d'uso reale coinvolge una classe molto più complessa.

+3

Perché? In quali circostanze qualcuno aggiungerebbe un attributo alla tua classe? Come autore di classe, come ti influenzerebbe? Come dice la massima, "siamo tutti adulti consenzienti" nella terra di Python; in questo caso significa che se violento la classe API aggiungendo un attributo, sono responsabile per eventuali conseguenze. Python non è Java né C++. – msw

+1

@msw - Principalmente per individuare gli errori che riguardano gli errori di battitura APPENA POSSIBILE. Se potessi evitare che i programmatori digitassero accidentalmente _obj.staet_ e poi si chiedessero perché la macchina a stati funzionasse in modo irrequieto, allora lo farei. Fondamentalmente, vorrei ridurre il tempo che intercorre tra l'errore di battitura e la realizzazione finale. –

+0

In Python non si può davvero usare la lingua per forzare il comportamento. Questo è uno dei motivi per cui test e recensioni di codice sono ancora più critici in Python. Diciamo che prendi il caso 'obj.staet' che descrivi, che ne dici di' ojb.state' e quando ti fermi. Le persone possono (e vogliono) scrivere codice spezzato in qualsiasi lingua ed è difficile proteggerle da se stesse. Infine, sei già protetto dalla metà dei problemi di "staet': fare riferimento a un oggetto' if obj.staet == 5: 'genererà un NameError; assignment 'obj.staet = 6' non verrà catturato da Python. – msw

risposta

6

Questo avrebbe senso per il tuo caso?

from functools import wraps 

class attr_block_meta(type): 
    def __new__(meta, cname, bases, dctry): 
     def _setattr(self, name, value): 
      if not hasattr(self, name): 
       raise AttributeError("'" + name + "' not an attibute of " + cname + " object.") 
      object.__setattr__(self, name, value) 

     def override_setattr_after(fn): 
      @wraps(fn) 
      def _wrapper(*args, **kwargs): 
       cls.__setattr__ = object.__setattr__ 
       fn(*args, **kwargs) 
       cls.__setattr__ = _setattr 
      return _wrapper 

     cls = type.__new__(meta, cname, bases, dctry) 
     cls.__init__ = override_setattr_after(cls.__init__) 
     return cls 


class ImPoint(object): 
    __metaclass__ = attr_block_meta 
    def __init__(self, q, z): 
     self.q = q 
     self.z = z 

point = ImPoint(1, 2) 
print point.q, point.z 
point.w = 3 # Raises AttributeError 

See this for more details on 'wraps'.

Probabilmente bisogno di smanettare un po 'di più con esso per ottenere più elegante, ma l'idea generale è quella di ignorare __setattr__ solo dopo init si chiama.

Detto questo, un approccio comune per questo è solo per usare object.__setattr__(self, field, value) internamente per bypassare l'AttributeError:

class attr_block_meta(type): 
    def __new__(meta, cname, bases, dctry): 
     def _setattr(self, name, value): 
      if not hasattr(self, name): 
       raise AttributeError("'" + name + "' not an attibute of " + cname + " object.") 
      object.__setattr__(self, name, value) 

     cls = type.__new__(meta, cname, bases, dctry) 
     cls.__setattr__ = _setattr 
     return cls 


class ImPoint(object): 
    __metaclass__ = attr_block_meta 
    def __init__(self, q, z): 
     object.__setattr__(self, 'q', q) 
     object.__setattr__(self, 'z', z) 

point = ImPoint(1, 2) 
print point.q, point.z 
point.w = 3 # Raises AttributeError 
+1

Grazie per la risposta; questo è (quasi) esattamente ciò che stavo cercando - un hook post __init __(). –

+0

Il secondo approccio non funzionerebbe per me dal momento che l'esigenza è quella di delegare uno sviluppo di classe concreto ad altri la cui conoscenza di Python è più debole della mia. Sto solo cercando di fornire una sandbox sicura. –

+0

@ work.bin In questo caso, quello che di solito faccio è fornire un metodo di supporto che astrae l'uso di '' object .__ setattr__'' – ArnauOrriols

10

Non reinventare la ruota.

Due modi semplici per raggiungere tale (non direttamente utilizzando un metaclasse) utilizzano:

  1. namedtuple s
  2. __slots__

Ad esempio, utilizzando namedtuple (basato sul esempio nel documenti):

Point = namedtuple('Point', ['x', 'y']) 
p = Point(11, 22) 
p.z = 33 # ERROR 

Ad esempio, utilizzando __slots__:

class Point(object): 
    __slots__ = ['x', 'y'] 
    def __init__(self, x=0, y=0): 
     self.x = x 
     self.y = y 

p = Point(11,22) 
p.z = 33 # ERROR 
+5

FWIW, '__slots__' è inteso per l'ottimizzazione dello spazio, non per la restrizione arbitraria dei possibili attributi di un'istanza. Implica anche alcune altre restrizioni, cf https://docs.python.org/2/reference/datamodel.html#slots. Ora che qui non avrebbe senso - un punto è un caso d'uso perfetto per gli slot poiché ha pochi attributi, di solito un bel po 'di esempi, e come per lo più un oggetto "dati" che aggiunge attributi non avrebbe molto senso ... –

+0

FYI, i due sono essenzialmente equivalenti. 'namedtuple' crea dinamicamente una nuova classe che usa' __slots__' per limitare gli attributi permessi ai nomi nel secondo argomento. – chepner

+4

@ shx2, come ho detto, ha senso utilizzare gli slot per un semplice oggetto "dati". Quello che volevo sottolineare è che gli slot non sono intesi come un modo per limitare il dinamismo, ma come uno scambio di ottimizzazione dello spazio. Le restrizioni arbitrarie inutili sono altamente nonpontiche. –

6

Non è necessario metaclassi per risolvere questo tipo di problema.

Se si desidera creare i dati una volta in primo piano e quindi renderli immutabili, utilizzerei sicuramente uno namedtuple come suggerito da shx2.

Altrimenti, è sufficiente definire una raccolta di campi consentiti nella classe e controllare __setattr__ per verificare se il nome che si sta tentando di impostare si trova nella raccolta dei campi consentiti. Non è necessario modificare l'implementazione di __setattr__ in modo parziale tramite __init__ - funzionerà durante lo __init__, proprio come funzionerà in seguito. Utilizzare uno tuple o uno frozenset come struttura dati per i campi consentiti, se si desidera scoraggiare il muting/cambiarli su una determinata classe.

class Point(object): 
    _allowed_attrs = ("x", "y") 

    def __init__(self, x, y): 
     self.x = x 
     self.y = y 

    def __setattr__(self, name, value): 
     if name not in self._allowed_attrs: 
      raise AttributeError(
       "Cannot set attribute {!r} on type {}".format(
        name, self.__class__.__name__)) 
     super(Point, self).__setattr__(name, value) 

p = Point(5, 10) 
p.x = 9 
p.y = "some string" 
p.z = 11 # raises AttributeError 

Questo può essere facilmente fattorizzato in una classe base per il riutilizzo:

class RestrictedAttributesObject(object): 
    _allowed_attrs =() 

    def __setattr__(self, name, value): 
     if name not in self._allowed_attrs: 
      raise AttributeError(
       "Cannot set attribute {!r} on type {}".format(
        name, self.__class__.__name__)) 
     super(RestrictedAttributesObject, self).__setattr__(name, value) 

class Point(RestrictedAttributesObject): 
    _allowed_attrs = ("x", "y") 

    def __init__(self, x, y): 
     self.x = x 
     self.y = y 

non credo sarebbe considerato divinatorio per bloccare gli attributi permessi di un oggetto in questo modo, e causerà qualche complicazione per sottoclassi che necessitano di attributi aggiuntivi (una sottoclasse dovrà assicurarsi che il campo _allowed_attrs abbia contenuti appropriati per esso).

1

Ho questo stesso bisogno (per uno sviluppo rapido API-hack). Io non uso metaclassi per questo, solo l'eredità:

class LockedObject(object): 
    def __setattr__(self, name, value): 
     if name == "_locked": 
      object.__setattr__(self, name, value) 
      return 

     if hasattr(self, "_locked"): 
      if not self._locked or hasattr(self, name): 
       object.__setattr__(self, name, value) 
      else: 
       raise NameError("Not allowed to create new attribute {} in locked object".format(name)) 
     else: # never called _lock(), so go on 
      object.__setattr__(self, name, value) 

    def _lock(self): 
     self._locked = True 

    def _unlock(self): 
     self._locked = False 

Poi:

class Base(LockedObject): 
    def __init__(self): 
     self.a = 0 
     self.b = 1 
     self._lock() 

Se ho bisogno di sottoclasse di base e aggiungere attributi extra Io uso di sblocco:

class Child(Base): 
    def __init__(self): 
     Base.__init__(self) 
     self._unlock() 
     self.c = 2 
     self._lock() 

Se Base è astratta puoi saltare il suo blocco e bloccare solo i bambini. Ho quindi alcune unittests che controllano che ogni classe pubblica sia bloccata dopo init a prendermi se dimentico il locking.

+0

Grazie per la risposta. Avevo già fatto ricorso a questo (da quando ho sollevato questa domanda). Tuttavia stavo cercando qualcosa di simile alla risposta fornita da @ArnauOrriols. –

Problemi correlati