如何强制/确保类属性为特定类型?

85 投票
8 回答
183448 浏览
提问于 2025-04-17 13:06

我想知道如何在Python中限制一个类的成员变量只能是特定类型。


详细说说:

我有一个类,这个类里面有几个成员变量,它们的值是从外部设置的。因为这些变量的使用方式,我需要它们是特定的类型,要么是整数(int),要么是列表(list)。

如果这是C++的话,我可以把这些变量设为私有的,然后在设置值的时候检查类型。但在Python里,这样做不太可能。那么,有没有办法限制这些变量的类型,让它们在运行时如果被赋值为错误的类型就报错?还是说我需要在每个使用这些变量的函数里都检查它们的类型?

8 个回答

29

一般来说,这样做并不是个好主意,正如@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__。对于更多的属性,使用上面展示的只设置描述符。直接使用属性来处理这种情况会引入更多问题,而不是解决问题。

40

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

你可以像其他回答那样使用属性。如果你想限制一个属性,比如说“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'>
>>> 

    

撰写回答