2012-02-16 142 views
20

如何将类成员变量限制为Python中的特定类型?强制/确保python类属性为特定类型


加长版:

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

谢谢。

+3

Pythonic的方法是适当地记录类。如果设置了错误的对象类型,代码将会失败。另一方面,用户可能会使用不会通过您的'isinstance'检查的类型,否则就会很好(鸭子打字)。 – yak 2012-02-16 07:22:55

回答

30

您可以使用属性像其他答案把它 - 所以,如果你想constraina单个属性,说“棒”, 并限制到一个整数,你可以这样写代码:

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为一类的装饰,并declar你想要的 属性在类体内等于属性需要限制太类型:

...  
... @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

谢谢!完美的作品。 – thornate 2012-02-20 12:29:07

+0

有没有简单的方法来进行多种类型的检查,例如bar = int还是float? – mysteryDate 2016-04-29 15:01:09

+0

Isinstance接受一个实例元组 - 因为它在这段代码上被调用,所以你可以简单地使用这里的一个元组类型 - 它应该只是工作:'bar = int,float' – jsbueno 2016-04-29 20:37:49

1

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

+0

不会有太大的帮助。要让某个属性以这种方式工作,您仍然需要实例中的一些存储,用户可以使用该存储绕过该属性。唯一的好处是你可以使用前导下划线约定来使内部值“私人”,而不是记录它。但是你也可以用普通的setter来做到这一点。 – 2016-12-16 17:24:08

+0

链接唯一的答案是坏的,而且确实链接被破坏。 – Antonio 2017-10-12 20:42:01

1

你可以完全按照你所说的说你会用C++来做;分配给他们通过setter方法,并让setter方法检查类型。 Python中的“私有状态”和“公共接口”的概念是通过文档和约定完成的,任何人都不可能使用setter而不是直接分配变量。但是如果你给出的属性名称以下划线开头,并将其设置为使用类的方式,那就应该这样做(不要使用带有两个下划线的__names;除非你是实际上在他们设计的情况下,这是冲突继承层次中的属性名称)。只有特别懒惰的开发人员才会避免使用该类的简单方法,这种方法可以用来记录内部名称并直接使用它们; 开发人员感到沮丧的行为异常(对于Python),并且不允许他们使用自定义列表类来代替列表。

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


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

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

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

+0

通常,type是更多的开发者指南他们实际上使用一个对象。它有助于传递好类型,并通知开发者如何正确使用对象的方法。 此外,使用“TypeError,传递给函数x的错误值,需要'列表'”而不是“类型int没有'追加'函数”来调试要容易得多。 当你真的需要一个列表时,为什么你会在你的函数中接受一个整数? – Nico 2013-06-26 20:28:52

+1

@Nico当你确实需要一个列表时,它并不是要接受一个整数。这是关于接受任何具有(适当的表现良好)“附加”方法的东西。当你编写的代码完全适用于它时,为什么会拒绝我的'Rope'类(专用字符串列表)?在一个真实世界的项目中,我实际上有一个模块,其中包含5个不同类型的字典类(除了'object'外没有任何超类共同的类)。如果我使用的所有库代码明确需要'dict'的实例,那么它们将毫无用处。 – Ben 2013-06-26 23:10:27

+0

“,强迫任何人使用你的setter而不是直接分配变量”实际上 - 这在Python中是不正确的,这几乎是不可能的。如果你使用描述符(这些描述符很容易用'property' helper callable实现),那么就需要一些难看的黑客入侵,根本不使用setter函数。 – jsbueno 2014-03-23 16:58:45

5

总的来说,这不是@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__方法来定制如何访问底层属性。这就好像在__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 ininstance(value, int): 
      raise TypeError('A.a must be an int') 
     self._a = value 

一个稍微不同的做同样的事情的方式,可以在@ jsbueno的回答中找到。

虽然使用属性这种方式很漂亮,主要解决问题,但它确实存在一些问题。首先是你有一个“私人”_a属性,用户可以直接修改,绕过你的类型检查。这与使用纯粹的getter和setter几乎是相同的问题,但现在的a可以作为重定向到后台的setter的“正确”属性访问,使用户不太可能混淆_a。第二个问题是你有一个多余的getter来使这个属性工作在读写状态。这些问题是this问题的主题。

创建真正的二传手,只描述

这种解决方案可能是最强大的整体。在accepted answer中提出了上述问题。基本上,而是采用了属性,它有一堆多余的装饰,并且你无法摆脱的便利设施,创建自己的描述符注释和使用,对于那些需要类型检查的任何属性:

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 ininstance(value, int): 
      raise TypeError('A.a must be an int') 
     self.__dict__['a'] = value 

setter方法藏匿处将实际值直接放入实例的__dict__以避免无限期地自我递归。这使得可以在不提供显式获取器的情况下获取该属性的值。由于描述符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 ininstance(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

TL; DR

对于需要进行类型检查的极少数属性,请直接覆盖__setattr__。对于更多的属性,使用如上所示的setter-only描述符。直接为这类应用程序使用属性引入了比解决问题更多的问题。

1

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

1

感谢以前的文章和一些想法,我相信我想出了一个更加用户友好的方式来限制类属性是特定类型的。所有的

首先,我们创建一个函数,它普遍的型式试验:

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)) 

然后我们简单地使用,并通过制定者在我们的课堂上应用它。我认为这是相对简单的,并遵循干燥,特别是一旦你将它导出到一个单独的模块,为您的整个项目。看下面的例子:

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

    @property 
    def name(self): 
     return self.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'>! 
0

因为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'>.