Numpy: ndarray,dtype和ufunc的类型提示/注释(PEP 484)

创建于 2016-03-02  ·  70评论  ·  资料来源: numpy/numpy

功能要求:对带有Numpy数据结构的PEP 484的有机支持。

有没有人为特定的numpy.ndarray类实现类型提示?

现在,我正在使用type.Any,但是最好有更具体的内容。

例如,如果numpy员工为其array_like对象类添加了类型别名。 更好的是,在dtype级别实现支持,以便支持其他对象以及ufunc。

原来的SO问题

01 - Enhancement static typing

最有用的评论

我想再次戳一下它,以查看是否进行了讨论,特别是关于类型提示形状信息的讨论,这在我的许多应用程序中特别有用。 是否有状态跟踪器,或者这不是一个足够高的优先级,因此无法为其分配任何资源?

所有70条评论

我认为没有人想到它。 也许您想? :-)

我还建议如果您要对此进行后续操作,请关闭gh问题并将讨论移至邮件列表,因为它更适合开放式设计讨论。

在获得答案后,我决定解决此问题。

需要明确的是,我们实际上并不反对支持很酷的新python功能或其他任何东西(相反)。 仅仅是我们是一个志愿者运营的项目,没有很多资源,所以只有有兴趣的人加紧努力,事情才会发生。

如果您尝试开始做某事或希望招募其他有兴趣的人来帮助,那么邮件列表通常是最好的地方。

谢谢,@ njsmith。 我决定从这里开始,是因为问题跟踪更加有序,而不是非结构化的邮件列表(我在寻找“功能请求”标签以及其他功能...)

自从回答我问题的那个人以可行的解决方案回到我身边之后,我决定离开此事。
也许应该将Numpy文档更新为包含他的答案(如果您这样做,请确保给予他荣誉)。

再次感谢!

大家好! 我只是想知道在这个问题上是否有任何进展。 谢谢。

这里的邮件列表中对此有一些讨论。

我正在向有兴趣进一步讨论此问题的人重新开放此问题。

我认为这对于NumPy肯定是理想的,但是NumPy API确实存在一些棘手的方面,可以进行输入排序,例如NumPy当前如何在np.array构造函数中接受任意对象(尽管我们想清理,请参阅https://github.com/numpy/numpy/issues/5353)。

一些好的工作正在这里完成: https :

关于是否将工作推向numpy或进行排版的讨论: https :

CC @mrocklin

这确实是NumPy的绝佳补充。 将其推向排版或NumPy的下一步是什么? 即使是不完整的存根也会很有用,我很乐意提供一些指导?

@henryJack最好的起点可能是工具:弄清楚我们如何以与mypy兼容并支持增量添加的方式将基本类型注释集成到NumPy存储库中(并理想地对其进行测试)。

然后,从极少的注释开始,我们可以从那里开始。 特别是,由于我们没有指定它们的好方法,所以我暂时跳过dtype注释(即,仅执行ndarray ,而不是ndarray[int] )。

如果有帮助,我有一个替代版本的注释,我已经编写该注释供Google使用,并且可以开源。 但是我们有自己独特的构建系统,并使用pytype进行类型检查,因此可能会有怪癖将其移植到上游。

我想测试注释的唯一方法是在示例代码段上实际运行mypy并检查输出?

将批注与代码集成在一起或作为单独的存根更好?

我想我们还应该从Dropbox和Pandas中学到,我们应该从代码库的叶子还是核心数据结构开始?

@shoyer figure out how we can integrate basic type annotations
不仅仅是将https://github.com/machinalis/mypy-data/blob/master/numpy-mypy/numpy/__init__.pyi放在numpy模块基本目录中就可以做到这一点。.在某种类型的实验版本中至少

将批注与代码集成在一起或作为单独的存根更好?

与代码集成会很不错,但是我认为NumPy尚不可行。 即使使用类型注释的注释字符串版本,我们也需要在Python 2上从typing导入,并且将依赖项添加到NumPy几乎是不可能的。

而且,大多数核心数据结构和功能(诸如ndarrayarray )都在扩展模块中定义,我们仍然需要在其中使用存根。

不仅仅是将https://github.com/machinalis/mypy-data/blob/master/numpy-mypy/numpy/__init__.pyi放在numpy模块的基本目录中就可以做到这一点。至少

是的,我认为外部代码就足够了。 但是,mypy如何处理带有不完整类型注释的库?

如果可能的话,我们可以直接注释numpy.core.multiarray而不是仅在顶层注释。 ( multiarray是扩展模块,在其中定义了NumPy的核心类型,如ndarray 。)我认为这将允许NumPy本身对其某些纯Python模块使用类型检查。

我很好奇, np.empty(shape=(5, 5), dtype='float32')是什么类型?

np.linalg.svd是什么类型?

我认为@kjyv在定义这些目标时采取了行动。

np.emptyhttps :
np.linalg.svdhttps :

看来类型已经参数化了,这是它们的dtype吗? 参数化其尺寸或形状是否可行? Python的键入模块支持多少复杂程度?

是的,它们由其dtype参数化。 我不是打字模块方面的专家,但我认为您可以让ndarray类型继承Generic[dtype, int]来对ndim进行参数化。 我相信朱莉娅就是这么做的。 我不确定您是否可以轻松地对形状进行参数化。 我也不知道首先会带来什么好处,或者为什么没有这样做。

可以在dtype参数中使用numpy dtypes还是只能输入
模块类型?

同样奇怪的是,numpy.empty返回类型为Any的数组。 我猜测
交互并从dtype =关键字值中获取类型具有挑战性吗?

2017年9月1日下午6:42,“ Jacques Kvam” [email protected]写道:

是的,它们由其dtype参数化。 我不是打字专家
模块,但我认为您可以让ndarray类型继承Generic [dtype,
int]来对ndim进行参数化。 我相信朱莉娅就是这么做的。 我不是
确定您是否可以轻松地对形状进行参数化。 我也不确定
将会带来的好处,或者为什么没有做到这一点,为什么首先要这么做。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub上查看
https://github.com/numpy/numpy/issues/7370#issuecomment-326698639或静音
线程
https://github.com/notifications/unsubscribe-auth/AASszMlYO7iHdoPE_GU--njIYICSVVZ0ks5seIhFgaJpZM4Hm_CR

您可以使用numpy dtype,我们只需要定义它们即可。 这是通过floating和np.std完成的。

https://github.com/kjyv/mypy-data/blob/24ea87d952a98ef62680e812440aaa5bf49753ae/numpy-mypy/numpy/__init__.pyi#L198

我不确定,我认为不可能。 我认为您不能根据参数的值修改输出类型。 我认为我们能做的最好的就是将我们要关心的所有类型专门化函数重载。

https://docs.python.org/3/library/typing.html#typing.overload

另一个选择可能是引入一些严格类型的别名,因此np.empty[dtype]是具有签名(ShapeType) -> ndarray[dtype]的函数。

不寻常的np.cast[dtype](x)函数已经有一些先例

@jwkvam好,所以也许dtype注释是可行的-我只是建议从简单开始,然后从那里开始。

我认为, TypeVar可能被用来代替过载,可能:

D = TypeVar('D', np.float64, np.complex128, np.int64, ...)  # every numpy generic type
def empty(dtype: Type[D]) -> ndarray[Type[D]]: ...

如果我正确理解这一点,则意味着empty(np.float64) -> ndarray[np.float64]

能够键入检查形状和尺寸信息也很棒,但是我认为当前的类型检查器无法胜任这项任务。 Generic[int]是一个错误,例如- Generic的参数必须是TypeVar实例:
https://github.com/python/cpython/blob/868710158910fa38e285ce0e6d50026e1d0b2a8c/Lib/typing.py#L1131 -L1133

我们还需要表达涉及尺寸的签名。 例如, np.expand_dims映射ndim -> ndim+1

我想一种可行的方法是为每个非负整数定义类,例如ZeroOneTwoThree ,..然后为每个定义重载。 那会很累。

在TensorFlow中, tf.Dimension()tf.TensorShape()让您静态表达形状。 但这不是在类型系统中完成的事情。 而是,每个函数都有一个与之关联的辅助函数,该辅助函数根据输入的形状和任何非张量参数确定输出的静态形状。 我认为,如果我们希望通过NumPy做到这一点,我们将需要类似的东西,但是Python的打字系统中没有任何东西可以暗示这种灵活性。

@shoyer我明白了,是的,令人失望。 我能够破解以下内容

_A = TypeVar('_A')
_B = TypeVar('_B', int, np.int64, np.int32)

class Abs(Generic[_A, _B]):
    pass

class Conc(Abs[_A, int]):
    pass

但是我不认为这是领先的...

看来您的示例有效! 没有类型约束,它似乎工作得更好。 我可以测试str类的dtype。 我不得不删除默认参数,无法弄清楚该如何工作。

D = TypeVar('D')
def empty(shape: ShapeType, dtype: Type[D], order: str='C') -> ndarray[D]: ...

和代码

def hello() -> np.ndarray[int]:
    return np.empty(5, dtype=float)

我懂了

error: Argument 2 to "empty" has incompatible type Type[float]; expected Type[int]

我有些困惑,因为如果我交换类型:

def hello() -> np.ndarray[float]:
    return np.empty(5, dtype=int)

我没有错。 即使我不认为任何标记为协变的。

即使类型系统没有我们想要的那样复杂。 您认为这仍然值得吗? 我将不胜感激的一项好处是通过jedi可以更好地完成代码。

我有些困惑,因为如果我交换类型:

我认为这里的问题是int实例被隐式认为对float注释有效。 请参阅键入PEP的数字塔上的注释:
https://www.python.org/dev/peps/pep-0484/#the -numeric-tower

我认为,如果我们坚持使用NumPy标量类型而不是通用的Python类型进行注释,例如np.ndarray[np.integer]而不是np.ndarray[int]可以避免这种情况。

这实际上比我想象的要容易一些,因为TypeVar具有bound参数。 所以修改我的例子:

D = TypeVar('D', bound=np.generic)
def empty(dtype: Type[D]) -> ndarray[D]: ...

我不得不删除默认参数,无法弄清楚该如何工作。

我不太确定你在这里得到什么?

我只是尝试在存根中编码dtype的默认值。 他们在mypy数据仓库中做到了这一点。

def empty(shape: ShapeType, dtype: DtypeType=float, order: str='C') -> ndarray[Any]: ...

来自https://github.com/kjyv/mypy-data/blob/master/numpy-mypy/numpy/__init__.pyi#L523

按照您的示例,我无法使mypy与dtype的默认参数一起使用。 我尝试了dtype: Type[D]=floatdtype: Type[D]=Type[float]

我认为dtype也需要成为通用类型,然后您需要将默认值设置为numpy通用子类,例如np.float64而不是float ,例如,

# totally untested!
D = TypeVar('D', bound=np.generic)

class dtype(Generic[D]):
    <strong i="9">@property</strong>
    def type(self) -> Type[D]: ...

class ndarray(Generic[D]):
    <strong i="10">@property</strong>
    def dtype(self) -> dtype[D]: ...

DtypeLike = Union[dtype[D], D]  # both are coercible to a dtype
ShapeLike = Tuple[int, ...]

def empty(shape: ShapeLike, dtype: DtypeLike[D] = np.float64) -> ndarray[D]: ...

那是不对的。 D == type(dtype.type) == type ,所以您的类型参数化是没有用的,因为使用的唯一参数是D = type

@ eric-wieser糟糕,我认为现在已解决。

关于mypy的问题跟踪器(主要是python / mypy#3540),已经进行了一些相关讨论。 在那里,我们认为主要问题是numpy数组在概念上将其维包含在其类型中,而当前的类型系统并不真正支持该类型。 如果mypy或经过排版的项目可以以任何方式帮助numpy进行打字,请告诉我们!

关于mypy的问题跟踪器(主要是python / mypy#3540),已经进行了一些相关讨论。 在那里,我们认为主要问题是numpy数组在概念上将其维包含在其类型中,而当前的类型系统并不真正支持该类型。 如果mypy或经过排版的项目可以以任何方式帮助numpy进行打字,请告诉我们!

我可以想象在这里以参数化类型编码或多或少的信息。 例如,像np.empty((2, 3))这样的数组可以是以下任何一种类型:

  1. Array[float64, (2, 3)]
  2. Array[float64, (n, m)]
  3. Array[float64, ndim=2]
  4. Array[float64]
  5. Array

@JelleZijlstra您对像mypy这样的工具可能会处理的问题有何看法? 我们能变得多么复杂?

显然,要支持形状和尺寸,需要在类型系统中进行大量工作。 我对此表示欢迎(并在python / mypy#3540中写下了很多想法),但现在让我们将其称为NumPy。 考虑到numpy复杂的类型层次结构和通用类型的挑战,仅让ndarray[float64]工作似乎就足够困难。

是的,我认为第一步只是获得对numpy(以及Pandas和sklearn)的基本键入支持,而无需考虑这些类型的形状和其他额外约束。

其他约束的问题在于,仅描述dtype(形状= 5,6)是不够的,而必须有一种语言来描述对该形状的约束。 可以想象,您想定义一个仅接受正方形numpy形状作为输入的函数,或者一个函数的一个维度必须是另一个维度的2倍。

这样的事情在合同项目中完成了。

我还认为, PEP 472在这里可以很好地支持,因为那时人们真的可以做类似Array[float64, ndim=2]事情。

确实,PEP 472很适合键入,尽管它可能是实现此目的的更简单的修补程序之一! (如果您有兴趣重新开始围绕它的讨论,请对我进行ping操作,因为我认为索引中也有一些引人注目的用例)。

我不确定自己如何做出贡献,但是出于多种原因,我绝对认为这将是一个很棒的功能。 但是,我们朝着这个方向前进,似乎[]变成了一种调用对象的不同方式。 因此, object(*args, **kwargs)执行某些操作, object[*args, **kwargs]会执行其他操作,然后我们甚至可以进行泛化,并具有object{*args, **kwags}object<*args, **kwargs> 。 ;-)

@mitarndarray[float].constrain(ndim=2)类的注释。 我们已经有很多可用的语法,并且与装饰器不同,注释没有限制

实际上,我尝试了以下语法: ndarray[float](ndim=2) ,因此重载泛型__call__再次返回一个类,而不是一个类的实例。 但是对于不是泛型的类型,这变得棘手。

我认为主要问题在于对ndarray[float]支持,因为ndarray[float]并不是ndarray真正存在的东西,因此必须自己更改ndarray我不确定这样做是一个很好的一般原则(更改上游代码以支持更好的键入)。

另一种方法可能是使用新类型的类型变量ConstrainedTypeVar ,您可以在其中执行类似ConstrainedTypeVar('A', bound=ndarray, dtype=float, ndim=2)类的操作,然后将A用作函数签名中的var。 但这变得非常冗长。

写了一篇文档,提出了一些想法,关于广播和尺寸标识的概念如何键入数组形状。

核心思想包括:

  1. 添加一个DimensionVar原语,该原语允许使用数组维的符号身份
  2. ...Ellipsis )识别...指示数组广播。

例如,键入np.matmul / @

from typing import DimensionVar, NDArray, overload

I = DimensionVar('I')
J = DimensionVar('J')
K = DimensionVar('K')

<strong i="17">@overload</strong>
def matmul(a: NDArray[..., I, J], b: NDArray[..., J, K]) -> NDArray[..., I, K]: ...

<strong i="18">@overload</strong>
def matmul(a: NDArray[J], b: NDArray[..., J, K]) -> NDArray[..., K]: ...

<strong i="19">@overload</strong>
def matmul(a: NDArray[..., I, J], b: NDArray[J]) -> NDArray[..., I]: ...

这些足以允许键入广义ufuncs 。 有关更多详细信息和示例,请参见文档。

如果我们已经选择保持NDArrayndarray不同,则可能同时支持dtype和shape的解决方案:

NDArray[float].shape[I, J, K]
NDArray[float]
NDArray.shape[I, J, K]

只是想一想,也有这样的捷径是否有意义?

NDArray.ndim[2]  # NDArray.shape[..., ...]
NDArray[float].ndim[2]  # NDArray[float].shape[..., ...]

—可以简化许多签名,尤其是在下游代码中。

@aldanor我认为您的意思是NDArray.shape[:, :]...表示“零个或多个尺寸”,在这种情况下不太正确)。 但是,是的,这看起来很合理。


快速更新dtypes类型:我使用上述方法编写了一个玩具模块,该np.generic子类与Generic用于参数化ndarray / dtype类型。

正如我所期望的那样,这似乎最适合mypy,包括类型推断,相当于np.empty(..., dtype=np.float32) 。 它确实无法捕获涉及Union类型的我的故意类型错误之一(稍后我将提交错误报告)。

我认为这对于dtypes可能已经足够了。 如果没有键入对文字值的支持,我们就无法使用指定为字符串的dtype( dtype='float32' )进行类型推断。 也许更成问题的是,它也不处理来自dtype=float类的Python类型的类型推断。 但是这些类型可能是模棱两可的(例如,在Linux上dtype=int映射到np.int64 ,在Windows上映射到np.int32 ),因此无论如何都要使用显式泛型。 只要在所有可能的情况下类型推断均无效,就可以,只要将规格dtype=float推断为Any的dtype而不引发错误即可。

但是这些类型可能不明确(例如,dtype = int映射到Linux上的np.int64和Windows上的np.int32)

这并不是模棱两可的-在所有情况下,它都映射到np.int_ C long类型的np.int_

我已经写了邮件列表,以便在单独的程序包中为NumPy编写类型存根达成共识:
https://mail.python.org/pipermail/numpy-discussion/2017-November/077429.html

太好了,谢谢

根据邮件列表上的共识,我想宣布https://github.com/numpy/numpy_stubs营业!

我们将从基本注释(不支持dtype)开始。 如果有人想把基本的PR放在一起为回购添加PEP 561脚手架,将不胜感激!

是,是,1000X是!

请注意以下问题的所有人:我已经在python / typing跟踪器上打开了两个问题:

  • ndarray一般键入(https://github.com/python/typing/issues/513)
  • ndarray输入的语法(https://github.com/python/typing/issues/516)

打字功能的预计发布时间是什么?
是否有任何理由尝试保持2.7兼容性?
早期的评论提到了与python 2集成的困难。此后,numpy似乎改变了其立场。

我知道事物正在移动目标,但是以Python 3.4-3.6之类的目标为目标会有意义吗?

打字功能的预计发布时间是什么?

在PyCon上有一些关于此的讨论(整数泛型,也就是简单的依赖类型),我将基于这些讨论和@shoyer编写的原始文档很快编写一个proto-PEP。 我的目标是让PEP编写,在mypy中实现并及时为Python 3.8 beta 1接受(也很可能随后在typing向Python 2移植新类型)

@hmaarrfk至于为NumPy本身编写类型注释,我们开始在一个单独的存储库中进行此操作: https

当然,我很乐意在可能的地方提供帮助,并且我看到了这个存储库。 我只知道这些事情需要时间。
我看到了仓库,并注意到一个提交提到了2.7兼容性,这就是我问的原因。

Python 3.8 Beta的发布时间为2019年中期。 Numpy提到他们将在2018年底停止新功能。

键入似乎是numpy的“必备”功能,而不是“必备”功能。 因此,以两种语言为目标似乎有些困难,尤其是如果该功能将在numpy自己的支持截止日期之后开始出现的话。

我会对阅读@ilevkivskyi在PEP中必须说的内容感兴趣。

@hmaarrfk您对Python 2.7支持提出了一个好观点。 老实说,我还没有完全考虑。 我确实希望我们最终会放弃它,但是鉴于主要的用例是编写与Python 2/3兼容的代码,因此mypy本身也不再支持Python 2.7。

就目前而言,在我们的类型注释中似乎不需要很多折衷就可以支持Python 2,因此我很乐意将其保留下来,尤其是考虑到它来自对它显然感兴趣的贡献者。

我想再次戳一下它,以查看是否进行了讨论,特别是关于类型提示形状信息的讨论,这在我的许多应用程序中特别有用。 是否有状态跟踪器,或者这不是一个足够高的优先级,因此无法为其分配任何资源?

transonic这个用于泛化numpy加速器的项目中,我们有一种类型提示语法,可以替代使用注释的Pythran注释。 目前,它不适用于mypy,但我想知道它是否有用。 查看示例: https :

如果此问题有用,我会提到我已经制作了一个用于将文档字符串转换为类型注释的工具: https :

我在几个项目中将其与pre-commit一起使用,以使文档字符串与类型注释保持同步。

您仍然需要将文档字符串中的类型转换为符合PEP484。

大家好,

我想尽我所能,所以我分叉了仓库,并开始添加类型提示。 我的想法是自下而上,因此从“简单”功能开始,然后从那里开始。 (从低落的果实开始)

例如在_string_helpers.py ,我向一些变量和函数添加了类型提示。

LOWER_TABLE: str = "".join(_all_chars[:65] + _ascii_lower + _all_chars[65 + 26:])
UPPER_TABLE: str = "".join(_all_chars[:97] + _ascii_upper + _all_chars[97 + 26:])

def english_lower(s: str) -> str:
    """ Apply English case rules to convert ASCII strings to all lower case.
   ...
    """
    lowered = s.translate(LOWER_TABLE)
    return lowered

你怎么看待这件事?

我建议您做一点工作并打开PR以获得评论。 numpy的目标是较旧的python(3.5引入的注释,IIRC),这会破坏这些构建,因此也许可以考虑编写.pyi文件或查看mypy文档,以了解是否有关于最佳实践的更多指导。

到目前为止,我们一直在单独的numpy-stub中进行注释
存储库,但过程很慢。

在2019年11月14日星期四上午9:57 Ben Samuel [email protected]写道:

我建议您做一点工作并打开PR以获得评论。 麻木
针对较旧的python(3.5引入的注解,IIRC),并且此
会破坏这些构建,所以也许考虑编写.pyi文件或检查
mypy文档,以查看是否有关于最佳做法的更多指导。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub上查看
https://github.com/numpy/numpy/issues/7370?
或退订
https://github.com/notifications/unsubscribe-auth/AAJJFVTWTKLP63AK2C2IUW3QTVRMXANCNFSM4B436CIQ

@ bsamuel-ui numpy当前需要Python 3.5 +,NEP-29 [1]指出可以将其提高到3.6+
[1] https://numpy.org/neps/nep-0029-deprecation_policy.html

实际上,所有Python 3版本都支持注释(用于函数args和返回类型)。 3.6仅介绍了变量注释。 在早期的Python 3版本(<3.5)中,您必须使用typing模块的反向端口。

我在第一个.pyi文件中添加了请求请求。 它需要一些工作,但是如果你们可以看看它会很好,这样我可以得到一些初步反馈

正如gh-14905中提到的,我们在https://github.com/numpy/numpy-stubs中有一个存根库的开头

我的坏@mattip。 我将从numpy中删除合并请求,然后向numpy-stubs中添加一个新请求

它仍然打开,但我相信numpy在主版本中已经支持

你好
我正在尝试为向量3d定义一个类型别名,因此是dtype int32的形状(3,)的numpy数组。

(我知道我可以使用np.ndarray键入提示,但是如何获得更具体的提示?我在这里阅读了所有内容,但没有得到,我也搜索了有关如何使用numpy类型进行Python键入的教程,但没有找到任何东西。)

就像可以写:

from typing import Tuple
VectorType = Tuple[int, int, int]

我试着做:

VectorType = np.ndarray(shape=(3,), dtype=np.int32)

这是正确的方法吗?

有人可以在这里给我讲教程或示例吗?

另外,我找到了这个仓库,它是“ Numpy的类型提示”: https :

Numpy会整合吗?
@ramonhagenaars

@mattip

正如gh-14905中提到的,我们在https://github.com/numpy/numpy-stubs中有一个存根库的开头

看起来这已合并到主仓库中。 这已经发布了,还是在路线图上? 试图决定我们是否应该探索诸如https://github.com/ramonhagenaars/nptyping之类的第三方,或者(理想情况下)等待/使用正式支持的类型提示。

谢谢。

我们已经将许多numyp-stub合并到了开发分支中。 您可以通过查找静态键入标签来跟踪进度。 希望这将是下一个版本的一部分。 您可以使用HEAD版本的numpy尝试当前合并的内容。 我们一直在寻找贡献者:就问题和拉取请求进行有建设性的审查,文档和评论是提供帮助的几种方法。

(我知道我可以使用np.ndarray键入提示,但是如何获得更具体的提示?我在这里阅读了所有内容,但没有得到,我也搜索了有关如何使用numpy类型进行Python键入的教程,但没有找到任何东西。)

在这个领域有很多兴趣,但是还不支持NumPy数组的更具体的类型(dtypes和维度)。

我已经提交了@ GilShoshan94 FWIW https://github.com/ramonhagenaars/nptyping/issues/27

此页面是否有帮助?
0 / 5 - 0 等级