繁体   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