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 还有特殊的规定
相容规则如下:
- 对T1及其子类型T2,T2与T1相容(里氏替换)
- 任何类型都与Any相容:声明为Any类型的参数接受任何类型的对象
- 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'])
- typing.TYPE_CHECKING 常量在运行时始终为False,不过类型检查工具在做类型检查时会假装为True
- 为series 变量显示声明类型,这样Mypy 的输出更易读懂
- 这个if 语句禁止在运行测试时执行后面的3行
- reveal_type() 不能在运行时调用,因为它不是常规函数,而是Mypy 提供的调试设施,因此无须导入import 导入。Mypy 每遇到一个伪函数调用reveal_type() 就输出一个调试消息,
显示参数的推导类型 - 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)
-
reveal_type(fseries) 显示test_top_tuples 中的 series 是 Iterator[tuple[int,str]] 类型。即我显示声明的类型
-
reveal_typel(result) 确认 top 调用返回的类型是我想要的:根据为series 声明的类型,result的类型是list[tuple[int,str]]
-
revea_type(series) 显示test_top_objects_error 中的 series 是 list[object] 类型。在这个测试中,我没有为series 注解类型,因此是推导出来
-
我是有意测试类型错误,Mypy 也确实发现了错误:可迭代对象series 中元素的类型不能是object(必须是SupportsLessThan) 类型
本文来自博客园,作者:chuangzhou,转载请注明原文链接:https://www.cnblogs.com/czzz/p/15859093.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!