让Python类在创建新属性时抛出错误

3 投票
3 回答
1181 浏览
提问于 2025-04-18 15:30

假设这是我的类:

class A:
    def __init__(self):
        self.good_attr = None
        self.really_good_attr = None
        self.another_good_attr = None

然后调用者可以设置这些变量的值:

a = A()
a.good_attr = 'a value'
a.really_good_attr = 'better value'
a.another_good_attr = 'a good value'

但他们也可以添加新的属性:

a.goood_value = 'evil'

这对我的使用场景来说并不理想。我的对象是用来将多个值传递给一组方法的。(所以本质上,这个对象替代了几个方法中一长串共享参数,避免了重复,并清楚地区分了哪些是共享的,哪些是不同的。)如果调用者拼错了属性名称,那么这个属性就会被忽略,这样就会导致意想不到的、令人困惑的行为,可能还很难找出问题所在。最好是能够快速失败,通知调用者他们使用了一个会被忽略的属性名称。因此,我希望在他们使用一个对象中不存在的属性名称时,能有类似以下的行为:

>>> a.goood_value = 'evil'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: A instance has no attribute 'goood_value'

我该如何实现这个?

我还想说明的是,我完全知道调用者可以创建一个新类,随心所欲地做任何事情,完全绕过这个限制。不过,这种做法是不被支持的。创建我提供的对象只是为了快速检查拼写错误,节省时间,帮助那些使用我提供的对象的人(包括我自己),而不是让他们困惑,想知道为什么事情会出现意外的行为。

3 个回答

-1

把参数设为私有,可以在前面加两个下划线,比如写成 self.__good_attr。这样的话,外面的人就不能直接修改这个参数了。接着,写一个函数来设置这个 __good_attr 变量,如果传入的值不对,就让这个函数抛出一个异常,提醒出错了。

0

一个更常用的选择是使用命名元组。

Python 3.6及更高版本

在Python 3.6及以上版本中,你可以很简单地使用typing.NamedTuple来实现这个功能:

from typing import NamedTuple, Any

class A(NamedTuple):
    good_attr: Any = None
    really_good_attr: Any = None
    another_good_attr: Any = None

如果需要的话,可以使用更具体的类型限制,但必须包含注解,这样NamedTuple才能识别属性。

这样做不仅阻止了添加新属性,还阻止了设置已有属性:

>>> a = A()
>>> a.goood_value = 'evil'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'goood_value'
>>> a.good_attr = 'a value'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

这就要求你在创建的时候必须指定所有的值:

a = A(
    good_attr='a value',
    really_good_attr='better value',
    another_good_attr='a good value',
)

通常这样做没有问题,如果有问题,可以通过巧妙使用局部变量来解决。

Python 3.5及更低版本(包括2.x)

这些版本的Python要么没有typing模块,要么typing.NamedTuple无法像上面那样使用。在这些版本中,你可以使用collections.namedtuple来实现类似的效果。

定义这个类很简单:

from collections import namedtuple

A = namedtuple('A', ['good_attr', 'really_good_attr', 'another_good_attr'])

然后创建对象的方式和上面一样:

a = A(
    good_attr='a value',
    really_good_attr='better value',
    another_good_attr='a good value',
)

不过,这不允许在调用构造函数时省略某些值。你可以在创建对象时明确地包含None值:

a = A(
    good_attr='a value',
    really_good_attr=None,
    another_good_attr='a good value',
)

或者你可以使用几种技术中的一种来给参数一个默认值:

A.__new__.func_defaults = (None,) * 3
a = A(
    good_attr='a value',
    another_good_attr='a good value',
)
8

你可以通过 __setattr__ 方法来拦截属性的设置。这个方法会在你设置任何属性时被调用,所以要记住,它也会被用于你那些“正确”的属性:

class A(object):
    good_attr = None
    really_good_attr = None
    another_good_attr = None

    def __setattr__(self, name, value):
        if not hasattr(self, name):
            raise AttributeError(
                '{} instance has no attribute {!r}'.format(
                    type(self).__name__, name))
        super(A, self).__setattr__(name, value)

因为 good_attr 等属性是在类中定义的,所以当你使用 hasattr() 检查这些属性时,它会返回 True,并且不会抛出异常。你也可以在 __init__ 方法中设置这些属性,但要让 hasattr() 正常工作,这些属性必须在类中定义。

另一种方法是创建一个白名单,用来进行测试。

示例:

>>> a = A()
>>> a.good_attr = 'foo'
>>> a.bad_attr = 'foo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 10, in __setattr__
AttributeError: A instance has no attribute 'bad_attr'

当然,一个有决心的开发者仍然可以通过往 a.__dict__ 实例字典中添加键来给你的实例添加属性。

另一种选择是使用 __slots__ 的副作用;使用 slots 可以节省内存,因为字典比直接把值放到 Python 为每个实例创建的 C 结构中占用更多空间(这样就不需要键和动态表了)。这个副作用是,这种类的实例没有地方可以添加更多属性:

class A(object):
    __slots__ = ('good_attr', 'really_good_attr', 'another_good_attr')

    def __init__(self):
        self.good_attr = None
        self.really_good_attr = None
        self.another_good_attr = None

那么错误信息看起来会是:

>>> a = A()
>>> a.good_attr = 'foo'
>>> a.bad_attr = 'foo'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'bad_attr'

但请务必阅读文档中关于使用 __slots__ 的注意事项。

因为在使用 __slots__ 时没有 __dict__ 实例属性,这个选项实际上关闭了在实例上设置任意属性的可能性。

撰写回答