如何强制/确保类属性为特定类型?
我想知道如何在Python中限制一个类的成员变量只能是特定类型。
详细说说:
我有一个类,这个类里面有几个成员变量,它们的值是从外部设置的。因为这些变量的使用方式,我需要它们是特定的类型,要么是整数(int),要么是列表(list)。
如果这是C++的话,我可以把这些变量设为私有的,然后在设置值的时候检查类型。但在Python里,这样做不太可能。那么,有没有办法限制这些变量的类型,让它们在运行时如果被赋值为错误的类型就报错?还是说我需要在每个使用这些变量的函数里都检查它们的类型?
8 个回答
一般来说,这样做并不是个好主意,正如@yak在评论中提到的原因。你基本上是在阻止用户提供有效的参数,这些参数虽然有正确的属性或行为,但却不在你硬编码的继承树中。
撇开免责声明不谈,你可以尝试几种方法来实现你想要的功能。主要的问题是,Python中没有私有属性。所以如果你有一个普通的对象引用,比如self._a
,你无法保证用户不会直接修改它,即使你提供了一个进行类型检查的设置方法。下面的选项展示了如何真正执行类型检查。
重写 __setattr__ 方法
这个方法只适合少数几个属性使用。__setattr__
方法是在你用点符号给普通属性赋值时被调用的。例如,
class A:
def __init__(self, a0):
self.a = a0
如果我们现在执行A().a = 32
,它会调用A().__setattr__('a', 32)
,这在后台是这样工作的。实际上,在__init__
中使用self.a = a0
时也会使用self.__setattr__
。你可以利用这个来强制进行类型检查:
class A:
def __init__(self, a0):
self.a = a0
def __setattr__(self, name, value):
if name == 'a' and not isinstance(value, int):
raise TypeError('A.a must be an int')
super().__setattr__(name, value)
这种方法的缺点是,你需要为每种要检查的类型写一个单独的if name == ...
(或者if name in ...
来检查多个名称)。优点是这是让用户几乎不可能绕过类型检查的最直接的方法。
创建一个属性
属性是用来替代你普通属性的对象,通常是通过使用装饰器来实现。描述符可以有__get__
和__set__
方法,用于自定义如何访问底层属性。这有点像把__setattr__
中的相应if
分支放到一个只针对那个属性运行的方法中。这里有个例子:
class A:
def __init__(self, a0):
self.a = a0
@property
def a(self):
return self._a
@a.setter
def a(self, value):
if not isinstance(value, int):
raise TypeError('A.a must be an int')
self._a = value
还有一种稍微不同的做法可以在@jsbueno的回答中找到。
虽然这样使用属性很方便,并且大部分解决了问题,但也存在一些问题。第一个问题是你有一个“私有”的_a
属性,用户可以直接修改它,从而绕过你的类型检查。这几乎和使用普通的getter和setter是一样的问题,只不过现在a
作为“正确”的属性可以在后台重定向到setter,这样用户就不太可能去修改_a
了。第二个问题是你需要一个多余的getter来使属性能够读写。这些问题在这个问题中有讨论。
创建一个真正的只设置描述符
这个解决方案可能是最稳健的。它在上面提到的问题的接受答案中被建议。基本上,不使用属性,因为属性有很多你无法去掉的附加功能,而是创建你自己的描述符(和装饰器),用于任何需要类型检查的属性:
class SetterProperty:
def __init__(self, func, doc=None):
self.func = func
self.__doc__ = doc if doc is not None else func.__doc__
def __set__(self, obj, value):
return self.func(obj, value)
class A:
def __init__(self, a0):
self.a = a0
@SetterProperty
def a(self, value):
if not isinstance(value, int):
raise TypeError('A.a must be an int')
self.__dict__['a'] = value
设置方法直接将实际值存储到实例的__dict__
中,以避免无限递归。这使得可以在不提供显式getter的情况下获取属性的值。由于描述符a
没有__get__
方法,搜索会继续,直到找到__dict__
中的属性。这确保了所有的设置都通过描述符/设置方法,而获取则允许直接访问属性值。
如果你有很多属性需要这样的检查,可以把self.__dict__['a'] = value
这一行放到描述符的__set__
方法中:
class ValidatedSetterProperty:
def __init__(self, func, name=None, doc=None):
self.func = func
self.__name__ = name if name is not None else func.__name__
self.__doc__ = doc if doc is not None else func.__doc__
def __set__(self, obj, value):
ret = self.func(obj, value)
obj.__dict__[self.__name__] = value
class A:
def __init__(self, a0):
self.a = a0
@ValidatedSetterProperty
def a(self, value):
if not isinstance(value, int):
raise TypeError('A.a must be an int')
更新
Python3.6几乎可以直接为你做到这一点:https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements
总结
对于需要类型检查的少数几个属性,直接重写__setattr__
。对于更多的属性,使用上面展示的只设置描述符。直接使用属性来处理这种情况会引入更多问题,而不是解决问题。
从Python 3.5开始,你可以使用类型提示来表明一个类的属性应该是什么类型。这样的话,你可以在持续集成的过程中加入像MyPy这样的工具,来检查所有的类型约定是否被遵守。
举个例子,下面这个Python脚本:
class Foo:
x: int
y: int
foo = Foo()
foo.x = "hello"
MyPy会给出以下错误:
6: error: Incompatible types in assignment (expression has type "str", variable has type "int")
如果你想在程序运行时强制检查类型,可以使用enforce这个包。来自它的自述文件:
>>> import enforce
>>>
>>> @enforce.runtime_validation
... def foo(text: str) -> None:
... print(text)
>>>
>>> foo('Hello World')
Hello World
>>>
>>> foo(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/william/.local/lib/python3.5/site-packages/enforce/decorators.py", line 106, in universal
_args, _kwargs = enforcer.validate_inputs(parameters)
File "/home/william/.local/lib/python3.5/site-packages/enforce/enforcers.py", line 69, in validate_inputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'text' was not of type <class 'str'>. Actual type was <class 'int'>.
你可以像其他回答那样使用属性。如果你想限制一个属性,比如说“bar”,并且把它限制为整数,你可以写这样的代码:
class Foo(object):
def _get_bar(self):
return self.__bar
def _set_bar(self, value):
if not isinstance(value, int):
raise TypeError("bar must be set to an integer")
self.__bar = value
bar = property(_get_bar, _set_bar)
这样就可以了:
>>> f = Foo()
>>> f.bar = 3
>>> f.bar
3
>>> f.bar = "three"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in _set_bar
TypeError: bar must be set to an integer
>>>
(还有一种新的写属性的方法,使用“property”这个内置功能作为获取方法的装饰器,但我更喜欢上面提到的旧方法)。
当然,如果你的类里面有很多属性,并且想用这种方式来保护它们,那代码可能会变得很冗长。不过不用担心,Python有很强的自省能力,可以创建一个类装饰器,帮助你用更少的代码来实现这个功能。
def getter_setter_gen(name, type_):
def getter(self):
return getattr(self, "__" + name)
def setter(self, value):
if not isinstance(value, type_):
raise TypeError(f"{name} attribute must be set to an instance of {type_}")
setattr(self, "__" + name, value)
return property(getter, setter)
def auto_attr_check(cls):
new_dct = {}
for key, value in cls.__dict__.items():
if isinstance(value, type):
value = getter_setter_gen(key, value)
new_dct[key] = value
# Creates a new class, using the modified dictionary as the class dict:
return type(cls)(cls.__name__, cls.__bases__, new_dct)
你只需要把auto_attr_check
作为类装饰器使用,然后在类的主体中声明你想要的属性,并把它们设置为需要限制的类型:
...
... @auto_attr_check
... class Foo(object):
... bar = int
... baz = str
... bam = float
...
>>> f = Foo()
>>> f.bar = 5; f.baz = "hello"; f.bam = 5.0
>>> f.bar = "hello"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in setter
TypeError: bar attribute must be set to an instance of <type 'int'>
>>> f.baz = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in setter
TypeError: baz attribute must be set to an instance of <type 'str'>
>>> f.bam = 3 + 2j
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in setter
TypeError: bam attribute must be set to an instance of <type 'float'>
>>>