如何编写在__init __()之后阻止创建新属性的元类?

时间:2015-09-24 12:01:41

标签: python python-2.7 metaclass

目前我覆盖了班级' __setattr__()在课程结束时#39;阻止新属性创建的__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选项,发现它们缺乏我的需求。请不要将注意力集中在我用来说明问题的削减点()示例上;实际的用例涉及更复杂的类。

4 个答案:

答案 0 :(得分:10)

不要重新发明轮子。

实现这一目标的两种简单方法(不直接使用元类)使用:

  1. namedtupleŠ
  2. __slots__
  3. 例如,使用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
    

答案 1 :(得分:6)

您不需要元类来解决此类问题。

如果你想预先创建数据然后让它变成不可变的话,我肯定会使用namedtuple作为shx2建议。

否则,只需在类上定义一组允许的字段,然后__setattr__检查您尝试设置的名称是否在允许的字段集合中。您无需在__setattr__中部分地更改__init__的实施方式 - 它将在__init__期间工作,这与以后的工作方式相同。如果您不想阻止在给定类上更改/更改它们,请使用tuplefrozenset作为允许字段的数据结构。

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

我不认为以这种方式锁定对象的允许属性会被视为 pythonic ,并且它会导致需要其他属性的子类的一些复杂化(子类将具有确保_allowed_attrs字段具有适合它的内容。)

答案 2 :(得分: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
  

有关“换行”的更多详情,请参阅this

您可能需要更多地使用它来使其更优雅,但一般的想法是仅在调用 init 之后覆盖__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

答案 3 :(得分: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并添加额外的属性,我使用unlock:

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

如果Base是抽象的,您可以跳过锁定并锁定孩子。 我有一些单元测试,检查每个公共类在init之后被锁定,以便在忘记锁定时抓住我。