簡體   English   中英

如何干凈地寫__getitem__?

[英]How to write __getitem__ cleanly?

在Python中,當實現序列類型時,我經常(相對而言)發現自己編寫這樣的代碼:

class FooSequence(collections.abc.Sequence):
    # Snip other methods

    def __getitem__(self, key):
        if isinstance(key, int):
            # Get a single item
        elif isinstance(key, slice):
            # Get a whole slice
        else:
            raise TypeError('Index must be int, not {}'.format(type(key).__name__))

代碼使用isinstance()顯式檢查其參數的類型。 這被認為是 Python社區中的反模式 我該如何避免呢?

  • 我不能使用functools.singledispatch ,因為這非常故意與方法不兼容(它將嘗試在self上發送,這完全沒用,因為我們已經通過OOP多態性調度self )。 它的工作原理與@staticmethod ,但如果我有什么需要得到的東西出來的self
  • 轉換為int()然后捕獲TypeError ,檢查切片,並且可能重新提升仍然很難看,盡管可能稍微不那么重要。
  • 將整數轉換為單元素切片並使用相同的代碼處理這兩種情況可能更清晰,但這有其自身的問題(返回0[0] ?)。

盡管看起來很奇怪,但我懷疑你擁有它的方式是最好的方法。 模式通常存在以包含常見的用例,但這並不意味着在遵循它們時應將它們視為福音,這會使生活變得更加困難。 PEP 443在明確的類型檢查中給出的主要原因是它“脆弱且不能延伸”。 但是,這主要適用於隨時采用多種不同類型的自定義函數。 來自__getitem__Python文檔

對於序列類型,接受的鍵應該是整數和切片對象。 請注意,負索引的特殊解釋(如果類希望模擬序列類型)取決於__getitem __()方法。 如果key是不合適的類型,則可能引發TypeError; 如果序列的索引集之外的值(在對負值進行任何特殊解釋之后),則應引發IndexError。 對於映射類型,如果缺少鍵(不在容器中),則應引發KeyError。

Python文檔明確說明了應該接受的兩種類型,以及如果提供了不屬於這兩種類型的項目該怎么辦。 鑒於這些類型是由文檔本身提供的,它不太可能改變(這樣做會破壞更多的實現而不僅僅是你的實現),因此,對於可能會改變的Python本身來說,編寫代碼可能並不值得。

如果您打算避免明確的類型檢查,我會指出您的SO答案 它包含一個@methdispatch裝飾器的簡潔實現(不是我的名字,但我會用它滾動),它允許@singledispatch使用方法強制它檢查args[1] (arg)而不是args[0] (self )。 使用它應該允許您使用__getitem__方法使用自定義單一調度。

你是否認為這些“pythonic”中的任何一個都取決於你,但請記住,雖然Python的Zen指出“特殊情況不足以破壞規則”,但它立即注意到“實用性超越純度” 。 在這種情況下,只檢查文檔明確指出的兩種類型是__getitem__應該支持的唯一事情對我來說似乎是實用的方法。

我不知道有辦法避免這樣做一次 這只是以這種方式使用動態類型語言的權衡。 但是,這並不意味着你必須一遍又一遍地做。 我會通過創建一個帶有拆分方法名稱的抽象類來解決它,然后從該類繼承,而不是直接從Sequence繼承,如:

class UnannoyingSequence(collections.abc.Sequence):

    def __getitem__(self, key):
        if isinstance(key, int):
            return self.getitem(key)
        elif isinstance(key, slice):
            return self.getslice(key)
        else:
            raise TypeError('Index must be int, not {}'.format(type(key).__name__))

    # default implementation in terms of getitem
    def getslice(self, key):
        # Get a whole slice

class FooSequence(UnannoyingSequence):
    def getitem(self, key):
        # Get a single item

    # optional efficient, type-specific implementation not in terms of getitem
    def getslice(self, key):
        # Get a whole slice

這足以清理FooSequence ,如果我只有一個派生類,我甚至可以這樣做。 標准庫尚未以這種方式工作,我感到很驚訝。

反模式用於普通用戶代碼進行類型檢查,尤其是使用type()函數1

當與內部進行isinstance()時,可能需要進行2種類型的檢查,並且isinstance()是首選方法。

換句話說,你的代碼完全是Pythonic,它唯一的問題是錯誤信息(它沒有提到slice )。


披露:我是Python核心開發人員。


1當絕對需要時, isinstance()是更好的選擇。

2特別是__getitem__等方法

為了保持pythonic,你可以使用語義而不是對象的類型。 因此,如果您有一些參數作為序列的訪問者,那就這樣使用它。 盡可能長時間地使用抽象參數。 如果您期望一組用戶標識符,請不要指望一個集合,而是一些帶有方法add數據結構。 如果你期望一些文本,不要指望一個unicode對象,而是一些帶有encodedecode方法的字符的容器。

我假設一般你想做一些像“使用基本實現的行為,除非提供一些特殊值。如果你想實現__getitem__ ,你可以使用一個區分區別,如果提供一個特殊值,會發生不同的事情。我使用以下模式:

class FooSequence(collections.abc.Sequence):
    # Snip other methods

    def __getitem__(self, key):
        try:
            if key == SPECIAL_VALUE:
                return SOMETHING_SPECIAL
            else:
                return self.our_baseclass_instance[key]
        except AttributeError:
            raise TypeError('Wrong type: {}'.format(type(key).__name__))

如果要區分單個值(在perl術語“標量”中)和序列(在Java術語“集合”中),那么確定是否實現了迭代器是很好的。 您可以像我現在一樣使用try-catch模式或hasattr

>>> a = 42
>>> b = [1, 3, 5, 7]
>>> c = slice(1, 42)
>>> hasattr(a, "__iter__")
False
>>> hasattr(b, "__iter__")
True
>>> hasattr(c, "__iter__")
False
>>>

適用於我們的例子:

class FooSequence(collections.abc.Sequence):
    # Snip other methods

    def __getitem__(self, key):
        try:
            if hasattr(key, "__iter__"):
                return map(lambda x: WHATEVER(x), key)
            else:
                return self.our_baseclass_instance[key]
        except AttributeError:
            raise TypeError('Wrong type: {}'.format(type(key).__name__))

像python和ruby這樣的動態編程語言使用duck typing。 鴨子是一種動物,像鴨子一樣走路,像鴨子一樣游動,像鴨子一樣呱呱叫。 不是因為有人稱之為“鴨子”。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM