NEP 16 — 用于识别“鸭子数组”的抽象基类#
- 作者:
纳撒尼尔·史密斯 < njs @ pobox 。 com >
- 地位:
取消
- 类型:
标准轨道
- 创建:
2018-03-06
- 解决:
笔记
该 NEP 已被撤回,取而代之的是NEP 22中描述的基于协议的方法
抽象的#
我们建议添加一个抽象基类AbstractArray
,以便第三方类可以声明它们“像”一样的能力
ndarray
,以及一个asabstractarray
执行类似的函数,asarray
只是它不改变地传递
AbstractArray
实例。
详细说明#
NumPy 和第三方包中的许多函数都以如下代码开头:
def myfunc(a, b):
a = np.asarray(a)
b = np.asarray(b)
...
这确保了a
和b
是np.ndarray
对象,因此
myfunc
可以继续假设它们在语义上(在 Python 级别)以及它们在内存中的存储方式(在 C 级别)方面都像 ndarray 一样。但其中许多函数仅适用于 Python 级别的数组,这意味着它们实际上并不需要ndarray
对象本身:它们可以与任何“嘎嘎”像 ndarray 的 Python 对象一样工作,例如稀疏数组, dask 的惰性数组,或 xarray 的标记数组。
然而,目前,这些库无法表达它们的对象可以像 ndarray 一样嘎嘎叫,并且函数也无法myfunc
表达它们对任何像 ndarray 那样嘎嘎叫的东西感到满意。此 NEP 的目的是提供这两个功能。
有时人们建议np.asanyarray
为此目的使用,但不幸的是它的语义完全相反:它保证它返回的对象使用与 an 相同的内存布局ndarray
,但根本不告诉你任何关于它的语义,这使得它本质上不可能安全地使用实践。事实上,ndarray
随 NumPy –np.matrix
和
– 一起分发的两个
子类np.ma.masked_array
确实具有不兼容的语义,如果将它们传递给这样的函数,myfunc
则不会将它们作为特殊情况进行检查,那么它可能会默默地返回不正确的结果。
声明一个对象可以像数组一样嘎嘎#
我们可以使用两种基本方法来检查对象是否像数组一样嘎嘎作响。我们可以检查类上的特殊属性:
def quacks_like_array(obj):
return bool(getattr(type(obj), "__quacks_like_array__", False))
或者,我们可以定义一个抽象基类(ABC):
def quacks_like_array(obj):
return isinstance(obj, AbstractArray)
如果您了解 ABC 的工作原理,就会发现这本质上相当于保留一组已声明以实现接口的全局类型
AbstractArray
,然后检查其成员资格。
在这些因素之间,ABC 方法似乎具有许多优点:
这是Python 的标准,“一个明显的方式”来做到这一点。
ABC 可以进行内省(例如
help(np.AbstractArray)
做一些有用的事情)。ABC 可以提供有用的 mixin 方法。
ABC 与 mypy 类型检查
functools.singledispatch
等其他功能集成。
一件明显需要检查的事情是这个选择是否会影响速度。在运行 Linux 的 Thinkpad T450s 上使用 CPython 3.7 预发行版(修订版 c4d77a661138d,自编译,无 PGO)上附带的基准测试脚本,我们发现:
np.asarray(ndarray_obj) 330 ns
np.asarray([]) 1400 ns
Attribute check, success 80 ns
Attribute check, failure 80 ns
ABC, success via subclass 340 ns
ABC, success via register() 700 ns
ABC, failure 370 ns
笔记:
包含前两行是为了将其他行放在上下文中。
这里使用 3.7,因为 ABC
getattr
和 ABC 都在此版本中进行了大量优化,并且它更能代表 Python 的长期未来。 (Failedgetattr
不一定再构造异常对象,并且 ABC 已在 C 中重新实现。)“成功”行指的是
quacks_like_array
返回 True 的情况。 “失败”行是返回 False 的情况。ABC 的第一个测量是子类,定义如下:
class MyArray(AbstractArray): ...
第二个是针对如下定义的子类:
class MyArray: ... AbstractArray.register(MyArray)
我不知道为什么这些之间有这么大的差异。
在实践中,无论哪种方式,我们只会在首先检查众所周知的类型(例如 、 等)之后才进行完整测试。ndarray
这list
就是NumPy 当前检查其他双下划线属性的方式
,并且相同的想法适用于这两种方法。因此,这些数字不会影响常见情况,只会影响我们实际上有一个AbstractArray
或另一个第三方对象的情况,
该对象最终将通过__array__
或__array_interface__
最终作为对象数组。
总而言之,使用 ABC 会比使用属性稍微慢一些,但这不会影响最常见的路径,而且减慢的幅度相当小(对于已经花费更长时间的操作,大约需要 250 ns)。此外,我们可以进一步优化这一点(例如,通过保留已知为 AbstractArray 子类的类型的小型 LRU 缓存,假设大多数代码一次只会使用这些类型中的一种或两种),但目前还不清楚这甚至很重要——如果asarray
无操作传递的速度是配置文件中出现的瓶颈,那么我们可能已经让它们更快了! (快速实现这一点是微不足道的,但我们没有。)
考虑到 ABC 的语义和可用性优势,这似乎是一个可以接受的权衡。
规格asabstractarray
#
鉴于AbstractArray
, 的定义asabstractarray
很简单:
def asabstractarray(a, dtype=None):
if isinstance(a, AbstractArray):
if dtype is not None and dtype != a.dtype:
return a.astype(dtype)
return a
return asarray(a, dtype=dtype)
注意事项:
asarray
也接受一个order=
参数,但我们不在这里包含它,因为它与内存表示的细节有关,并且该函数的全部要点是您使用它来声明您不关心内存表示的细节。使用该
astype
方法允许a
对象决定如何实现其特定类型的转换。为了与 严格兼容,我们在 dtype 已经正确时
asarray
跳过调用 。astype
比较:>>> a = np.arange(10) # astype() always returns a view: >>> a.astype(a.dtype) is a False # asarray() returns the original object if possible: >>> np.asarray(a, dtype=a.dtype) is a True
如果您继承的话,您到底承诺什么AbstractArray
?#
随着时间的推移,这可能会得到完善。当然,理想的情况是您的类应该与真实的类没有区别ndarray
,但除了用户的期望之外,没有什么可以强制实现这一点。在实践中,声明你的类实现了AbstractArray
接口仅仅意味着它将开始传递asabstractarray
,因此通过子类化它,你是说如果某些代码适用于
ndarray
s 但对你的类来说会中断,那么你愿意接受错误报告关于这一点。
首先,我们应该声明__array_ufunc__
为一个抽象方法,并将这些NDArrayOperatorsMixin
方法添加为 mixin 方法。
声明astype
为@abstractmethod
可能也有意义,因为它被asabstractarray
.我们可能还想继续添加一些基本属性,例如ndim
, shape
,
dtype
。
添加新的抽象方法会有点棘手,因为 ABC 在子类时强制执行这些方法;因此,简单地添加一个新的 @abstractmethod将破坏向后兼容性。如果这成为一个问题,那么我们可以使用一些技巧来实现一个 @upcoming_abstractmethod装饰器,该装饰器仅在该方法丢失时发出警告,并将其视为常规弃用周期。 (在这种情况下,我们要反对的是“支持缺少 X 功能的抽象数组”。)
命名#
ABC 的名称并不重要,因为它只会在相对特殊的情况下很少被引用。函数的名称非常重要,因为大多数现有实例都
asarray
应该被它替换,并且将来每个人都应该默认使用它,除非他们有特定的原因要使用它asarray
。这表明它的名字确实应该比……更短、更容易记住asarray
,但这很困难。我在这个草案中使用过asabstractarray
,但我对它不是很满意,因为它太长了,如果没有无休止的劝告,人们不太可能习惯性地开始使用它。
一种选择是实际更改 的asarray
语义,以便它不改变地传递AbstractArray
对象。但我担心可能有很多代码调用
asarray
然后将结果传递给某个不进行任何进一步类型检查的 C 函数(因为它知道其调用者已经使用了asarray
)。如果我们允许asarray
返回
AbstractArray
对象,然后有人调用这些 C 包装器之一并向其传递一个AbstractArray
对象(例如稀疏数组),那么他们就会遇到段错误。现在,在相同的情况下,
asarray
将改为调用对象的__array__
方法,或使用缓冲区接口来创建视图,或传递具有对象数据类型的数组,或引发错误,或类似的。在大多数情况下,这些结果可能实际上都不理想,所以也许将其设为段错误就可以了?但鉴于我们不知道此类代码有多常见,这很危险。 OTOH,如果我们从头开始,那么这可能是理想的解决方案。
我们不能使用asanyarray
or array
,因为它们已经被占用了。
还有其他想法吗?np.cast
, np.coerce
?
执行#
重命名
NDArrayOperatorsMixin
为AbstractArray
(保留别名以实现向后兼容)并使其成为 ABC。Add
asabstractarray
(或者无论我们最终如何称呼它),可能还有一个 C API 等效项。开始将 NumPy 内部函数迁移到
asabstractarray
适当的地方。
向后兼容性#
这纯粹是一个新功能,因此不存在兼容性问题。 (除非我们决定改变其本身的语义asarray
。)
拒绝的替代方案#
提出的一项建议是为数组接口的不同子集定义多个抽象类。该提案中的任何内容都不会阻止 NumPy 或第三方将来这样做,但很难提前猜测哪些子集有用。此外,“完整的 ndarray 接口”是现有库编写时所期望的(因为它们使用实际的 ndarray)和测试(因为它们使用实际的 ndarray 进行测试),因此它是迄今为止最简单的起点。
讨论链接#
附录:基准测试脚本#
import perf
import abc
import numpy as np
class NotArray:
pass
class AttrArray:
__array_implementer__ = True
class ArrayBase(abc.ABC):
pass
class ABCArray1(ArrayBase):
pass
class ABCArray2:
pass
ArrayBase.register(ABCArray2)
not_array = NotArray()
attr_array = AttrArray()
abc_array_1 = ABCArray1()
abc_array_2 = ABCArray2()
# Make sure ABC cache is primed
isinstance(not_array, ArrayBase)
isinstance(abc_array_1, ArrayBase)
isinstance(abc_array_2, ArrayBase)
runner = perf.Runner()
def t(name, statement):
runner.timeit(name, statement, globals=globals())
t("np.asarray([])", "np.asarray([])")
arrobj = np.array([])
t("np.asarray(arrobj)", "np.asarray(arrobj)")
t("attr, False",
"getattr(not_array, '__array_implementer__', False)")
t("attr, True",
"getattr(attr_array, '__array_implementer__', False)")
t("ABC, False", "isinstance(not_array, ArrayBase)")
t("ABC, True, via inheritance", "isinstance(abc_array_1, ArrayBase)")
t("ABC, True, via register", "isinstance(abc_array_2, ArrayBase)")
版权所有#
本文档已置于公共领域。