强制/确保python类属性具有特定类型

时间:2012-02-16 04:54:10

标签: python class variables

如何将类成员变量限制为Python中的特定类型?


更长的版本:

我有一个类,它有几个在类外部设置的成员变量。由于它们的使用方式,它们必须是特定类型,int或list。如果这是C ++,我会简单地将它们设为私有,并在' set'中进行类型检查。功能。鉴于这是不可能的,有没有办法限制变量的类型,以便在运行时发生错误/异常,如果他们被赋予了错误类型的值?或者我是否需要在使用它们的每个函数中检查它们的类型?

感谢。

8 个答案:

答案 0 :(得分:38)

您可以像使用其他答案一样使用属性 - 所以,如果你想约束单个属性,说“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
>>> 

(还有一种新的编写属性的方法,使用内置的“属性”作为getter方法的装饰器 - 但我更喜欢旧的方式,就像我把它放在上面一样。)

当然,如果您的类上有很多属性,并希望以这种方式保护所有属性,那么它就会变得冗长。没有什么可担心的 - 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("%s attribute must be set to an instance of %s" % (name, 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'>
>>> 

答案 1 :(得分:11)

总的来说,由于@yak在评论中提到的原因,这不是一个好主意。您基本上阻止用户提供具有正确属性/行为但不在您硬编码的继承树中的有效参数。

除了免责声明之外,还有一些选项适用于您要做的事情。主要问题是Python中没有私有属性。因此,如果你只有一个普通的旧对象引用,比如说self._a,即使你已经提供了一个为它进行类型检查的setter,你也不能保证用户不会直接设置它。以下选项演示了如何真正实施类型检查。

覆盖__setattr __

此方法仅适用于您执行此操作的(非常)少量属性。使用点表示法分配常规属性时会调用__setattr__方法。例如,

class A:
    def __init__(self, a0):
        self.a = a0

如果我们现在A().a = 32,则会拨打A().__setattr__('a', 32) under the hood。实际上,self.a = a0中的__init__也使用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__方法,用于自定义如何访问基础属性。这有点像在if中取相应的__setattr__分支,并将其放入一个仅针对该属性运行的方法中。这是一个例子:

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,使得用户不太可能会遇到{ {1}}。第二个问题是你有一个多余的getter来使属性工作为读写。这些问题是this问题的主题。

创建一个真正的Setter-Only Descriptor

这个解决方案可能是整体上最强大的。在accepted answer中建议上述问题。基本上,不是使用一个具有一堆你无法摆脱的装饰和便利的属性,而是创建自己的描述符(和装饰器)并将其用于任何需要进行类型检查的属性:

_a

setter将实际值直接存入实例的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 ,以避免无限期地重复进入自身。这使得可以在不提供显式getter的情况下获取属性的值。由于描述符__dict__没有a方法,因此搜索将继续,直到找到__get__中的属性为止。这可确保所有集合都通过描述符/设置器,同时允许直接访问属性值。

如果您有大量需要检查的属性,可以将行__dict__移到描述符的self.__dict__['a'] = value方法中:

__set__

<强>更新

Python3.6几乎可以为您提供:https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements

TL; DR

对于需要进行类型检查的极少数属性,请直接覆盖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') 。对于大量属性,请使用如上所示的setter-only描述符。直接为这种应用程序使用属性会引入比它解决的问题更多的问题。

答案 2 :(得分:2)

注1:@Blckknght感谢您的公正评论。我在太简单的测试套件中错过了递归问题。

注2:当我刚学习Python时,我写了这个答案。现在我宁愿使用Python的描述符,请参阅例如link1link2

感谢之前的帖子和一些想法,我相信我已经找到了一种更加用户友好的方式来限制类属性是否具有特定类型。

首先,我们创建一个函数,它通常测试类型:

def ensure_type(value, types):
    if isinstance(value, types):
        return value
    else:
        raise TypeError('Value {value} is {value_type}, but should be {types}!'.format(
            value=value, value_type=type(value), types=types))

然后我们只需通过setter在我们的类中使用它。我认为这是相对简单的并且遵循DRY,特别是一旦将其导出到单独的模块以供给整个项目。请参阅以下示例:

class Product:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    @property
    def name(self):
        return self.__dict__['name']

    @name.setter
    def name(self, value):
        self.__dict__['name'] = ensure_type(value, str)

    @property
    def quantity(self):
        return self.quantity

    @quantity.setter
    def quantity(self, value):
        self.__dict__['quantity'] = ensure_type(value, int)

测试产生合理的结果。首先看测试:

if __name__ == '__main__':
    from traceback import format_exc

    try:
        p1 = Product(667, 5)
    except TypeError as err:
        print(format_exc(1))

    try:
        p2 = Product('Knight who say...', '5')
    except TypeError as err:
        print(format_exc(1))

    p1 = Product('SPAM', 2)
    p2 = Product('...and Love', 7)
    print('Objects p1 and p2 created successfully!')

    try:
        p1.name = -191581
    except TypeError as err:
        print(format_exc(1))

    try:
        p2.quantity = 'EGGS'
    except TypeError as err:
        print(format_exc(1))

测试结果如下:

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 35, in <module>
    p1 = Product(667, 5)
TypeError: Value 667 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 40, in <module>
    p2 = Product('Knights who say...', '5')
TypeError: Value 5 is <class 'str'>, but should be <class 'int'>!

Objects p1 and p2 created successfully!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 49, in <module>
    p1.name = -191581
TypeError: Value -191581 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 54, in <module>
    p2.quantity = 'EGGS'
TypeError: Value EGGS is <class 'str'>, but should be <class 'int'>!

答案 3 :(得分:2)

从Python 3.5开始,您可以使用type-hints来指示类属性应该是特定类型。然后,您可以在持续集成过程中包含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'>.

答案 4 :(得分:1)

您可以使用在C ++中提到的相同类型的property。您将从http://adam.gomaa.us/blog/2008/aug/11/the-python-property-builtin/获得有关财产的帮助。

答案 5 :(得分:1)

你可以完全按照你说的说用C ++做的那样做;对它们进行赋值通过setter方法,并使用setter方法检查类型。 Python中的“私有状态”和“公共接口”的概念是通过文档和约定来完成的,并且几乎不可能强制任何人使用你的setter而不是直接分配变量。但是如果你给出以下划线开头的属性名称并将setter作为使用你的类的方式,那就应该这样做(不要使用带有两个下划线的__names;它几乎总是比它的价值更麻烦,除非在他们设计的情况下,你实际上,这是在继承层次结构中冲突的属性名称)。只有特别愚蠢的开发人员才会避免使用类的简单方式,就像它记录的方式一样,有利于弄清楚内部名称是什么并直接使用它们; 开发人员对您的课程表现异常(对于Python)感到沮丧,并且不允许他们使用类似自定义列表的类来代替列表。

您可以像其他答案所描述的那样使用属性来执行此操作,同时仍然使其看起来像是直接分配属性。


就个人而言,我发现在Python中强制执行类型安全的尝试是无用的。并不是因为我认为静态类型检查总是较差,但是因为即使你可以在100%的时间内工作的Python变量上添加类型要求,它们也无法有效地保证你的程序没有类型错误,因为它们只会在运行时引发异常

想一想;当您的静态编译程序成功编译没有错误时,您知道它完全没有编译器可以检测到的所有错误(在Haskell或Mercury等语言的情况下,这是一个非常好的保证,但仍然不完整;在语言如C ++或Java ... meh)。

但是在Python中,只有在执行时才会注意到类型错误。这意味着,即使您可以在程序中获得完整的静态类型强制执行无处不在,您仍需要定期执行具有100%代码覆盖率的测试套件,以实际知道您的程序没有类型错误。但是,如果您经常执行完全覆盖的测试,那么即使没有尝试强制执行类型,如果您有任何类型错误,知道!所以这个好处对我来说真的不值得。你正在抛弃Python的优势(灵活性)而不会在其中一个弱点(静态错误检测)中获得更多的东西。

答案 6 :(得分:1)

我知道这个讨论已经解决,但更简单的解决方案是使用下面的Python Structure模块。这将要求您在为其分配值之前为数据创建容器,但它在保持数据类型静态方面非常有效。 https://pypi.python.org/pypi/structures

答案 7 :(得分:0)

我知道这很老了,但这是该问题的第一个Google搜索结果,所有这些答案似乎都过于复杂。至少对于python 3而言,这是最简单的解决方案:

<nb-checkbox [checked]="checked" (checkedChange)="toggle($event)">Toggle me</nb-checkbox>

运行时会引发异常:

class dog:
    species = 'Dog' 
    def __init__(self, name, age, weight):  
        self.name = str(name)
        self.age = int(age)
        self.weight = float(weight)

'''The following line tries to create a new object from the 'dog' class and 
passes a string instead of an integer for the age argument'''

newdog = dog('spike','three',60) 

在Python中,原始数据类型(int,float,str,booleans)本身就是类。因此,如果在对象创建过程中传递方法参数之前实例化类的类属性,则将尽可能转换参数值(例如,从int到float),或者如果无法转换数据类型,则将引发异常。 (例如,从字符串到整数)。