如何编写元类以防止在 __init__() 之后创建新属性?

How to write metaclass which would prevent creating new attributes after __init__()?

目前我在 class' __init__() 方法的末尾重写 class' __setattr__() 以防止创建新属性 -

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__() 并在 metaclasses 的帮助下自动执行此操作?

我最接近的是-

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

是否有更通用的方法可以不需要子class 属性的先验知识?
基本上,如何避免行 dctry.update({'x': 0, 'y': 0}) 并使这项工作不管 class 属性的名称是什么?

P.S。 - FWIW 我已经评估了 __slots__ 和 namedtuple 选项,发现它们不符合我的需要。请不要将注意力集中在我用来说明问题的精简 Points() 示例上;实际用例涉及复杂得多 class.

不要重新发明轮子。

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

  1. namedtuples
  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

你不需要元classes 来解决这类问题。

如果您想预先创建一次数据然后使其不可变,我肯定会按照 shx2 的建议使用 namedtuple

否则,只需在 class 上定义一组允许的字段,然后让 __setattr__ 检查您尝试设置的名称是否在允许的字段集合中。您不需要在 __init__ 中途更改 __setattr__ 的实现——它在 __init__ 期间的工作方式与以后的工作方式相同。如果您想在给定的 class.

上阻止 mutating/changing 它们,请使用 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 以供重复使用:

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 以这种方式锁定对象的允许属性,这会给 sub[=33= 带来一些复杂性]es 需要额外的属性(subclass 必须确保 _allowed_attrs 字段具有适合它的内容)。

这对你的情况有意义吗?

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

您可能需要 fiddle 多一点,让它更优雅,但一般的想法是仅在 init[=21= 之后重写 __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

我也有同样的需求(为了快速开发 API)。我不为此使用 metaclasses,只是继承:

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()

如果我需要子class 基础并添加额外的属性我使用解锁:

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

如果 Base 是抽象的,您可以跳过它的锁定而只锁定子级。 然后我进行了一些单元测试,检查每个 public class 在初始化后是否被锁定,如果我忘记锁定就可以抓住我。