<button id="2ddn7"><acronym id="2ddn7"></acronym></button>

<button id="2ddn7"><acronym id="2ddn7"></acronym></button>

  • <button id="2ddn7"><object id="2ddn7"></object></button>
      1. <s id="2ddn7"></s>
        更多課程 選擇中心


        Python培訓

        400-111-8989

        用Python 編寫的 Python 解釋器,確定嗎?

        • 發布:Allison Kaptur
        • 來源:Python開發者
        • 時間:2018-04-24 15:32

        你在閱讀完“用Python 編寫的 Python 解釋器”時,你心中出現的符號是句號、問號還是嘆號?帶著你的所思所想,我們來閱讀本文:Byterun是一個用Python實現的Python解釋器,盡管Byterun很小,但它能執行大多數簡單的Python程序。這個Python解釋器的基礎結構可以滿足500行的限制。在這一章我們會搞清楚這個解釋器的結構,給你足夠的知識探索下去。

        A Python Interpreter

        在開始之前,讓我們縮小一下“Pyhton解釋器”的意思。在討論Python的時候,“解釋器”這個詞可以用在很多不同的地方。有的時候解釋器指的是REPL,當你在命令行下敲下python時所得到的交互式環境。有時候人們會相互替代的使用Python解釋器和Python來說明執行Python代碼的這一過程。在本章,“解釋器”有一個更精確的意思:執行Python程序過程中的最后一步。

        在解釋器接手之前,Python會執行其他3個步驟:詞法分析,語法解析和編譯。這三步合起來把源代碼轉換成code object,它包含著解釋器可以理解的指令。而解釋器的工作就是解釋code object中的指令。

        你可能很奇怪執行Python代碼會有編譯這一步。Python通常被稱為解釋型語言,就像Ruby,Perl一樣,它們和編譯型語言相對,比如C,Rust。然而,這里的術語并不是它看起來的那樣精確。大多數解釋型語言包括Python,確實會有編譯這一步。而Python被稱為解釋型的原因是相對于編譯型語言,它在編譯這一步的工作相對較少(解釋器做相對多的工作)。在這章后面你會看到,Python的編譯器比C語言編譯器需要更少的關于程序行為的信息。

        A Python Python Interpreter

        Byterun是一個用Python寫的Python解釋器,這點可能讓你感到奇怪,但沒有比用C語言寫C語言編譯器更奇怪。(事實上,廣泛使用的gcc編譯器就是用C語言本身寫的)你可以用幾乎的任何語言寫一個Python解釋器。

        用Python寫Python既有優點又有缺點。最大的缺點就是速度:用Byterun執行代碼要比用CPython執行慢的多,CPython解釋器是用C語言實現的并做了優化。然而Byterun是為了學習而設計的,所以速度對我們不重要。使用Python最大優點是我們可以僅僅實現解釋器,而不用擔心Python運行時的部分,特別是對象系統。比如當Byterun需要創建一個類時,它就會回退到“真正”的Python。另外一個優勢是Byterun很容易理解,部分原因是它是用高級語言寫的(Python!)(另外我們不會對解釋器做優化 — 再一次,清晰和簡單比速度更重要)

        Building an Interpreter

        在我們考察Byterun代碼之前,我們需要一些對解釋器結構的高層次視角。Python解釋器是如何工作的?

        Python解釋器是一個虛擬機,模擬真實計算機的軟件。我們這個虛擬機是棧機器,它用幾個棧來完成操作(與之相對的是寄存器機器,它從特定的內存地址讀寫數據)。

        Python解釋器是一個字節碼解釋器:它的輸入是一些命令集合稱作字節碼。當你寫Python代碼時,詞法分析器,語法解析器和編譯器生成code object讓解釋器去操作。每個code object都包含一個要被執行的指令集合 — 它就是字節碼 — 另外還有一些解釋器需要的信息。字節碼是Python代碼的一個中間層表示:它以一種解釋器可以理解的方式來表示源代碼。這和匯編語言作為C語言和機器語言的中間表示很類似。

        A Tiny Interpreter

        為了讓說明更具體,讓我們從一個非常小的解釋器開始。它只能計算兩個數的和,只能理解三個指令。它執行的所有代碼只是這三個指令的不同組合。下面就是這三個指令:

        LOAD_VALUE

        ADD_TWO_VALUES

        PRINT_ANSWER

        我們不關心詞法,語法和編譯,所以我們也不在乎這些指令是如何產生的。你可以想象,你寫下7 + 5,然后一個編譯器為你生成那三個指令的組合。如果你有一個合適的編譯器,你甚至可以用Lisp的語法來寫,只要它能生成相同的指令。

        假設

        7 + 5

        生成這樣的指令集:

        Python解釋器是一個棧機器,所以它必須通過操作棧來完成這個加法。(Figure 1.1)解釋器先執行第一條指令,LOAD_VALUE,把第一個數壓到棧中。接著它把第二個數也壓到棧中。然后,第三條指令,ADD_TWO_VALUES,先把兩個數從棧中彈出,加起來,再把結果壓入棧中。最后一步,把結果彈出并輸出。

        LOAD_VALUE這條指令告訴解釋器把一個數壓入棧中,但指令本身并沒有指明這個數是多少。指令需要一個額外的信息告訴解釋器去哪里找到這個數。所以我們的指令集有兩個部分:指令本身和一個常量列表。(在Python中,字節碼就是我們稱為的“指令”,而解釋器執行的是code object。)

        為什么不把數字直接嵌入指令之中?想象一下,如果我們加的不是數字,而是字符串。我們可不想把字符串這樣的東西加到指令中,因為它可以有任意的長度。另外,我們這種設計也意味著我們只需要對象的一份拷貝,比如這個加法 7 + 7, 現在常量表 "numbers"只需包含一個7。

        你可能會想為什么會需要除了ADD_TWO_VALUES之外的指令。的確,對于我們兩個數加法,這個例子是有點人為制作的意思。然而,這個指令卻是建造更復雜程序的輪子。比如,就我們目前定義的三個指令,只要給出正確的指令組合,我們可以做三個數的加法,或者任意個數的加法。同時,棧提供了一個清晰的方法去跟蹤解釋器的狀態,這為我們增長的復雜性提供了支持。

        現在讓我們來完成我們的解釋器。解釋器對象需要一個棧,它可以用一個列表來表示。它還需要一個方法來描述怎樣執行每條指令。比如,LOAD_VALUE會把一個值壓入棧中。

        這三個方法完成了解釋器所理解的三條指令。但解釋器還需要一樣東西:一個能把所有東西結合在一起并執行的方法。這個方法就叫做run_code, 它把我們前面定義的字典結構what-to-execute作為參數,循環執行里面的每條指令,如何指令有參數,處理參數,然后調用解釋器對象中相應的方法。

        為了測試,我們創建一個解釋器對象,然后用前面定義的 7 + 5 的指令集來調用run_code。

        顯然,它會輸出12

        盡管我們的解釋器功能受限,但這個加法過程幾乎和真正的Python解釋器是一樣的。這里,我們還有幾點要注意。

        首先,一些指令需要參數。在真正的Python bytecode中,大概有一半的指令有參數。像我們的例子一樣,參數和指令打包在一起。注意指令的參數和傳遞給對應方法的參數是不同的。

        第二,指令ADD_TWO_VALUES不需要任何參數,它從解釋器棧中彈出所需的值。這正是以棧為基礎的解釋器的特點。

        記得我們說過只要給出合適的指令集,不需要對解釋器做任何改變,我們做多個數的加法。考慮下面的指令集,你覺得會發生什么?如果你有一個合適的編譯器,什么代碼才能編譯出下面的指令集?

        從這點出發,我們開始看到這種結構的可擴展性:我們可以通過向解釋器對象增加方法來描述更多的操作(只要有一個編譯器能為我們生成組織良好的指令集)。

        Variables

        接下來給我們的解釋器增加變量的支持。我們需要一個保存變量值的指令,STORE_NAME;一個取變量值的指令LOAD_NAME;和一個變量到值的映射關系。目前,我們會忽略命名空間和作用域,所以我們可以把變量和值的映射直接存儲在解釋器對象中。最后,我們要保證what_to_execute除了一個常量列表,還要有個變量名字的列表。

        我們的新的的實現在下面。為了跟蹤哪名字綁定到那個值,我們在__init__方法中增加一個environment字典。我們也增加了STORE_NAME和LOAD_NAME方法,它們獲得變量名,然后從environment字典中設置或取出這個變量值。

        現在指令參數就有兩個不同的意思,它可能是numbers列表的索引,也可能是names列表的索引。解釋器通過檢查所執行的指令就能知道是那種參數。而我們打破這種邏輯 ,把指令和它所用何種參數的映射關系放在另一個單獨的方法中。

        僅僅五個指令,run_code這個方法已經開始變得冗長了。如果保持這種結構,那么每條指令都需要一個if分支。這里,我們要利用Python的動態方法查找。我們總會給一個稱為FOO的指令定義一個名為FOO的方法,這樣我們就可用Python的getattr函數在運行時動態查找方法,而不用這個大大的分支結構。run_code方法現在是這樣:

        Real Python Bytecode

        現在,放棄我們的小指令集,去看看真正的Python字節碼。字節碼的結構和我們的小解釋器的指令集差不多,除了字節碼用一個字節而不是一個名字來指示這條指令。為了理解它的結構,我們將考察一個函數的字節碼。考慮下面這個例子:

        Python在運行時會暴露一大批內部信息,并且我們可以通過REPL直接訪問這些信息。對于函數對象cond,cond.__code__是與其關聯的code object,而cond.__code__.co_code就是它的字節碼。當你寫Python代碼時,你永遠也不會想直接使用這些屬性,但是這可以讓我們做出各種惡作劇,同時也可以看看內部機制。

        當我們直接輸出這個字節碼,它看起來完全無法理解 — 唯一我們了解的是它是一串字節。很幸運,我們有一個很強大的工具可以用:Python標準庫中的dis module。

        dis是一個字節碼反匯編器。反匯編器以為機器而寫的底層代碼作為輸入,比如匯編代碼和字節碼,然后以人類可讀的方式輸出。當我們運行dis.dis, 它輸出每個字節碼的解釋。

        這些都是什么意思?讓我們以第一條指令LOAD_CONST為例子。第一列的數字(2)表示對應源代碼的行數。第二列的數字是字節碼的索引,告訴我們指令LOAD_CONST在0位置。第三列是指令本身對應的人類可讀的名字。如果第四列存在,它表示指令的參數。如果第5列存在,它是一個關于參數是什么的提示。

        考慮這個字節碼的前幾個字節:[100, 1, 0, 125, 0, 0]。這6個字節表示兩條帶參數的指令。我們可以使用dis.opname,一個字節到可讀字符串的映射,來找到指令100和指令125代表是什么:

        第二和第三個字節 — 1 ,0 —是LOAD_CONST的參數,第五和第六個字節 — 0,0 — 是STORE_FAST的參數。就像我們前面的小例子,LOAD_CONST需要知道的到哪去找常量,STORE_FAST需要找到名字。(Python的LOAD_CONST和我們小例子中的LOAD_VALUE一樣,LOAD_FAST和LOAD_NAME一樣)。所以這六個字節代表第一行源代碼x = 3.(為什么用兩個字節表示指令的參數?如果Python使用一個字節,每個code object你只能有256個常量/名字,而用兩個字節,就增加到了256的平方,65536個)。

        Conditionals and Loops

        到目前為止,我們的解釋器只能一條接著一條的執行指令。這有個問題,我們經常會想多次執行某個指令,或者在特定的條件下略過它們。為了可以寫循環和分支結構,解釋器必須能夠在指令中跳轉。在某種程度上,Python在字節碼中使用GOTO語句來處理循環和分支!讓我們再看一個cond函數的反匯編結果:

        第三行的條件表達式if x < 5被編譯成四條指令:LOAD_FAST, LOAD_CONST, COMPARE_OP和 POP_JUMP_IF_FALSE。x < 5對應加載x,加載5,比較這兩個值。指令POP_JUMP_IF_FALSE完成if語句。這條指令把棧頂的值彈出,如果值為真,什么都不發生。如果值為假,解釋器會跳轉到另一條指令。

        這條將被加載的指令稱為跳轉目標,它作為指令POP_JUMP的參數。這里,跳轉目標是22,索引為22的指令是LOAD_CONST,對應源碼的第6行。(dis用>>標記跳轉目標。)如果X < 5為假,解釋器會忽略第四行(return yes),直接跳轉到第6行(return "no")。因此解釋器通過跳轉指令選擇性的執行指令。

        Python的循環也依賴于跳轉。在下面的字節碼中,while x < 5這一行產生了和if x < 10幾乎一樣的字節碼。在這兩種情況下,解釋器都是先執行比較,然后執行POP_JUMP_IF_FALSE來控制下一條執行哪個指令。第四行的最后一條字節碼JUMP_ABSOLUT(循環體結束的地方),讓解釋器返回到循環開始的第9條指令處。當 x < 10變為假,POP_JUMP_IF_FALSE會讓解釋器跳到循環的終止處,第34條指令。

        Explore Bytecode

        我鼓勵你用dis.dis來試試你自己寫的函數。一些有趣的問題值得探索:

        對解釋器而言for循環和while循環有什么不同?

        能不能寫出兩個不同函數,卻能產生相同的字節碼?

        elif是怎么工作的?列表推導呢?

        Frames

        到目前為止,我們已經知道了Python虛擬機是一個棧機器。它能順序執行指令,在指令間跳轉,壓入或彈出棧值。但是這和我們心想的解釋器還有一定距離。在前面的那個例子中,最后一條指令是RETURN_VALUE,它和return語句想對應。但是它返回到哪里去呢?

        為了回答這個問題,我們必須嚴增加一層復雜性:frame。一個frame是一些信息的集合和代碼的執行上下文。frames在Python代碼執行時動態的創建和銷毀。每個frame對應函數的一次調用。— 所以每個frame只有一個code object與之關聯,而一個code object可以很多frame。比如你有一個函數遞歸的調用自己10次,這時有11個frame。總的來說,Python程序的每個作用域有一個frame,比如,每個module,每個函數調用,每個類定義。

        Frame存在于調用棧中,一個和我們之前討論的完全不同的棧。(你最熟悉的棧就是調用棧,就是你經常看到的異常回溯,每個以”File ‘program.py'”開始的回溯對應一個frame。)解釋器在執行字節碼時操作的棧,我們叫它數據棧。其實還有第三個棧,叫做塊棧,用于特定的控制流塊,比如循環和異常處理。調用棧中的每個frame都有它自己的數據棧和塊棧。

        讓我們用一個具體的例子來說明。假設Python解釋器執行到標記為3的地方。解釋器正在foo函數的調用中,它接著調用bar。下面是frame調用棧,塊棧和數據棧的示意圖。我們感興趣的是解釋器先從最底下的foo()開始,接著執行foo的函數體,然后到達bar。

        現在,解釋器在bar函數的調用中。調用棧中有3個fram:一個對應于module層,一個對應函數foo,別一個對應函數bar。(Figure 1.2)一旦bar返回,與它對應的frame就會從調用棧中彈出并丟棄。

        字節碼指令RETURN_VALUE告訴解釋器在frame間傳遞一個值。首先,它把位于調用棧棧頂的frame中的數據棧的棧頂值彈出。然后把整個frame彈出丟棄。最后把這個值壓到下一個frame的數據棧中。

        當Ned Batchelder和我在寫Byterun時,很長一段時間我們的實現中一直有個重大的錯誤。我們整個虛擬機中只有一個數據棧,而不是每個frame都有個一個。我們做了很多測試,同時在Byterun和真正的Python上,為了保證結果一致。我們幾乎通過了所有測試,只有一樣東西不能通過,那就是生成器。最后,通過仔細的閱讀Cpython的源碼,我們發現了錯誤所在。把數據棧移到每個frame就解決了這個問題。

        回頭在看看這個bug,我驚訝的發現Python真的很少依賴于每個frame有一個數據棧這個特性。在Python中幾乎所有的操作都會清空數據棧,所以所有的frame公用一個數據棧是沒問題的。在上面的例子中,當bar執行完后,它的數據棧為空。即使foo公用這一個棧,它的值也不會受影響。然而,對應生成器,一個關鍵的特點是它能暫停一個frame的執行,返回到其他的frame,一段時間后它能返回到原來的frame,并以它離開時的同樣的狀態繼續執行。

        Byterun

        現在我們有足夠的Python解釋器的知識背景去考察Byterun。

        Byterun中有四種對象。

        VirtualMachine類,它管理高層結構,frame調用棧,指令到操作的映射。這是一個比前面Inteprter對象更復雜的版本。

        Frame類,每個Frame類都有一個code object,并且管理者其他一些必要的狀態信息,全局和局部命名空間,指向調用它的frame的指針和最后執行的字節碼指令。

        Function類,它被用來代替真正的Python函數。回想一下,調用函數時會創建一個新的frame。我們自己實現Function,所以我們控制新frame的創建。

        Block類,它只是包裝了代碼塊的3個屬性。(代碼塊的細節不是解釋器的核心,我們不會花時間在它身上,把它列在這里,是因為Byterun需要它。)

        The VirtualMachine Class

        程序運行時只有一個VirtualMachine被創建,因為我們只有一個解釋器。VirtualMachine保存調用棧,異常狀態,在frame中傳遞的返回值。它的入口點是run_code方法,它以編譯后的code object為參數,以創建一個frame為開始,然后運行這個frame。這個frame可能再創建出新的frame;調用棧隨著程序的運行增長縮短。當第一個frame返回時,執行結束。

        The Frame Class

        接下來,我們來寫Frame對象。frame是一個屬性的集合,它沒有任何方法。前面提到過,這些屬性包括由編譯器生成的code object;局部,全局和內置命名空間;前一個frame的引用;一個數據棧;一個塊棧;最后執行的指令。(對于內置命名空間我們需要多做一點工作,Python對待這個命名空間不同;但這個細節對我們的虛擬機不重要。)

        接著,我們在虛擬機中增加對frame的操作。這有3個幫助函數:一個創建新的frame的方法,和壓棧和出棧的方法。第四個函數,run_frame,完成執行frame的主要工作,待會我們再討論這個方法。

        The Function Class

        Function的實現有點扭曲,但是大部分的細節對理解解釋器不重要。重要的是當調用函數時 — __call__方法被調用 — 它創建一個新的Frame并運行它。

        接著,回到VirtualMachine對象,我們對數據棧的操作也增加一些幫助方法。字節碼操作的棧總是在當前frame的數據棧。這讓我們能完成POP_TOP,LOAD_FAST,并且讓其他操作棧的指令可讀性更高。

        在我們運行frame之前,我們還需兩個方法。

        第一個方法,parse_byte_and_args,以一個字節碼為輸入,先檢查它是否有參數,如果有,就解析它的參數。這個方法同時也更新frame的last_instruction屬性,它指向最后執行的指令。一條沒有參數的指令只有一個字節長度,而有參數的字節有3個字節長。參數的意義依賴于指令是什么。比如,前面說過,指令POP_JUMP_IF_FALSE,它的參數指的是跳轉目標。BUILD_LIST, 它的參數是列表的個數。LOAD_CONST,它的參數是常量的索引。

        一些指令用簡單的數字作為參數。對于另一些,虛擬機需要一點努力去發現它意義。標準庫中的dismodule中有一個備忘單解釋什么參數有什么意思,這讓我們的代碼更加簡潔。比如,列表dis.hasname告訴我們LOAD_NAME, IMPORT_NAME,LOAD_GLOBAL,和另外的9個指令都有同樣的意思:名字列表的索引。

        下一個方法是dispatch,它查看給定的指令并執行相應的操作。在CPython中,這個分派函數用一個巨大的switch語句完成,有超過1500行的代碼。幸運的是,我們用的是Python,我們的代碼會簡潔的多。我們會為每一個字節碼名字定義一個方法,然后用getattr來查找。就像我們前面的小解釋器一樣,如果一條指令叫做FOO_BAR,那么它對應的方法就是byte_FOO_BAR。現在,我們先把這些方法當做一個黑盒子。每個指令方法都會返回None或者一個字符串why,有些情況下虛擬機需要這個額外why信息。這些指令方法的返回值,僅作為解釋器狀態的內部指示,千萬不要和執行frame的返回值相混淆。

        The Block Class

        在我們完成每個字節碼方法前,我們簡單的討論一下塊。一個塊被用于某種控制流,特別是異常處理和循環。它負責保證當操作完成后數據棧處于正確的狀態。比如,在一個循環中,一個特殊的迭代器會存在棧中,當循環完成時它從棧中彈出。解釋器需要檢查循環仍在繼續還是已經停止。

        為了跟蹤這些額外的信息,解釋器設置了一個標志來指示它的狀態。我們用一個變量why實現這個標志,它可以None或者是下面幾個字符串這一,"continue", "break","excption",return。他們指示對塊棧和數據棧進行什么操作。回到我們迭代器的例子,如果塊棧的棧頂是一個loop塊,why是continue,迭代器就因該保存在數據棧上,不是如果why是break,迭代器就會被彈出。

        塊操作的細節比較精密,我們不會花時間在這上面,但是有興趣的讀者值得仔細的看看。

        The Instructions

        剩下了的就是完成那些指令方法了:byte_LOAD_FAST,byte_BINARY_MODULO等等。而這些指令的實現并不是很有趣,這里我們只展示了一小部分,完整的實現在這兒(足夠執行我們前面所述的所有代碼了。)

        Dynamic Typing: What the Compiler Doesn’t Know

        你可能聽過Python是一種動態語言 — 是它是動態類型的。在我們建造解釋器的過程中,已經流露出這個描述。

        動態的一個意思是很多工作在運行時完成。前面我們看到Python的編譯器沒有很多關于代碼真正做什么的信息。舉個例子,考慮下面這個簡單的函數mod。它區兩個參數,返回它們的模運算值。從它的字節碼中,我們看到變量a和b首先被加載,然后字節碼BINAY_MODULO完成這個模運算。

        計算19 % 5得4,— 一點也不奇怪。如果我們用不同類的參數呢?

        剛才發生了什么?你可能見過這樣的語法,格式化字符串。

        用符號%去格式化字符串會調用字節碼BUNARY_MODULO.它取棧頂的兩個值求模,不管這兩個值是字符串,數字或是你自己定義的類的實例。字節碼在函數編譯時生成(或者說,函數定義時)相同的字節碼會用于不同類的參數。

        Python的編譯器關于字節碼的功能知道的很少。而取決于解釋器來決定BINAYR_MODULO應用于什么類型的對象并完成真確的操作。這就是為什么Python被描述為動態類型:直到你運行前你不必知道這個函數參數的類型。相反,在一個靜態類型語言中,程序員需要告訴編譯器參數的類型是什么(或者編譯器自己推斷出參數的類型。)

        編譯器的無知是優化Python的一個挑戰 — 只看字節碼,而不真正運行它,你就不知道每條字節碼在干什么!你可以定義一個類,實現__mod__方法,當你對這個類的實例使用%時,Python就會自動調用這個方法。所以,BINARY_MODULO其實可以運行任何代碼。

        看看下面的代碼,第一個a % b看起來沒有用。

        不幸的是,對這段代碼進行靜態分析 — 不運行它 — 不能確定第一個a % b沒有做任何事。用 %調用__mod__可能會寫一個文件,或是和程序的其他部分交互,或者其他任何可以在Python中完成的事。很難優化一個你不知道它會做什么的函數。在Russell Power和Alex Rubinsteyn的優秀論文中寫道,“我們可用多快的速度解釋Python?”,他們說,“在普遍缺乏類型信息下,每條指令必須被看作一個INVOKE_ARBITRARY_METHOD。”

        Conclusion

        恭喜你閱讀完了“用Python 編寫的 Python 解釋器,確定嗎?”這篇文章,你確定了嗎?不知道你現在是想要逗號、句號、問號還是嘆號來表達你的想法?Byterun是一個比CPython容易理解的簡潔的Python解釋器。Byterun復制了CPython的主要結構:一個基于棧的指令集稱為字節碼,它們順序執行或在指令間跳轉,向棧中壓入和從中彈出數據。解釋器隨著函數和生成器的調用和返回,動態的創建,銷毀frame,并在frame間跳轉。Byterun也有著和真正解釋器一樣的限制:因為Python使用動態類型,解釋器必須在運行時決定指令的正確行為。我鼓勵你去反匯編你的程序,然后用Byterun來運行。你很快會發現這個縮短版的Byterun所沒有實現的指令。完整的實現在這里或者仔細閱讀真正的CPython解釋器ceval.c,你也可以實現自己的解釋器!還想了解更多關于python的知識,解決python方面的相關疑惑,來達內python培訓班獲取吧!

        免責聲明:內容和圖片源自網絡,版權歸原作者所有,如有侵犯您的原創版權請告知,我們將盡快刪除相關內容。

        預約申請免費試聽課

        填寫下面表單即可預約申請免費試聽!怕錢不夠?可就業掙錢后再付學費! 怕學不會?助教全程陪讀,隨時解惑!擔心就業?一地學習,可全國推薦就業!

        上一篇:人工智能時代已近,還不了解Python數據類型的就趕緊來學!?
        下一篇:區塊鏈就是這么簡單,用Python就可以從零開始創建區塊鏈!

        如何運用Python編程處理大數據?用Python編程處理大數據的技巧是什么?

        Python面向對象編程的知識點都在這了!

        Python的高級特征及用法(部分)

        聽說這些Python知識,很少有人知道!

        • 掃碼領取資料

          回復關鍵字:視頻資料

          免費領取 達內課程視頻學習資料

        • 視頻學習QQ群

          添加QQ群:1143617948

          免費領取達內課程視頻學習資料

        Copyright ? 2021 Tedu.cn All Rights Reserved 京ICP備08000853號-56 京公網安備 11010802029508號 達內時代科技集團有限公司 版權所有

        選擇城市和中心
        黑龍江省

        吉林省

        河北省

        湖南省

        貴州省

        云南省

        廣西省

        海南省

        青青青草网站免费观看|青青青视频在线观看 超真实强奷视频在线看 百度 好搜 搜狗
        <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>