2015-09-24 4 views
7

В настоящее время я переопределять класс __setattr__() в конце класса метод __init__(), чтобы предотвратить новое создание атрибута -Как написать метакласс, который предотвратит создание новых атрибутов после __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) 

Есть ли способ, чтобы избежать вручную перекрывая __setattr__() и сделать это автоматически с помощью метаклассов?

Ближайший я пришел был -

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 

Есть более общий способ сделать это, например, что априорное знание подкласса атрибутов не требуется?
В принципе, как избежать строки dctry.update({'x': 0, 'y': 0}) и сделать эту работу независимо от того, что такое имена атрибутов класса?

P.S. - FWIW Я уже оценил параметры __slots__ и namedtuple и нашел их в недостатке для моих нужд. Пожалуйста, не сузите свой фокус к рассмотренным пунктам(), которые я использовал для иллюстрации вопроса; фактический прецедент предполагает гораздо более сложный класс.

+3

Почему? При каких обстоятельствах кто-то добавит атрибут вашему классу? Как автор класса, как это повлияет на вас? Как гласит максим, «мы все соглашаемся с взрослыми» на земле Питона; в этом случае это означает, что если я нарушаю API класса, добавив атрибут, я несу ответственность за любые последствия. Python - это не Java и C++. – msw

+1

@msw - В основном, чтобы поймать ошибки, связанные с опечатками, как можно скорее. Если бы я мог избегать программирования, случайно набрав _obj.staet_, а затем задаваясь вопросом, почему машина штата работает не так, я бы это сделал. В принципе, я хотел бы сократить время, которое идет между опечатками к возможной реализации. –

+0

В Python вы действительно не можете использовать язык для принуждения поведения. Это одна из причин того, что тесты и обзоры кода еще более критичны в Python. Итак, скажем, вы поймаете случай «obj.staet», который вы описываете, как насчет 'ojb.state' и когда вы остановитесь. Люди могут (и будут) писать сломанный код на любом языке, и трудно защитить их от самих себя. Наконец, вы уже защищены от половины проблем 'staet': ссылаясь на объект' if obj.staet == 5: 'будет генерировать NameError; Назначение 'obj.staet = 6' не будет уловлено Python. – msw

ответ

6

Будет ли это иметь смысл для вашего дела?

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

Вы, вероятно, нужно доделать немного больше с ним, чтобы получить его более изящным, но общая идея состоит в том, чтобы переопределить __setattr__ только после инициализации называется.

Сказав, что общий подход к этому просто использовать object.__setattr__(self, field, value) внутренне обойти 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

Спасибо за ответ; это (почти) именно то, что я искал - сообщение __init __() hook. –

+0

Второй подход не сработает для меня, поскольку требование состоит в том, чтобы делегировать конкретное развитие класса другим, чье знание Python менее слабо, чем мое. Я просто пытаюсь предоставить безопасную песочницу. –

+0

@ work.bin В этом случае я обычно предоставляю вспомогательный метод, который абстрагирует использование '' object .__ setattr__'' – ArnauOrriols

10

Не изобретайте велосипед.

Два простых способов достижения этой цели (без непосредственного использования метакласса) используют:

  1. namedtuple с
  2. __slots__

Например, при использовании namedtuple (на основании, например, в документы):

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

, например, с помощью __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__' предназначен для оптимизации пространства, а не для произвольного ограничения возможных атрибутов экземпляра. Он также налагает немало других ограничений, cf https://docs.python.org/2/reference/datamodel.html#slots. Теперь, когда это не имеет смысла, точка - идеальный вариант использования слотов, поскольку он имеет несколько атрибутов, как правило, довольно много экземпляров, и поскольку в основном атрибут добавления данных «данных» не имеет большого смысла ... –

+0

FYI, эти два по существу эквивалентны. 'namedtuple' динамически создает новый класс, который использует' __slots__', чтобы ограничить допустимые атрибуты именами во втором аргументе. – chepner

+4

@ shx2, как я уже упоминал, имеет смысл использовать слоты для простого объекта «данных» - я хотел бы указать, что слоты _not_ предназначены для ограничения динамизма, а как компромисс оптимизации пространства. Бесполезные произвольные ограничения весьма непримиримы. –

6

Вам не нужно метаклассами решать такого рода проблемы.

Если вы хотите создать данные раньше, а затем, чтобы они были неизменными, я определенно использовал бы namedtuple, как предлагает shx2.

В противном случае просто определите набор допустимых полей в классе и отметьте __setattr__, чтобы узнать, находится ли имя, которое вы пытаетесь установить, в коллекции разрешенных полей. Вам не нужно изменять реализацию __setattr__ частичным способом через __init__ - он будет работать во время __init__, точно так же, как он будет работать позже. Используйте tuple или frozenset как структуру данных для разрешенных полей, если вы хотите отказать в изменении или изменении их в данном классе.

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 

Это может быть легко вынесен в базовый класс для повторного использования:

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 

Я не думаю, что это будет считаться вещим заблокировать допустимые атрибуты объект таким образом, и это вызовет некоторое усложнение для подклассов, которым нужны дополнительные атрибуты (подкласс должен гарантировать, что поле _allowed_attrs имеет соответствующее ему содержимое).

1

У меня есть эта же потребность (для развития быстрой хак API). Я не использую метаклассами для этого, только наследование:

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 

Тогда:

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

Если мне нужно создать подкласс Base и добавить дополнительные атрибуты я использую разблокировать:

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

Если База абстрактная, вы можете пропустить ее блокировку и просто заблокировать дочерние элементы. У меня есть некоторые unittests, которые проверяют, что каждый открытый класс заблокирован после init, чтобы поймать меня, если я забуду замок.

+0

Спасибо за ответ. Я уже прибегал к этому (начиная с поднятия этого вопроса). Однако я искал что-то похожее на ответ, предоставленный @ArnauOrriols. –

 Смежные вопросы

  • Нет связанных вопросов^_^