2012-02-16 65 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'>.