您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么使用裝飾器擴展Python中的計時器功能”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“怎么使用裝飾器擴展Python中的計時器功能”文章能幫助大家解決問題。
假設我們需要跟蹤代碼庫中一個給定函數所花費的時間。使用上下文管理器,基本上有兩種不同的選擇:
1. 每次調用函數時使用 Timer:
with Timer("some_name"): do_something()
當我們在一個py文件里多次調用函數 do_something(),那么這將會變得非常繁瑣并且難以維護。
2. 將代碼包裝在上下文管理器中的函數中:
def do_something(): with Timer("some_name"): ...
Timer
只需要在一個地方添加,但這會為do_something()
的整個定義增加一個縮進級別。
更好的解決方案是使用 Timer 作為裝飾器。裝飾器是用于修改函數和類行為的強大構造。
裝飾器是包裝另一個函數以修改其行為的函數。你可能會有疑問,這怎么實現呢?其實函數是 Python 中的first-class
對象,換句話說,函數可以以變量的形式傳遞給其他函數的參數,就像任何其他常規對象一樣。因此此處有較大的靈活性,也是 Python 幾個最強大功能的基礎。
我們首先創建第一個示例,一個什么都不做的裝飾器:
def turn_off(func): return lambda *args, **kwargs: None
首先注意這個turn_off()
只是一個常規函數。之所以成為裝飾器,是因為它將一個函數作為其唯一參數并返回另一個函數。我們可以使用turn_off()
來修改其他函數,例如:
>>> print("Hello") Hello >>> print = turn_off(print) >>> print("Hush") >>> # Nothing is printed
代碼行 print = turn_off(print)
用 turn_off()
裝飾器裝飾了 print
語句。實際上,它將函數 print()
替換為匿名函數 lambda *args, **kwargs: None
并返回 turn_off()
。匿名函數 lambda 除了返回 None 之外什么都不做。
要定義更多豐富的裝飾器,需要了解內部函數。內部函數是在另一個函數內部定義的函數,它的一種常見用途是創建函數工廠:
def create_multiplier(factor): def multiplier(num): return factor * num return multiplier
multiplier()
是一個內部函數,在 create_multiplier()
內部定義。注意可以訪問 multiplier()
內部的因子,而 multiplier()
未在 create_multiplier()
外部定義:
multiplier
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'multiplier' is not defined
相反,可以使用create_multiplier()
創建新的 multiplier 函數,每個函數都基于不同的參數factor:
double = create_multiplier(factor=2) double(3)
6
quadruple = create_multiplier(factor=4) quadruple(7)
28
同樣,可以使用內部函數來創建裝飾器。裝飾器是一個返回函數的函數:
def triple(func): def wrapper_triple(*args, **kwargs): print(f"Tripled {func.__name__!r}") value = func(*args, **kwargs) return value * 3 return wrapper_triple
triple()
是一個裝飾器,因為它是一個期望函數 func()
作為其唯一參數并返回另一個函數 wrapper_triple()
的函數。注意 triple()
本身的結構:
第 1 行開始了 triple()
的定義,并期望一個函數作為參數。
第 2 到 5 行定義了內部函數 wrapper_triple()
。
第 6 行返回 wrapper_triple()
。
這是種定義裝飾器的一般模式(注意內部函數的部分):
第 2 行開始 wrapper_triple()
的定義。此函數將替換 triple()
修飾的任何函數。參數是 *args
和 **kwargs
,用于收集傳遞給函數的任何位置參數和關鍵字參數。我們可以靈活地在任何函數上使用 triple()
。
第 3 行打印出修飾函數的名稱,并指出已對其應用了 triple()
。
第 4 行調用 func()
,triple()
修飾的函數。它傳遞傳遞給 wrapper_triple()
的所有參數。
第 5 行將 func()
的返回值增加三倍并將其返回。
接下來的代碼中,knock()
是一個返回單詞 Penny 的函數,將其傳給triple() 函數,并看看輸出結果是什么。
>>> def knock(): ... return "Penny! " >>> knock = triple(knock) >>> result = knock() Tripled 'knock' >>> result 'Penny! Penny! Penny! '
我們都知道,文本字符串與數字相乘,是字符串的一種重復形式,因此字符串 'Penny' 重復了 3 次。可以認為,裝飾發生在knock = triple(knock)
。
上述方法雖然實現了裝飾器的功能,但似乎有點笨拙。PEP 318 引入了一種更方便的語法來應用裝飾器。下面的 knock()
定義與上面的定義相同,但裝飾器用法不同。
>>> @triple ... def knock(): ... return "Penny! " ... >>> result = knock() Tripled 'knock' >>> result 'Penny! Penny! Penny! '
@
符號用于應用裝飾器,@triple
表示 triple()
應用于緊隨其后定義的函數。
Python 標準庫中定義的裝飾器方法之一是:@functools.wraps
。這在定義你自己的裝飾器時非常有用。前面說過,裝飾器是用另一個函數替換了一個函數,會給你的函數帶來一個微妙的變化:
knock
<function triple.<locals>.wrapper_triple at 0x7fa3bfe5dd90>
@triple
裝飾了 knock()
,然后被 wrapper_triple()
內部函數替換,被裝飾的函數的名字會變成裝飾器函數,除了名稱,還有文檔字符串和其他元數據都將會被替換。但有時,我們并不總是想將被修飾的函數的所有信息都被修改了。此時 @functools.wraps
正好解決了這個問題,如下所示:
import functools def triple(func): @functools.wraps(func) def wrapper_triple(*args, **kwargs): print(f"Tripled {func.__name__!r}") value = func(*args, **kwargs) return value * 3 return wrapper_triple
使用 @triple
的這個新定義保留元數據:
@triple def knock(): return "Penny! " knock
<function knock at 0x7fa3bfe5df28>
注意knock()
即使在被裝飾之后,也同樣保留了它的原有函數名稱。當定義裝飾器時,使用 @functools.wraps
是一種不錯的選擇,可以為大多數裝飾器使用的如下模板:
import functools def decorator(func): @functools.wraps(func) def wrapper_decorator(*args, **kwargs): # Do something before value = func(*args, **kwargs) # Do something after return value return wrapper_decorator
在本節中,云朵君將和大家一起學習如何擴展 Python 計時器,并以裝飾器的形式使用它。接下來我們從頭開始創建 Python 計時器裝飾器。
根據上面的模板,我們只需要決定在調用裝飾函數之前和之后要做什么。這與進入和退出上下文管理器時的注意事項類似。在調用修飾函數之前啟動 Python 計時器,并在調用完成后停止 Python 計時器。可以按如下方式定義 @timer
裝飾器:
import functools import time def timer(func): @functools.wraps(func) def wrapper_timer(*args, **kwargs): tic = time.perf_counter() value = func(*args, **kwargs) toc = time.perf_counter() elapsed_time = toc - tic print(f"Elapsed time: {elapsed_time:0.4f} seconds") return value return wrapper_timer
可以按如下方式應用 @timer
:
@timer def download_data(): source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) download_data() # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ]
Elapsed time: 0.5414 second
回想一下,還可以將裝飾器應用于先前定義的下載數據的函數:
requests.get = requests.get(source_url, headers=headers)
使用裝飾器的一個優點是只需要應用一次,并且每次都會對函數計時:
data = requests.get(0)
Elapsed time: 0.5512 seconds
雖然@timer
順利完成了對目標函數的定時。但從某種意義上說,你又回到了原點,因為該裝飾器 @timer
失去了前面定義的類 Timer
的靈活性或便利性。換句話說,我們需要將 Timer
類表現得像一個裝飾器。
現在我們似乎已經將裝飾器用作應用于其他函數的函數,但其實不然,因為裝飾器必須是可調用的。Python中有許多可調用的類型,可以通過在其類中定義特殊的.__call__()
方法來使自己的對象可調用。以下函數和類的行為類似:
def square(num): return num ** 2 square(4)
16
class Squarer: def __call__(self, num): return num ** 2 square = Squarer() square(4)
16
這里,square
是一個可調用的實例,可以對數字求平方,就像square()
第一個示例中的函數一樣。
我們現在向現有Timer
類添加裝飾器功能,首先需要 import functools。
# timer.py import functools # ... @dataclass class Timer: # The rest of the code is unchanged def __call__(self, func): """Support using Timer as a decorator""" @functools.wraps(func) def wrapper_timer(*args, **kwargs): with self: return func(*args, **kwargs) return wrapper_timer
在之前定義的上下文管理器 Timer ,給我們帶來了不少便利。而這里使用的裝飾器,似乎更加方便。
@Timer(text="Downloaded the tutorial in {:.2f} seconds") def download_data(): source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) download_data() # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ]
Downloaded the tutorial in 0.72 seconds
有一種更直接的方法可以將 Python 計時器變成裝飾器。其實上下文管理器和裝飾器之間的一些相似之處:它們通常都用于在執行某些給定代碼之前和之后執行某些操作。
基于這些相似之處,在 python 標準庫中定義了一個名為 ContextDecorator
的 mixin
類,它可以簡單地通過繼承 ContextDecorator
來為上下文管理器類添加裝飾器函數。
from contextlib import ContextDecorator # ... @dataclass class Timer(ContextDecorator): # Implementation of Timer is unchanged
當以這種方式使用 ContextDecorator
時,無需自己實現 .__call__()
,因此我們可以大膽地將其從 Timer 類中刪除。
接下來,再最后一次重改 download_data.py
示例,使用 Python 計時器作為裝飾器:
# download_data.py import requests from timer import Timer @Timer() def main(): source_url = 'https://cloud.tsinghua.edu.cn/d/e1ccfff39ad541908bae/files/?p=%2Fall_six_datasets.zip&dl=1' headers = {'User-Agent': 'Mozilla/5.0'} res = requests.get(source_url, headers=headers) with open('dataset/datasets.zip', 'wb') as f: f.write(res.content) if __name__ == "__main__": main()
我們與之前的寫法進行比較,唯一的區別是第 3 行的 Timer 的導入和第 4 行的 @Timer()
的應用。使用裝飾器的一個顯著優勢是它們通常很容易調用。
但是,裝飾器仍然適用于整個函數。這意味著代碼除了記錄了下載數據所需的時間外,還考慮了保存數據所需的時間。運行腳本:
$ python download_data.py # Python Timer Functions: Three Ways to Monitor Your Code
[ ... ]
Elapsed time: 0.69 seconds
從上面打印出來的結果可以看到,代碼記錄了下載數據和保持數據一共所需的時間。
當使用 Timer 作為裝飾器時,會看到與使用上下文管理器類似的優勢:
省時省力: 只需要一行額外的代碼即可為函數的執行計時。
可讀性: 當添加裝飾器時,可以更清楚地注意到代碼會對函數計時。
一致性: 只需要在定義函數時添加裝飾器即可。每次調用時,代碼都會始終如一地計時。
然而,裝飾器不如上下文管理器靈活,只能將它們應用于完整函數。
這里展開下面的代碼塊以查看 Python 計時器timer.py
的完整源代碼。
# timer.py import time from contextlib import ContextDecorator from dataclasses import dataclass, field from typing import Any, Callable, ClassVar, Dict, Optional class TimerError(Exception): """A custom exception used to report errors in use of Timer class""" @dataclass class Timer(ContextDecorator): """Time your code using a class, context manager, or decorator""" timers: ClassVar[Dict[str, float]] = {} name: Optional[str] = None text: str = "Elapsed time: {:0.4f} seconds" logger: Optional[Callable[[str], None]] = print _start_time: Optional[float] = field(default=None, init=False, repr=False) def __post_init__(self) -> None: """Initialization: add timer to dict of timers""" if self.name: self.timers.setdefault(self.name, 0) def start(self) -> None: """Start a new timer""" if self._start_time is not None: raise TimerError(f"Timer is running. Use .stop() to stop it") self._start_time = time.perf_counter() def stop(self) -> float: """Stop the timer, and report the elapsed time""" if self._start_time is None: raise TimerError(f"Timer is not running. Use .start() to start it") # Calculate elapsed time elapsed_time = time.perf_counter() - self._start_time self._start_time = None # Report elapsed time if self.logger: self.logger(self.text.format(elapsed_time)) if self.name: self.timers[self.name] += elapsed_time return elapsed_time def __enter__(self) -> "Timer": """Start a new timer as a context manager""" self.start() return self def __exit__(self, *exc_info: Any) -> None: """Stop the context manager timer""" self.stop()
可以自己使用代碼,方法是將其保存到一個名為的文件中timer.py
并將其導入:
from timer import Timer
PyPI 上也提供了 Timer,因此更簡單的選擇是使用 pip 安裝它:
pip install codetiming
注意,PyPI 上的包名稱是codetiming
,安裝包和導入時都需要使用此名稱Timer
:
from codetiming import Timer
除了名稱和一些附加功能之外,codetiming.Timer
與 timer.Timer
完全一樣。總而言之,可以通過三種不同的方式使用 Timer
:
1. 作為一個類:
t = Timer(name="class") t.start() # Do something t.stop()
2. 作為上下文管理器:
with Timer(name="context manager"): # Do something
3. 作為裝飾器:
@Timer(name="decorator") def stuff(): # Do something
這種 Python 計時器主要用于監控代碼在單個關鍵代碼塊或函數上所花費的時間。
使用 Python 對代碼進行計時有很多選擇。這里我們學習了如何創建一個靈活方便的類,可以通過多種不同的方式使用該類。對 PyPI 的快速搜索發現,已經有許多項目提供 Python 計時器解決方案。
在本節中,我們首先了解有關標準庫中用于測量時間的不同函數的更多信息,包括為什么 perf_counter()
更好,然后探索優化代碼的替代方案。
在本文之前,包括前面介紹python定時器的文章中,我們一直在使用 perf_counter()
來進行實際的時間測量,但是 Python 的時間庫附帶了幾個其他也可以測量時間的函數。這里有一些:
time()
perf_counter_ns()
monotonic()
process_time()
擁有多個函數的一個原因是 Python 將時間表示為浮點數。浮點數本質上是不準確的。之前可能已經看到過這樣的結果:
>>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> 0.1 + 0.1 + 0.1 == 0.3 False
Python 的 Float 遵循 IEEE 754 浮點算術標準,該標準以 64 位表示所有浮點數。因為浮點數有無限多位數,即不能用有限的位數來表達它們。
考慮time()
這個函數的主要目的,是它表示的是現在的實際時間。它以自給定時間點(稱為紀元)以來的秒數來表示函數。time()
返回的數字很大,這意味著可用的數字較少,因而分辨率會受到影響。簡而言之, time()
無法測量納秒級差異:
>>> import time >>> t = time.time() >>> t 1564342757.0654016 >>> t + 1e-9 1564342757.0654016 >>> t == t + 1e-9 True
一納秒是十億分之一秒。上面代碼中,將納秒添加到參數 t ,他并不會影響結果。與 time() 不同的是,perf_counter()
使用一些未定義的時間點作為它的紀元,它可以使用更小的數字,從而獲得更好的分辨率:
>>> import time >>> p = time.perf_counter() >>> p 11370.015653846 >>> p + 1e-9 11370.015653847 >>> p == p + 1e-9 False
眾所周知,將時間表示為浮點數是非常具有挑戰的一件事,因此 Python 3.7 引入了一個新選項:每個時間測量函數現在都有一個相應的 _ns
函數,它以 int
形式返回納秒數,而不是以浮點數形式返回秒數。例如,time()
現在有一個名為 time_ns()
的納秒對應項:
import time time.time_ns()
1564342792866601283
整數在 Python 中是無界的,因此 time_ns()
可以為所有永恒提供納秒級分辨率。同樣,perf_counter_ns()
是 perf_counter()
的納秒版本:
>>> import time >>> time.perf_counter() 13580.153084446 >>> time.perf_counter_ns() 13580765666638
我們注意到,因為 perf_counter()
已經提供納秒級分辨率,所以使用 perf_counter_ns()
的優勢較少。
注意: perf_counter_ns()
僅在 Python 3.7 及更高版本中可用。在 Timer 類中使用了 perf_counter()
。這樣,也可以在較舊的 Python 版本上使用 Timer。
有兩個函數time
不測量time.sleep時間:process_time()
和thread_time()。
通常希望Timer
能夠測量代碼所花費的全部時間,因此這兩個函數并不常用。而函數 monotonic()
,顧名思義,它是一個單調計時器,一個永遠不會向后移動的 Python 計時器。
除了 time()
之外,所有這些函數都是單調的,如果調整了系統時間,它也隨之倒退。在某些系統上,monotonic()
與 perf_counter()
的功能相同,可以互換使用。我們可以使用 time.get_clock_info()
獲取有關 Python 計時器函數的更多信息:
>>> import time >>> time.get_clock_info("monotonic") namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09) >>> time.get_clock_info("perf_counter") namespace(adjustable=False, implementation='clock_gettime(CLOCK_MONOTONIC)', monotonic=True, resolution=1e-09)
注意,不同系統上的結果可能會有所不同。
PEP 418 描述了引入這些功能的一些基本原理。它包括以下簡短描述:
time.monotonic()
: 超時和調度,不受系統時鐘更新影響
time.perf_counter()
:基準測試,短期內最精確的時鐘
time.process_time()
:分析進程的CPU時間
在實際工作中,通常會想優化代碼進一步提升代碼性能,例如想知道將列表轉換為集合的最有效方法。下面我們使用函數 set()
和直接花括號定義集合 {...}
進行比較,看看這兩種方法哪個性能更優,此時需要使用 Python 計時器來比較兩者的運行速度。
>>> from timer import Timer >>> numbers = [7, 6, 1, 4, 1, 8, 0, 6] >>> with Timer(text="{:.8f}"): ... set(numbers) ... {0, 1, 4, 6, 7, 8} 0.00007373 >>> with Timer(text="{:.8f}"): ... {*numbers} ... {0, 1, 4, 6, 7, 8} 0.00006204
該測試結果表明直接花括號定義集合可能會稍微快一些,但其實這些結果非常不確定。如果重新運行代碼,可能會得到截然不同的結果。因為這會受計算機的性能和計算機運行狀態所影響:例如當計算機忙于其他任務時,就會影響我們程序的結果。
更好的方法是多次重復運行相同過程,并獲取平均耗時,就能夠更加精確地測量目標程序的性能大小。因此可以使用 timeit 標準庫,它旨在精確測量小代碼片段的執行時間。雖然可以從 Python 導入和調用 timeit.timeit()
作為常規函數,但使用命令行界面通常更方便。可以按如下方式對這兩種變體進行計時:
$ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "set(nums)" 2000000 loops, best of 5: 163 nsec per loop $ python -m timeit --setup "nums = [7, 6, 1, 4, 1, 8, 0, 6]" "{*nums}" 2000000 loops, best of 5: 121 nsec per loop
timeit
自動多次調用代碼以平均噪聲測量。timeit
的結果證實 {*nums}
量比 set(nums)
快。
注意:在下載文件或訪問數據庫的代碼上使用 timeit
時要小心。由于 timeit
會自動多次調用程序,因此可能會無意中向服務器發送請求!
最后,IPython 交互式 shell
和 Jupyter Notebook
使用 %timeit
魔術命令對此功能提供了額外支持:
In [1]: numbers = [7, 6, 1, 4, 1, 8, 0, 6] In [2]: %timeit set(numbers) 171 ns ± 0.748 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) In [3]: %timeit {*numbers} 147 ns ± 2.62 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
同樣,測量結果表明直接花括號定義集合更快。在 Jupyter Notebooks
中,還可以使用 %%timeit cell-magic
來測量運行整個單元格的時間。
timeit
非常適合對特定代碼片段進行基準測試。但使用它來檢查程序的所有部分并找出哪些部分花費的時間最多會非常麻煩。此時我們想到可以使用分析器。
cProfile 是一個分析器,可以隨時從標準庫中訪問它。可以通過多種方式使用它,盡管將其用作命令行工具通常是最直接的:
$ python -m cProfile -o download_data.prof download_data.py
此命令在打開分析器的情況下運行 download_data.py。將 cProfile 的輸出保存在 download_data.prof 中,由 -o 選項指定。輸出數據是二進制格式,需要專門的程序才能理解。同樣,Python 在標準庫中有一個選項 pstats!它可以在 .prof
文件上運行 pstats
模塊會打開一個交互式配置文件統計瀏覽器。
$ python -m pstats download_data.prof Welcome to the profile statistics browser. download_data.prof% help ...
要使用 pstats,請在提示符下鍵入命令。通常你會使用 sort
和 stats
命令,strip
可以獲得更清晰的輸出:
download_data.prof% strip download_data.prof% sort cumtime download_data.prof% stats 10 ...
此輸出顯示總運行時間為 0.586 秒。它還列出了代碼花費最多時間的十個函數。這里按累積時間 ( cumtime
) 排序,這意味著當給定函數調用另一個函數時,代碼會計算時間。
總時間 ( tottime
) 列表示代碼在函數中花費了多少時間,不包括在子函數中的時間。要查找代碼花費最多時間的位置,需要發出另一個sort
命令:
download_data.prof% sort tottime download_data.prof% stats 10 ...
可以使用 pstats了解代碼大部分時間花在哪里,然后嘗試優化我們發現的任何瓶頸。還可以使用該工具更好地理解代碼的結構。例如,被調用者和調用者命令將顯示給定函數調用和調用的函數。
還可以研究某些函數。通過使用短語 timer
過濾結果來檢查 Timer
導致的開銷:
download_data.prof% stats timer ...
完成調查后,使用 quit
離開 pstats
瀏覽器。
如需更加深入了解更強大的配置文件數據接口,可以查看 KCacheGrind[8]。它使用自己的數據格式,也可以使用 pyprof2calltree 從 cProfile 轉換數據:
$ pyprof2calltree -k -i download_data.prof
該命令將轉換 download_data.prof
并打開 KCacheGrind
來分析數據。
這里為代碼計時的最后一個選項是 line_profiler。cProfile
可以告訴我們代碼在哪些函數中花費的時間最多,但它不會深入顯示該函數中的哪些行最慢,此時就需要 line_profiler
。
注意:還可以分析代碼的內存消耗。這超出了本教程的范圍,如果你需要監控程序的內存消耗,可以查看 memory-profiler。
行分析需要時間,并且會為我們的運行時增加相當多的開銷。正常的工作流程是首先使用 cProfile
來確定要調查的函數,然后在這些函數上運行 line_profiler
。line_profiler
不是標準庫的一部分,因此應該首先按照安裝說明進行設置。
在運行分析器之前,需要告訴它要分析哪些函數。可以通過在源代碼中添加 @profile
裝飾器來實現。例如,要分析 Timer.stop()
,在 timer.py
中添加以下內容:
@profile def stop(self) -> float: # 其余部分不變
注意,不需要導入profile
配置文件,它會在運行分析器時自動添加到全局命名空間中。不過,我們需要在完成分析后刪除該行。否則,會拋出一個 NameError 異常。
接下來,使用 kernprof 運行分析器,它是 line_profiler
包的一部分:
$ kernprof -l download_data.py
此命令自動將探查器數據保存在名為 download_data.py.lprof
的文件中。可以使用 line_profiler
查看這些結果:
$ python -m line_profiler download_data.py.lprof Timer unit: 1e-06 s Total time: 1.6e-05 s File: /home/realpython/timer.py Function: stop at line 35 # Hits Time PrHit %Time Line Contents ===================================== ...
首先,注意本報告中的時間單位是微秒(1e-06 s
)。通常,最容易查看的數字是 %Time
,它告訴我們代碼在每一行的函數中花費的總時間的百分比。
關于“怎么使用裝飾器擴展Python中的計時器功能”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。