【问题标题】:How to write __getitem__ cleanly?如何干净地写__getitem__?
【发布时间】:2015-01-12 03:30:36
【问题描述】:

在 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 社区中的regarded as an antipattern。如何避免?

  • 我不能使用functools.singledispatch,因为那是quite deliberately 与方法不兼容(它将尝试在self 上调度,这完全没用,因为我们已经通过OOP 多态性在self 上调度)。它适用于@staticmethod,但如果我需要从self 中取出东西怎么办?
  • 投到int(),然后接住TypeError,检查一个分片,并可能再次加注仍然很难看,尽管可能稍微少一些。
  • 将整数转换为单元素切片并使用相同的代码处理这两种情况可能更简洁,但这有其自身的问题(返回0[0]?)。

【问题讨论】:

  • 反模式使用type()而不是isinstance(),并在用户级别使用此类代码——在编写__dunder__方法时,您应该进行类型检查...使用@ 987654337@.

标签: python python-3.x


【解决方案1】:

尽管这看起来很奇怪,但我怀疑你拥有它的方式是处理事情的最佳方式。模式通常存在以包含常见用例,但这并不意味着当遵循它们会使生活变得更加困难时,它们应该被视为福音。 PEP 443 拒绝显式类型检查的主要原因是它“脆弱且无法扩展”。但是,这主要适用于随时采用多种不同类型的自定义函数。来自Python docs on __getitem__

对于序列类型,接受的键应该是整数和切片对象。请注意,负索引的特殊解释(如果类希望模拟序列类型)取决于 __getitem__() 方法。如果 key 的类型不合适,可能会引发 TypeError;如果序列的索引集之外的值(在对负值进行任何特殊解释之后),则应引发 IndexError。对于映射类型,如果缺少键(不在容器中),则应引发 KeyError。

Python 文档明确说明了应该接受的两种类型,以及如果提供了不属于这两种类型的项目该怎么办。鉴于这些类型是由文档本身提供的,它不太可能改变(这样做会破坏更多的实现,而不仅仅是你的实现),所以你可能不值得费心去针对 Python 本身可能发生的变化进行编码。

如果您打算避免显式类型检查,我会为您指出this SO answer。它包含 @methdispatch 装饰器的简洁实现(不是我的名字,但我会使用它),它允许 @singledispatch 通过强制检查 args[1] (arg) 而不是 args[0] (self )。使用它应该允许您在 __getitem__ 方法中使用自定义单一调度。

您是否考虑这些“pythonic”取决于您,但请记住,虽然 Python 之禅指出“特殊情况不足以打破规则”,但它会立即指出“实用性”胜过纯洁”。在这种情况下,仅检查文档明确指出的两种类型是 __getitem__ 应该支持的唯一东西,这对我来说似乎是一种实用的方法。

【讨论】:

    【解决方案2】:

    反模式是让代码进行显式类型检查,这意味着使用type() 函数。为什么?因为那样目标类型的子类将不再起作用。例如,__getitem__ 可以使用int,但使用type() 来检查它意味着int 子类,它可以工作,只会因为type() 不返回int 而失败。

    当需要进行类型检查时,isinstance 是合适的方法,因为它不排除子类。

    在编写__dunder__ 方法时,类型检查是必要的,也是预期的——使用isinstance()

    换句话说,您的代码完全是 Pythonic,唯一的问题是错误消息(它没有提到 slices)。

    【讨论】:

      【解决方案3】:

      我不知道有什么方法可以避免这样做一次。这只是以这种方式使用动态类型语言的权衡。但是,这并不意味着您必须一遍又一遍地这样做。我会通过创建一个带有拆分方法名称的抽象类来解决它一次,然后从该类继承而不是直接从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,如果我只有一个派生类,我什至可以这样做。我有点惊讶标准库还没有那样工作。

      【讨论】:

      • 当然,使用的语言为我们使用__getslice__ 执行此操作,但它已被弃用,这让我怀疑整个技术的可靠性。
      • @Kevin,由于不同的原因,它已被弃用。他们想创建一个slice 对象,但__getslice__ 采用了ij 参数。它会破坏向后兼容性。
      【解决方案4】:

      要保持 Python 风格,您需要处理语义而不是对象的类型。因此,如果您有一些参数作为序列的访问器,只需像这样使用它。尽可能长时间地使用参数的抽象。如果您期望一组用户标识符,请不要期望一组,而是一些具有方法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​​ 等动态编程语言使用鸭子类型。鸭子是一种动物,像鸭子一样走路,像鸭子一样游泳,像鸭子一样嘎嘎叫。不是因为有人称它为“鸭子”。

      【讨论】:

      • 我同意,但 getitem 的文档明确指出“只允许 int 和 slice 类型的对象”。没有理由允许或接受可迭代对象,原因很简单,getitem 不应该接受可迭代对象,并且出于[] 运算符的目的,永远不会提供列表。鸭子打字一切都很好,但是文档非常明确地说明了要接受哪些项目。请参阅the python docs for getitem 了解更多信息。
      • 我没有基础实现。我都是手工做的。出于说明目的,代码已被简化;我的实际代码是 3D 可切片的,不使用 NumPy,并且必须进行大量索引预处理,否则会分散注意力。
      • 在这种情况下,我认为@SevenDeadlySins 的回答是最有帮助的。
      猜你喜欢
      • 2016-08-06
      • 1970-01-01
      • 1970-01-01
      • 2013-03-01
      • 2014-01-11
      • 1970-01-01
      • 1970-01-01
      • 2010-10-26
      • 2018-06-20
      相关资源
      最近更新 更多