2015-09-24 37 views
7

Obecnie zastąpić klasę __setattr__() pod koniec klasy() metoda __init__ aby zapobiec nowe stworzenie atrybutu -Jak napisać metaclass, który uniemożliwiłby tworzenie nowych atrybutów po __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) 

Czy istnieje sposób, aby uniknąć przejęcia ręcznej __setattr__(), a to automatycznie przy pomocy metaclasses?

Najbliższy przyjechałem było -

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 

Czy istnieje bardziej ogólny sposób robi to w taki sposób, APRIORI znajomość podklasy atrybutów nie jest wymagana?
Zasadniczo, jak uniknąć linii dctry.update({'x': 0, 'y': 0}) i sprawić, by działało to niezależnie od nazw atrybutów klas?

P.S. - FWIW Dokonałem już oceny opcji __slots__ i nazwano cztery i nie znalazłem ich dla moich potrzeb. Proszę nie skupiać się na uproszczonym przykładzie Points(), który wykorzystałem do zilustrowania pytania; rzeczywisty przypadek użycia dotyczy znacznie bardziej złożonej klasy.

+3

Dlaczego? W jakich okolicznościach ktoś mógłby dodać atrybut do twojej klasy? Jako autor klasy, jak to wpłynie na Ciebie? Zgodnie z maksymą "wszyscy jesteśmy zgodnymi dorosłymi" w kraju Python; w tym przypadku oznacza to, że jeśli naruszę API klasy poprzez dodanie atrybutu, odpowiadam za wszelkie konsekwencje. Python nie jest Java ani C++. – msw

+1

@msw - Głównie w celu wychwycenia błędów dotyczących literówek JAK NAJSZYBCIEJ. Gdybym mógł uniknąć przypadkowego wpisania przez programistów _obj.staet_, a następnie zastanawiałem się, dlaczego maszyna stanowa działa niezręcznie, to ja bym to zrobił. Zasadniczo chciałbym skrócić czas pomiędzy literami a ostateczną realizacją. –

+0

W Pythonie naprawdę nie można używać języka do wymuszania zachowania. To jeden z powodów, dla których testy i recenzje kodu są jeszcze bardziej krytyczne w Pythonie. Powiedzmy, że złapałeś przypadek "obj.staet", który opisujesz, a co powiesz na 'ojb.state' i kiedy przestajesz. Ludzie mogą (i będą) pisać złamany kod w dowolnym języku i trudno jest je chronić przed samym sobą. Wreszcie, jesteś chroniony przed połowę problemów 'staet' już: odniesienie do obiektu' jeśli obj.staet == 5: 'rzuci NameError; przypisanie "obj.staet = 6" nie zostanie przechwycone przez Pythona. – msw

Odpowiedz

6

Czy to ma sens w twoim przypadku?

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

Prawdopodobnie trzeba pobawić się trochę bardziej z nim, aby uzyskać bardziej elegancki, ale ogólna idea jest przesłonić __setattr__ dopiero po startowych nazywa.

Mimo, że wspólne podejście do tego jest po prostu użyć object.__setattr__(self, field, value) wewnętrznie ominąć 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

Dzięki za odpowiedź; to jest (prawie) dokładnie to, czego szukałem - hak post __init __(). –

+0

Drugie podejście nie byłoby dla mnie skuteczne, ponieważ wymagane jest delegowanie konkretnego rozwoju klasy innym, których znajomość Pythona jest gorsza niż moja. Po prostu staram się zapewnić bezpieczną piaskownicę. –

+0

@ work.bin W tym przypadku zwykle używam metody pomocnika, która streszcza użycie 'obiektu .__ setattr__'' – ArnauOrriols

10

Nie wymyślaj ponownie koła.

Dwa proste sposoby osiągnięcia że (nie bezpośrednio za pomocą metaklasą) stosuje:

  1. namedtuple s
  2. __slots__

Na przykład, stosując namedtuple (w oparciu o przykład w docs):

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

Na przykład, używając __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__' jest przeznaczony do optymalizacji przestrzeni, a nie do arbitralnego ograniczania możliwych atrybutów instancji. Nakłada także sporo innych ograniczeń, https://docs.python.org/2/reference/datamodel.html#slots. Teraz, gdy nie ma to sensu - punkt jest doskonałym przykładem użycia slotów, ponieważ ma kilka atrybutów, zwykle kilka przykładów, a ponieważ większość obiektów "danych" dodających atrybuty nie ma większego sensu ... –

+0

FYI, dwa są zasadniczo równoważne. 'namedtuple' dynamicznie tworzy nową klasę, która używa' __slots__' w celu ograniczenia dozwolonych atrybutów do nazw w drugim argumencie. – chepner

+4

@ shx2 jak już wspomniałem, sensowne jest używanie slotów dla prostego obiektu "data" - chciałem podkreślić, że automaty nie są zamierzone jako sposób na ograniczenie dynamiki, ale jako kompromis w zakresie optymalizacji kosmosu. Bezużyteczne arbitralne ograniczenia są wysoce niepytotoniczne. –

6

Nie trzeba metaclasses do rozwiązywania tego rodzaju problemu.

Jeśli chcesz utworzyć dane raz z góry, a następnie mieć je niezmienne, zdecydowanie użyłbym namedtuple, jak sugeruje shx2.

W przeciwnym razie po prostu zdefiniuj zbiór dozwolonych pól na klasie i sprawdź, czy nazwa, którą próbujesz ustawić, znajduje się w zbiorze dozwolonych pól. Nie musisz zmieniać implementacji __setattr__ w części przez __init__ - będzie działać podczas __init__ tak samo jak będzie działać później. Użyj tuple lub frozenset jako struktury danych dla dozwolonych pól, jeśli chcesz zniechęcić do mutowania/zmieniania ich na danej klasie.

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 

Można to łatwo zbitek do podstawowej klasy do ponownego użycia:

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 

nie sądzę by to uznać pythonic aby zablokować dozwolonych atrybutów obiekt w ten sposób i spowoduje pewne komplikacje dla podklas, które wymagają dodatkowych atrybutów (podklasa będzie musiała zapewnić, że pole _allowed_attrs ma zawartość odpowiednią dla niego).

1

mam ten sam potrzebę (dla szybkiego rozwoju Hack API). Nie używam metaclasses za to właśnie dziedzictwo:

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 

Następnie:

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

Jeśli muszę podklasy bazę i dodać dodatkowe atrybuty używam odblokowania:

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

Jeśli Baza jest abstrakcyjna, można pominąć jej blokowanie i po prostu zablokować dziecko. Mam kilka unittests, które sprawdzają, że każda klasa publiczna jest zablokowana po init, aby mnie złapać, jeśli zapomnę blokady.

+0

Dzięki za odpowiedź. Już to wykorzystałem (od momentu podniesienia tego pytania). Jednak szukałem czegoś podobnego do odpowiedzi udzielonej przez @ArnauOrriols. –