【问题标题】:What exactly is a Sequence?究竟什么是序列?
【发布时间】:2020-11-08 05:46:11
【问题描述】:

Python docs 有点模棱两可

顺序

一个支持通过__getitem__()特殊方法使用整数索引的高效元素访问的可迭代对象,并定义了__len__() 返回序列长度的方法。一些内置序列 类型是列表、字符串、元组和字节。注意 dict 也支持 __getitem__()__len__(),但被认为是映射而不是序列,因为查找使用任意不可变键而不是 整数。

collections.abc.Sequence 抽象基类定义了一个更丰富的接口,不仅限于__getitem__()__len__(), 添加 count()、index()、__contains__()__reversed__()。类型 实现这个扩展接口的可以显式注册 使用 register()。

特别是,使用abc.collections.Sequence 作为recommended by some 的黄金标准意味着,例如,numpy 数组不是序列:

isinstance(np.arange(6),collections.abc.Sequence)
# False

还有一个叫做 Sequence Protocol 的东西,但它似乎只在 C-API 中公开。那里的标准是

int PySequence_Check(PyObject *o)

如果对象提供序列协议,则返回 1,否则返回 0。请注意,它为带有 a 的 Python 类返回 1 __getitem__() 方法,除非它们是 dict 子类,因为在一般情况下,无法确定它支持的键类型。 这个函数总是成功的。

最后,我不会太密切地关注这个新的(-ish)类型注释业务,但我想这也将受益于序列是什么的清晰概念。

所以我的问题既有哲学的一面,也有实用的一面:序列到底是什么?以及如何测试某事是否是序列?理想情况下,以某种方式制作 numpy 数组序列。如果我开始注释,我将如何处理序列?

【问题讨论】:

  • numpy 代码中,我们看到诸如“使用序列设置数组元素”之类的错误。列表或元组会引发这种情况,但不会引发单个元素数组。 arr[0] = [1] 给出错误; arr[0]=np.array([1]) 没有。 arr[0]=np.array([1,2]) 确实如此。
  • 实际的答案是,类型匹配 Sequence 需要主动注册(通过继承或 Sequence.register)和方法定义,没有人费心去做。
  • @user2357112supportsMonica 我知道那篇文章并且实际上已经引用了它。不过,注释的角度是新的,在我看来,它似乎又增加了一个寻求清晰的理由。另外,.index.count 没有在该线程中提及,所以我想知道它们是否是新的,collections.abc.Sequence 已经将两种方法与核心/最小序列要求相距甚远。
  • 并不是说它回答了问题的核心,但是,出于实际目的,如果您想使用专门针对序列和 NumPy 数组的类型注释,您也可以使用Union:SequenceOrNdarray = Union[Sequence[Any], np.ndarray] .

标签: python numpy sequence


【解决方案1】:

Python中打字简介

如果您知道什么是结构类型、名义类型和鸭子类型,请跳过。

我认为大部分混淆源于 typing 是 3.5 和 3.6 版本之间的临时模块这一事实。并且仍然会在 3.7 和 3.8 版本之间发生变化。这意味着 Python 试图通过类型注释处理类型的方式发生了很大变化。

python 既是鸭子类型又是名义类型也无济于事。也就是说,当访问对象的属性时,Python 是鸭子类型的。仅在运行时检查对象是否具有属性,并且仅在立即请求时才检查该对象。然而,Python 也有名义上的类型特征(例如isinstance()issubclass())。名义类型是一种类型被声明为另一种类型的子类。这可以通过继承,或者使用ABCMetaregister() 方法。

typing 最初使用名义类型的思想引入了它的类型。从 3.8 开始,它正试图允许更多的 Pythonic 结构类型。 结构类型与鸭子类型有关,只是它是在“编译时”而不是运行时考虑的。例如,当 linter 试图检测可能的类型错误时——例如,如果您要将 dict 传递给只接受像元组或列表这样的序列的函数。对于结构类型,如果类B 实现了A 的所有方法,则应将其视为A 的子类型,无论它是否已被声明为A 的子类型(如在名义类型中) )。

回答

序列(小 s)是鸭子类型。序列是提供对其成员的随机访问的任何有序对象集合。具体来说,如果它定义了__len____getitem__ 并使用0 到n-1 之间的整数索引,那么它就是一个序列。序列(大 s)是一种名义类型。也就是说,要成为一个序列,一个类必须这样声明,要么通过继承自序列,要么注册为子类。

一个numpy数组一个序列,但它不是一个序列,因为它没有注册为序列的子类。也不应该,因为它没有实现 Sequence 承诺的完整接口(缺少 count()index() 之类的东西)。

听起来你想要的是一个序列(小 s)的结构化类型。从 3.8 开始,这可以通过使用 protocols 来实现。协议定义了一组方法,一个类必须实现这些方法才能被视为协议的子类(结构类型)。

from typing import Protocol
import numpy as np

class MySequence(Protocol):
    def __getitem__(self, index):
        raise NotImplementedError
    def __len__(self):
        raise NotImplementedError
    def __contains__(self, item):
        raise NotImplementedError
    def __iter__(self):
        raise NotImplementedError

def f(s: MySequence):
    for i in range(len(s)):
        print(s[i], end=' ')
    print('end')

f([1, 2, 3, 4]) # should be fine
arr: np.ndarray = np.arange(5)
f(arr) # also fine
f({}) # might be considered fine! Depends on your type checker

协议相当新,因此并非所有 IDE/类型检查器都支持它们。我使用的 IDE PyCharm 可以。它不喜欢f({}),但很高兴将一个 numpy 数组视为一个序列(大 S)(也许并不理想)。您可以使用typingruntime_checkable 装饰器启用协议的运行时检查。请注意,所有这些都是单独检查每个协议方法是否可以在给定的对象/类上找到。因此,如果您的协议有很多方法,它可能会变得非常昂贵。

【讨论】:

  • 请注意,large-s 序列也与 small-s Sequence Typesother 规范相关。它将结构等价于标称大 s 序列编码。
  • 相关是的,但我认为它也是为了方便而不是明确的定义。也就是说,它实现了内置序列类型的共享功能集,但不作为序列的事实定义呈现。例如,它没有实现+*,它们都被列为序列实现的常见操作。我的回答是指出定义的松懈,并推动一个自定义协议,其中 OP 可以定义所需的规范。
  • 你能放大鸭子和结构吗?我不明白其中的区别。在这里编译与运行时是什么意思?
  • 运行时意味着程序实际运行时。编译时间,是在运行时之前运行的东西。对于类 C 语言,这主要是编译器。对于像 Python 这样的语言,这主要是 linter。对于 Python,结构类型只对 linter 有任何影响。在运行您的代码之前,linter 将尝试检测可能的类型错误。 Using Protocols 告诉 linter,我们只关心类是否实现了所需的方法,而不关心其严格的名义类型。也就是说,您关心的是类的行为类似于序列,而不是其名义类型。
  • 你有关于的参考吗?“如果它[...]使用0到n-1之间的整数索引,那么它是一个序列”要求?我找不到任何实际指定的内容。
【解决方案2】:

我认为在 Python 中定义序列最实用的方法是“支持整数索引的容器”。

Wikipedia 定义也成立:

序列是对象的枚举集合,其中允许重复且顺序很重要。

为了验证一个对象是否是一个序列,我会模仿Sequence Protocol中的逻辑:

hasattr(test_obj, "__getitem__") and not isinstance(test_obj, collections.abc.Mapping) 

【讨论】:

    【解决方案3】:

    根据您粘贴的文档:

    collections.abc.Sequence 抽象基类定义了一个更丰富的接口,不仅限于__getitem__()__len__(),还添加了count()index()__contains__()__reversed__()。实现此扩展接口的类型可以使用 register() 显式注册。

    numpy.ndarray 没有实现Sequence 协议,因为它没有实现count()index()

    >>> arr = numpy.arange(6)
    >>> isinstance(arr, Sequence)
    False
    >>> arr.count(3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'numpy.ndarray' object has no attribute 'count'
    >>> arr.index(3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'numpy.ndarray' object has no attribute 'index'
    

    对比range

    >>> r = range(6)
    >>> isinstance(r, Sequence)
    True
    >>> r.count(3)
    1
    >>> r.index(3)
    3
    

    如果您想声明arrSequence,您可以使用register() 类方法:

    >>> Sequence.register(numpy.ndarray)
    <class 'numpy.ndarray'>
    >>> isinstance(arr, Sequence)
    True
    

    但这是一个谎言,因为它实际上并没有实现协议(register() 函数实际上并没有检查它,它只是信任你):

    >>> arr.count(3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'numpy.ndarray' object has no attribute 'count'
    

    如果您将numpy.ndarray 传递给需要Sequence 的函数,这样做可能会导致错误。

    【讨论】:

    • 这真的站不住脚。大多数使用collections.abc.Sequence 注册的类型实际上并未实现所有方法(尤其是__reversed__)。检查issubclass(Whatever, collections.abc.Sequence) 实际上只检查Whatever 在其MRO 中是否有collections.abc.Sequence,或者Whatever(或超类)是否已在collections.abc.Sequence(或子类)中注册。没有任何接口检查。
    • 是的;我在回答中说过这一点,但值得重申的是,Sequence.register 实际上并没有验证您是否实现了协议。 如果模块向 Sequence 注册了一个实际上并没有实现它的类,这样类的实例的行为方式与Sequences 的行为方式不同,那么 这是一个谎言,它应该被视为该模块 IMO 中的一个错误。
    猜你喜欢
    • 2021-10-16
    • 1970-01-01
    • 2014-10-28
    • 2012-08-27
    • 2010-11-12
    • 2011-03-18
    • 2011-01-22
    • 1970-01-01
    相关资源
    最近更新 更多