如何把__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 社区中的 regarded as an antipattern。我该如何避免?
- 我不能使用
functools.singledispatch
,因为它 quite deliberately 与方法不兼容(它将尝试在 self
上调度,这完全没用,因为我们已经在 self
通过 OOP 多态性)。它适用于 @staticmethod
,但如果我需要从 self
中取出东西怎么办?
- 下注到
int()
,然后接住 TypeError
,检查 slice 并可能再次加注仍然很难看,尽管可能稍微不那么难看。
- 将整数转换为单元素切片并使用相同的代码处理这两种情况可能会更清晰,但这有其自身的问题(return
0
或 [0]
? ).
尽管这看起来很奇怪,但我怀疑您拥有它的方式是处理事情的最佳方式。模式通常存在以涵盖常见的用例,但这并不意味着当遵循它们会使生活变得更加困难时,它们应该被视为福音。 PEP 443 拒绝显式类型检查的主要原因是它“脆弱且无法扩展”。但是,这主要适用于随时采用多种不同类型的自定义函数。来自 Python docs on __getitem__
:
For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__() method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.
Python 文档明确说明了应该接受的两种类型,以及如果提供的项目不属于这两种类型,该怎么办。鉴于类型是由文档本身提供的,它不太可能改变(这样做会破坏比你的更多的实现),所以不值得费心去针对 Python 本身进行编码可能会发生变化。
如果您打算避免显式类型检查,我会向您指出 this SO answer。它包含 @methdispatch
装饰器(不是我的名字,但我会使用它)的简洁实现,它让 @singledispatch
通过强制检查 args[1]
(arg)而不是比 args[0]
(自我)。使用它应该允许您将自定义单一调度与您的 __getitem__
方法一起使用。
您是否认为这些“pythonic”中的任何一个取决于您,但请记住,虽然 Python 的 Zen 指出“特殊情况不足以打破规则”,但它然后立即注意到“实用性胜过纯度”。在这种情况下,仅检查文档明确指出的两种类型是唯一 __getitem__
应该支持的东西对我来说似乎是实用的方法。
我不知道有什么方法可以避免一次。这只是以这种方式使用动态类型语言的权衡。但是,这并不意味着您必须一遍又一遍地做。我会通过创建一个带有拆分方法名称的抽象 class 来解决它,然后从 class 继承而不是直接从 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
,如果我只有派生的 class,我什至可以这样做。我有点惊讶标准库还没有那样工作。
为了保持 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术语"scalar")和序列(用Java术语"collection"),那么就是python确定迭代器是否已实现。您可以使用 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".
反模式用于代码进行显式类型检查,这意味着使用 type()
函数。为什么?因为那时目标类型的子类将不再起作用。例如,__getitem__
可以使用 int
,但使用 type()
来检查它意味着 int
-子类,虽然可以工作,但会失败,因为 type()
不 return int
.
当需要 type-check 时,isinstance
是合适的方法,因为它不排除子类。
编写 __dunder__
方法时,类型检查是必要和预期的 -- 使用 isinstance()
.
换句话说,您的代码是完美的 Pythonic,唯一的问题是错误消息(它没有提到 slice
s)。
在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
上调度,这完全没用,因为我们已经在self
通过 OOP 多态性)。它适用于@staticmethod
,但如果我需要从self
中取出东西怎么办? - 下注到
int()
,然后接住TypeError
,检查 slice 并可能再次加注仍然很难看,尽管可能稍微不那么难看。 - 将整数转换为单元素切片并使用相同的代码处理这两种情况可能会更清晰,但这有其自身的问题(return
0
或[0]
? ).
尽管这看起来很奇怪,但我怀疑您拥有它的方式是处理事情的最佳方式。模式通常存在以涵盖常见的用例,但这并不意味着当遵循它们会使生活变得更加困难时,它们应该被视为福音。 PEP 443 拒绝显式类型检查的主要原因是它“脆弱且无法扩展”。但是,这主要适用于随时采用多种不同类型的自定义函数。来自 Python docs on __getitem__
:
For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__() method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.
Python 文档明确说明了应该接受的两种类型,以及如果提供的项目不属于这两种类型,该怎么办。鉴于类型是由文档本身提供的,它不太可能改变(这样做会破坏比你的更多的实现),所以不值得费心去针对 Python 本身进行编码可能会发生变化。
如果您打算避免显式类型检查,我会向您指出 this SO answer。它包含 @methdispatch
装饰器(不是我的名字,但我会使用它)的简洁实现,它让 @singledispatch
通过强制检查 args[1]
(arg)而不是比 args[0]
(自我)。使用它应该允许您将自定义单一调度与您的 __getitem__
方法一起使用。
您是否认为这些“pythonic”中的任何一个取决于您,但请记住,虽然 Python 的 Zen 指出“特殊情况不足以打破规则”,但它然后立即注意到“实用性胜过纯度”。在这种情况下,仅检查文档明确指出的两种类型是唯一 __getitem__
应该支持的东西对我来说似乎是实用的方法。
我不知道有什么方法可以避免一次。这只是以这种方式使用动态类型语言的权衡。但是,这并不意味着您必须一遍又一遍地做。我会通过创建一个带有拆分方法名称的抽象 class 来解决它,然后从 class 继承而不是直接从 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
,如果我只有派生的 class,我什至可以这样做。我有点惊讶标准库还没有那样工作。
为了保持 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术语"scalar")和序列(用Java术语"collection"),那么就是python确定迭代器是否已实现。您可以使用 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".
反模式用于代码进行显式类型检查,这意味着使用 type()
函数。为什么?因为那时目标类型的子类将不再起作用。例如,__getitem__
可以使用 int
,但使用 type()
来检查它意味着 int
-子类,虽然可以工作,但会失败,因为 type()
不 return int
.
当需要 type-check 时,isinstance
是合适的方法,因为它不排除子类。
编写 __dunder__
方法时,类型检查是必要和预期的 -- 使用 isinstance()
.
换句话说,您的代码是完美的 Pythonic,唯一的问题是错误消息(它没有提到 slice
s)。