让Python类在创建新属性时抛出错误
假设这是我的类:
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 个回答
把参数设为私有,可以在前面加两个下划线,比如写成 self.__good_attr。这样的话,外面的人就不能直接修改这个参数了。接着,写一个函数来设置这个 __good_attr 变量,如果传入的值不对,就让这个函数抛出一个异常,提醒出错了。
一个更常用的选择是使用命名元组。
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',
)
你可以通过 __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__
实例属性,这个选项实际上关闭了在实例上设置任意属性的可能性。