[英]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
对象,而是一些带有encode
和decode
方法的字符的容器。
我假设一般你想做一些像“使用基本实现的行为,除非提供一些特殊值。如果你想实现__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.