Python - 函数中的类型提示

考虑到静态类系统的局限性, PEP 484 只能引入一种渐进式类型系统

渐进式类型系统具有以下性质:

  • 是可选的
    默认情况下,类型检查工具不应对没有类型提示的代码发出警告。当类型检查工具无法确定对象的类型时,会假定其为Any类型。Any类型与其他所有类型兼容

  • 不在运行时捕获类型错误
    类型提示相关的问题由静态类型工具检查、lint程序和IDE捕获。在运行时不能阻止把不一致的值传给函数或分配给变量

  • 不能改善性能

Any 类型

Any 类型是渐进式类型系统的基础,是人们熟知的动态类型。下面是一个没有类型信息的函数:

def double(x):
  return x * 2

在类型检查工具看来,假定其具有以下类型信息:

def double(x: Any) -> Any:
  return x * 2

也就是说,x参数和返回值你可以是任何类型,二者甚至可以不同。Any 类型支持所有可能的操作
以下述签名为例,对比以下Any和Object

 def double(x: object) -> object:

这个函数也接受每一种类型的参数,因为任何类型都是object的子类型
然而,类型检查工具拒绝以下函数:

def double(x: object) -> object:
  return x * 2

这是因为object 不支持__mul__操作。Mypy 报告的错误如下所示。

PS E:\PyProject\study> mypy .\test\demo1.py
test\demo1.py:8: error: Unsupported operand types for * ("object" and "int")  [operator]
Found 1 error in 1 file (checked 1 source file)

越一般的类型,接口越窄,即支持的操作越少。object 类实现的操作比 abc.Sequence少,abc.Sequence 实现的操作比abc.MutableSequence 少,abc.MutableSequence 实现的操作比 list少。

但是Any 是一种魔法类型,位于类型层次结构的顶部和底部。Any 既是最一般的类型,也是最特定的类型(支持所有可能的操作)。至少,在类型检查工具看来是这样

当然没有任何一种类型可以支持所有可能的操作,因此使用Any 不利于类型检查工具完成核心任务,即检测潜在的非法操作,防止运行时异常导致程序崩溃

子类型与相容

传统的面向对象名义类型系统依靠的是子类型关系。对 T1 类及其子类 T2 来说,T2 是 T1 的子类型

以下述代码为例:

class T1:
   ...

class T2(T1):
  ...

def f1(p: T1) -> None...

o2 = T2()

f1(o2) # 有效

f1(o2) 调用运用了里氏替换原则(LSP)。其实,Barbara Liskov 是从受支持的操作角度定义子类型的:用 T2 类型的对象替换 T1 类型的对象,如果程序的行为依然正确,那么T2就是T1的子类型。

接着上一段代码,像下面这样做则违背了LSP.

def f2(p: T2) -> None:
  ...

o1 = T1()
f2(o1) # 类型错误

在渐进式类型系统中还有一种关系:相容(consistent-with)。满足子类型关系必定是相容的,不过对Any 还有特殊的规定

相容规则如下:

  1. 对T1及其子类型T2,T2与T1相容(里氏替换)
  2. 任何类型都与Any相容:声明为Any类型的参数接受任何类型的对象
  3. Any与任何类型都相容:始终可以把Any类型的对象传给预期其他类型的参数

下面使用前面定义的对象o1和o2来说明规则2和规则3。这段代码中的所有调用都是有效的。

def f3(p: Any):
  ...

o0 = object()
o1 = T1()
o2 = T2()

f3(o0) #
f3(o1) # 都有效: 规则2
f3(o2) # 


def f4(): # 返回值类型隐含为 Any
  ...

o4 = f4()

f1(o4) #
f2(o4) # 都有效:规则3 
f3(o4) #

任何渐进式类型系统都需要像Any这样的通配类型

简单的类型和类

像int、float、str 和 bytes 这样的简单类型可以直接在类型提示中使用。标准库、外部包中的具体类、以及用户定义的具体类,也可以在类型提示中使用

抽象基类在类型提示中也能用到

对类来说,相容的定义与子类型相似:子类与所有超类相容

然而,"实用胜过纯粹",凡是总有例外:int 与 complex 相容

内置类型int、float 和 complex 之间没有名义上的子类型关系,它们都是object的直接子类。但PEP 484声称,int与float 相容。float 与 complex 相容

Option[str] 结构其实时Union[str, None]的简写形式,表示plural 的类型可以是str或None

from typing import Optional

def show_count(count:int, singular: str, plural: Optional[str] = None) -> str:
    pass 

'''
从Python 3.10 开始, Union[str, bytes] 可以写成 str | bytes. 这种写法输入的内容更少,也不用从typing中导入Optional 
或Union,下面是新旧两种写法,可以比较一下
plural: Option[str] = None # 旧
plural: str | None = None # 新
'''


# 内置函数ord 的签名旧用到Union,其接受str 或 bytes 类型,并返回一个True
def ord(c:Union[str,bytes]) -> int:
  pass

下面示例中的函数接受一个str,但是可以返回一个str 或float

from typing import Union

def parse_token(token: str) -> Union[str,float]:
  try:
    return float(str)
  except ValueError:
    return token

尽量避免创建返回Union 类型值的函数,因为这会给用户带来额外的负担,迫使它们必须在运行时检查返回值的类型,判断该如何处理。但是,在简单的表达式计算器中可以像上一段代码中的parse_token 那样做。

Union[] 至少需要两种类型。嵌套的Union 类型与扁平的Union 类型效果相同。因此下面的类型提示:
Union[A,B,Union[C,D,E]]

与下面的类型提示作用一样:
Union[A,B,C,D,E]

Union 所含的类型之间不应相容。例如,Union[int,float] 就"画蛇添足" 了,因为int与float相容。仅使用float注解的参数也接受int值

泛化容器

泛型可以用类型参数来声明,以指明可以处理的项的类型

# 参数化一个list,约束元素类型
def tokenize(text: str) -> list[str]:
  return text.upper().split()

在python 3.9 及 以上版本中,类型提示的意思是 tokenize 函数返回一个list,而且各项均为 str 类型

stuff: list 和 stuff: list[Any] 这两个注解的意思相同,都表示stuff 是一个列表,而且列表中的项可以是任何类型对象

元组类型

1.用作记录的元组

元组用作记录时,使用内置类型tuple注解,字段的类型在[]内声明
例如:一个内容为城市名,人口数和所属国家的元组,('Shanghai',24.28,'China'),类型提示为:tuple[str,float,str]

2.带有具名字段,用作记录的元组

from typing import NamedTuple
from geolib import geohash as hs

PRECISION = 9


class Coordinate(NamedTuple):
    lat: float
    lon: float


def geohash(lat_lon: Coordinate) -> str:
    return hs.encode(*lat_lon, PRECISION)

'''
typing.NamedTuple 是tuple 子类的制造工厂,因此Coordinate 与tuple 相容,但是反过来就
不成立。
实践中可以放心地把Coordinate 实例传给下面定义的display函数
'''
def display(lat_lon: tuple[float, float]) -> str:
    lat, lon = lat_lon
    ns = 'N' if lat >= 0 else 'S'
    ew = 'E' if lon >= 0 else 'W'
    return f'{abs(lat):0.1f} {ns},{abs(lon):0.1f} {ew}'


if __name__ == '__main__':
    print(geohash(Coordinate(12.23, 3.33)))  # out: s44mef2uf
    print(display(Coordinate(12.23, 3.33)))  # out: 12.2 N,3.3 E

3.用作不可变序列的元组

如果想注解长度不定的元组,可以使用tuple[int,...] 表示项为int 类型,但是元组的长度不一定

省略号表示元素的数量 >=1 。可变长度的元组不能为字段指定不同的类型

泛化映射

泛化映射类型使用MappingType[KeyType,ValueType] 形式注解。在Python 3.9及以上版本中,内置类型dict 及collections 和 collections.abc 中的映射类型都可以这样注解。 更早的版本必须使用typing.Dict 和typing 模块中的其他映射类型



# 提前需了解:
print(chr(33)) # out: ! ,返回数字33的字符形式
print(unicodedata.name(chr(33)), '') # out:EXCLAMATION MARK,感叹号。该函数返回字符的名称


#  =====================================================================

import sys
import re
import unicodedata
from collections.abc import Iterator

RE_WORD = re.compile(r'\w+')
STOP_CODE = sys.maxunicode + 1


def tokenize(text: str) -> Iterator[str]: # 1
    """返回全大写字母构建的迭代器"""
    for match in RE_WORD.finditer(text):
        yield match.group().upper()


def name_index(start: int = 32, end: int = STOP_CODE) -> dict[str, set[str]]:
    index: dict[str, set[str]] = {}   # 2
    for char in (chr(i) for i in range(start, end)):
        if name := unicodedata.name(char, ''): # 3
            for word in tokenize(name):
                index.setdefault(word, set()).add(char)
    return index

if __name__ == '__main__':
    print(name_index(32, 65))  # {'SPACE': {' '}, 'EXCLAMATION': {'!'}, 'MARK': {'!', '?', '"'}, 'QUOTATION': {'"'}, 'NUMBER': {'#'}, 'SIGN': {'#', '>', '+', '$', '=', '%', '<'}, 'DOLLAR': {'$'}, 'PERCENT': {'%'}, 'AMPERSAND': {'&'}, 'APOSTROPHE': {"'"}, 'LEFT': {'('}, 'PARENTHESIS': {'(', ')'}, 'RIGHT': {')'}, 'ASTERISK': {'*'}, 'PLUS': {'+'}, 'COMMA': {','}, 'HYPHEN': {'-'}, 'MINUS': {'-'}, 'FULL': {'.'}, 'STOP': {'.'}, 'SOLIDUS': {'/'}, 'DIGIT': {'7', '3', '1', '6', '0', '9', '2', '8', '5', '4'}, 'ZERO': {'0'}, 'ONE': {'1'}, 'TWO': {'2'}, 'THREE': {'3'}, 'FOUR': {'4'}, 'FIVE': {'5'}, 'SIX': {'6'}, 'SEVEN': {'7'}, 'EIGHT': {'8'}, 'NINE': {'9'}, 'COLON': {':'}, 'SEMICOLON': {';'}, 'LESS': {'<'}, 'THAN': {'>', '<'}, 'EQUALS': {'='}, 'GREATER': {'>'}, 'QUESTION': {'?'}, 'COMMERCIAL': {'@'}, 'AT': {'@'}}


'''
1.tokenize 是一个生成器函数,返回一个可迭代对象,产出的是str类型
2.注解局部变量。如果不加类型提示则Mypy 将发出提示
3.使用了海象运算符,将整个表达式的结果赋值给name
4.name_index 返回一个字典,字典的健是str 类型,字典的值是一个set,set中的元素是str类型
'''

抽象基类

发送时要保守,接受时要大方 ---伯斯塔尔定律,又称稳健性法则

理想情况下,函数的参数应该接受那些抽象类型,而不是具体类型,这样对调用方来说更加灵活

以下述函数签名为例

from collections.abc import Mapping
def name2hex(name: str, color_map: Mapping[str, int]) -> str:

由于注解的类型是abc.Mapping,因此调用方可以提供dict、defaultdict 和 ChainMap 的实例,UserDict 子类的实例,或者Mapping 的任何子类型
相比之下,再看下面的签名:

def name2hex(name:str, color_map: dict[str,int]) -> str:

这里,color_map 必须是dict 或其子类,例如defaultDict 或 OrderDict。特别注意,使用collections.UserDict 的子类无法通过类型检查,尽管前面讲过,建议扩展collctions.UserDict 自定义映射。Mypy 会拒绝UserDict 或其衍生类的实例,因为,UserDict 不是dict 的子类,二者是同级关系,都是abc.MutableMapping 的子类

因此,一般来说再参数的类型提示中最好使用abc.Mapping 或 abc.MutableMapping,不要使用dict。如果 name2hex 函数无须改动传入的color_map,则最准确的类型提示是abc.Mapping。如此一来,调用方就不用提供实现了setdefault、pop 和 update 等方法的对象了,因此这些方法属于MutableMapping 接口,而不是Mapping 接口。这样做体现了伯斯塔尔定律的后半部分:接受时要大方。

伯斯塔尔定律还指出,发送时要保守。因此函数的返回值始终应该是一个具体对象,即返回值的类型提示应当是具体类型

在typing.List 的文档中有这样一段话:

泛化版list,可用于注解返回值。如果想泛化参数,推荐使用抽象容器类型,例如Sequence 或 Iterable

typing.Set 和 typing.Dict 文档也有类似的说明

从Python 3.9 开始,collection.abc 中的大多数抽象基类 和 collections 中的具体类,以及内置的容器,全都支持泛化类型提示,例如 collections.deque[str].使用Pyhton3.8 或之前的版本编写的代码时才需要使用typing 模块中对应的容器类型

Iterable

标准库中的math.fsum 函数,其参数的类型提示用的就是Iterable

截至Python 3.10 ,标准库不含注解,但是Mypy、Pycahrm 等可在Typeshed 项目中找到所需的类型提示。这些文件位于一些存根
文件中,这是一种特殊的源文件,扩展名为.pyi,文件中保存带注解的函数和方法签名,没有实现,有点类似于C语言的头文件。

from collections.abc import Iterable

FromTo = tuple[str, str] # 1

 
def zip_replace(text: str, changes: Iterable[FromTo]) -> str:   # 2
    for from_, to in changes:
        text = text.replace(from_, to)
    return text


if __name__ == '__main__':
    l33t = [('a', '4'), ('e', '3'), ('i', '1'), ('o', '0')]
    text = 'mad skilled noob powned leet'
    print(zip_replace(text, l33t))  # m4d sk1ll3d n00b p0wn3d l33t

1.FromTo 是类型别名,这里把tuple[str,str] 赋值给了FromTo,这样zip_replace 函数签名的可读性会好一些
2.changes 的类型为Iterable[FromTo],这Iterable[tuple[str,str]] 的效果一样,不过签名更短,可读性更高

从Python 3.10 开始,创建类型别名的首选方式如下:
from typing import TypeAlias
FromTo: TypeAlias = tuple[str,str]

参数化泛型和TypeVar

from collections.abc import Sequence
from random import shuffle
from typing import TypeVar

T = TypeVar('T')


def sample(population: Sequence[T], size: int) -> list[T]:
    if size < 1:
        raise ValueError('size must be >= 1')
    result = list(population)
    shuffle(result)
    return result[:size]

if __name__ == '__main__':
    print(sample([1, 2, 3, 4, 5], 3)) # [1, 3, 5]

参数化泛型是一种泛型,写作list[T],其中T是类型变量,每次使用时会绑定具体的类型。这样可在结果的类型中使用参数的类型。

sample 函数接受两个参数,一个是元素类型为T的Sequence,另一个是int。该函数会返回一个list,元素的类型也是T,具体类型由第一个参数决定

标准库中的statistics.mode 函数也是一例。该函数会返回一系列值中出现次数最多的数据点。

不使用TypVar,mode函数的签名可能会像下面的示例一样:

from collections import Counter
from collections.abc import Iterable

def mode(data: Iterable[float]) -> float:

    pairs = Counter(data).most_common(2)  # 统计data中每个元素出现的次数,返回最大的两个, parirs:[(元素,出现的次数)]
    if len(pairs) == 0:
        raise ValueError('no mode from empty data')
    return pairs[0][0]


if __name__ == '__main__':
    print(mode([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])) #  [(4, 4), (3, 3)]

mode 经常用户处理int 或 float 值, 但是Python 还有其他数值类型, 因此返回值类型最好是与Iterable 中的元素类型保持一致。使用TypeVar 可以改进签名,
先来看一个看似简单,但是不正确的参数化签名

from collections.abc import Iterable
from typing import TypeVar

T = TypeVar('T')
def mode(data: Iterable[T]) -> T: 

第一次出现在签名中时,类型参数T可以是任何类型。第二次出现时,与第一次出现的类型相同。

因此,任何可迭代对象都与Iterable[T]相容,包括collections.Counter 无法处理的不可哈希的可迭代类型。需要限制可以赋予T的类型。

下面介绍两种方式。

1.受限的TypeVar
TypeVar 还接受一些位置参数,以对类型参数施加限制。可以改进mode函数的签名,以接受指定的几种数值类型,如下所示。

from fractions import Fraction
from decimal import Decimal

from typing import TypeVar

NumberT = TypeVar('NumberT', float, Decimal, Fraction)

def mode(data: Iterable[NumberT]) -> NumberT:

然而,statistics.mode 文档中还有以下示例:

>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'

这样做当然可以,但是NumberT 名称就文不对题了。而且,不能发现一种mode可以处理的类型就添加,更好的方法是使用接下来介绍的TyperVar的另一项功能

2.有界的TypeVar
前例中,mode 函数主体内的Counter 类用于排名。Couner基于dict,因此可迭代对象data中的元素类型必须是可哈希的。

炸一看,下面的签名有效,返回项是Hashtable 类型,是一个抽象基类,只实现了__hash__ 方发,除了调用hash() 没有什么实际意义

from collections.abc import Iterable,Hashtable

def mode(data: Iterable[Hashtable]) -> HashTbale: 

现在的问题是,返回的是Hashable 类型。Hashable 是一个抽象基类,是实现了__hash__ 方法。因此,除了调用hash(),类型检查工具不会允许返回值做其他任何操作。所以,这么做没什么实际意义。

解决方法是使用TypeVar 的另一个可选参数,即关键字参数bound。这个参数会为可接受的类型设定一个上边界。下列使用bound=Hashtable 指明,类型参数可以是Hashable或它的任何子类

from collections import Counter
from collections.abc import Iterable,Hashable
from typing import TypeVar

HashableT = TypeVar('HashableT' ,bound=Hashable)

def mode(data: Iterable[HashableT]) -> HashableT:

    pairs = Counter(data).most_common(2)  # 统计data中每个元素出现的次数,返回最大的两个, parirs:[(元素,出现的次数)]
    if len(pairs) == 0:
        raise ValueError('no mode from empty data')
    return pairs[0][0]


if __name__ == '__main__':
    print(mode((1, 2, 2, 3, 3, 3, 4, 4, 4, 4))) #  [(4, 4), (3, 3)]


3.预定义的类型变量Anystr

typing 模块提供了一个预定义的类型变量,名为AnyStr。这个类型变量的定义如下所示

AnyStr = TypeVar("AnyStr", str, bytes)  # noqa: Y001

很多接受 bytes 或 str 的函数会使用 AnyStr,返回值也是二者之一。

静态协议

对类型提示来说,协议指的是typing.Protocal 的子类。定义接口供类型检查工具核查

"PEP 544 - Protocols Structural subtyping(static duck typing)" 提出的Protocol 类型类似于Go语言中的接口:定义协议类型时指定一个或多个方法,在需要使用协议类型的地方,类型检查工具会核查有没有实现指定的方法。

在Python中,协议通过typing.Protocol的子类定义。然而,实现协议的类不会与定义协议的类建立任何关系,不继承,也不用注册。类型检查工具负责查找可用的协议类型,施行用法检查

from collections.abc import Iterable
from typing import TypeVar

T = TypeVar('T')
def top(series: Iterable[T], length: int) -> list[T]:
    """ 接受一个列表,返回排序后前length 个值"""
    ordered = sorted(series, reverse=True)
    return ordered[:length]

如何约束T的类型?
如果把一个普通对象列表传给sorted函数,而不提供key函数,是什么情况?

>>> l = [object() for _ in range(4)]
>>> l
[<object object at 0x000001EB6A7A43B0>, <object object at 0x000001EB6A7A4330>, <object object at 0x000001EB6A7A43E0>, <object object at 0x000001EB6A7A43F0>]
>>> sorted(l)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'object' and 'object'
>>>

# 错误消息表明,sorted函数会在可迭代对象的元素上使用<运算符。
>>> class Spam:
...     def __init__(self, n):
...         self.n = n
...     def __lt__(self, other):
...         return self.n < other.n
...     def __repr__(self):
...         return f'Spam({self.n})'
...
>>> l = [Spam(n) for n in range(5)]
>>> sorted(l)
[Spam(0), Spam(1), Spam(2), Spam(3), Spam(4)]

确实如此,Spam对象构成的列表可以排序,因为Spam实现了支持 < 运算符的特殊方法__lt__
因此类型参数 T应该被限定为实现了__lt__的类型,但是目前遇到的问题是typin 或 abc中没有合适的类型,需要自己创建

# 定义协议
class SupperLessThan(Protocol):  # 协议是Protocal的子类
    def __lt__(self, other: Any) -> bool: ...  # 在协议体中定义一个或多个方法,方法的主体为...


# 使用协议
T = TypeVar('T', bound=SupperLessThan)
# T = TypeVar('T')


def top(series: Iterable[T], length: int) -> list[T]:
    """ 接受一个列表,返回排序后前length 个值"""
    ordered = sorted(series, reverse=True)
    return ordered[:length]

class Spam:
    def __init__(self, n):
        self.n = n
    def __lt__(self, other):
        return self.n < other.n
    def __repr__(self):
        return f'Spam({self.n})'

if __name__ == '__main__':
    print(top([Spam(n) for n in range(5)], 3))

mypy 工具检查结果:

# top([Spam(n) for n in range(5)], 3)

E:\PyProject\pytestDemo> mypy demo111.py
Success: no issues found in 1 source file

# top([object() for _ in range(5)], 3)

PS E:\PyProject\pytestDemo> mypy demo111.py
demo111.py:31: error: Value of type variable "T" of "top" cannot be "object"  [type-var]
Found 1 error in 1 file (checked 1 source file)

下面使用pytest 测试以下top函数,先使用top处理通过生成器表达式产出的tuple[int,str] 值,再处理一个object列表。对于object列表,我们预期抛出TypeError 异常。

from collections.abc import Iterator
from typing import TYPE_CHECKING # 1
import pytest

from top import top

def test_top() -> None:
    fruit =  'mango pear apple kiwi banana'.split()
    series: Iterator[tuple[int,str]] =((len(s), s) for s in fruit) # 2
    length  = 3
    expect = [(6,'banana'),(5,'mango'),(5,'apple')]
    result = top(series,length)
    if TYPE_CHECKING: # 3
        reveal_type(series) #4
        reveal_type(expect)
        reveal_type(result)
    assert result == expect

def test_top_objects_error() -> None:
    series = [object() for _ in range(4)]
    if TYPE_CHECKING:
        reveal_type(series)
    with pytest.raises(TypeError) as excinfo:
        top(series,3) # 5
    assert "'<' not supported" in str(excinfo.value)

if __name__ == '__main__':
    pytest.main(['-s', 'demo3.py'])
  1. typing.TYPE_CHECKING 常量在运行时始终为False,不过类型检查工具在做类型检查时会假装为True
  2. 为series 变量显示声明类型,这样Mypy 的输出更易读懂
  3. 这个if 语句禁止在运行测试时执行后面的3行
  4. reveal_type() 不能在运行时调用,因为它不是常规函数,而是Mypy 提供的调试设施,因此无须导入import 导入。Mypy 每遇到一个伪函数调用reveal_type() 就输出一个调试消息,
    显示参数的推导类型
  5. Mypy 对这一行报错

上述测试能通过。不过,即使没有top.py 中的类型提示,测试也能通过。我们就的目的是使用Mypy 检查测试文件,确认TypeVar 中的声明是正确的。

mypy top_test.py 输出如下:

PS E:\PyProject\study> mypy .\test\top_test.py\
test\top_test.py:20: note: Revealed type is "typing.Iterator[tuple[builtins.int, builtins.str]]"  #1
test\top_test.py:21: note: Revealed type is "builtins.list[tuple[builtins.int, builtins.str]]"
test\top_test.py:22: note: Revealed type is "builtins.list[tuple[builtins.int, builtins.str]]"  #2
test\top_test.py:28: note: Revealed type is "builtins.list[builtins.object]"                    #3
test\top_test.py:30: error: Value of type variable "T" of "top" cannot be "object"  [type-var]  #4
Found 1 error in 1 file (checked 1 source file)

  1. reveal_type(fseries) 显示test_top_tuples 中的 series 是 Iterator[tuple[int,str]] 类型。即我显示声明的类型

  2. reveal_typel(result) 确认 top 调用返回的类型是我想要的:根据为series 声明的类型,result的类型是list[tuple[int,str]]

  3. revea_type(series) 显示test_top_objects_error 中的 series 是 list[object] 类型。在这个测试中,我没有为series 注解类型,因此是推导出来

  4. 我是有意测试类型错误,Mypy 也确实发现了错误:可迭代对象series 中元素的类型不能是object(必须是SupportsLessThan) 类型

posted @   chuangzhou  阅读(159)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示