Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
者 | qcrao
責編 | 屠敏
出品 | CSDN博客
剛開始寫這篇文章的時候,目標非常大,想要探索 Go 程序的一生:編碼、編譯、匯編、鏈接、運行、退出。它的每一步具體如何進行,力圖弄清 Go 程序的這一生。
在這個過程中,我又復習了一遍《程序員的自我修養》。這是一本講編譯、鏈接的書,非常詳細,值得一看!數年前,我第一次看到這本書的書名,就非常喜歡。因為它模仿了周星馳喜劇之王里出現的一本書 ——《演員的自我修養》。心向往之!
在開始本文之前,先推薦一位頭條大佬的博客——《面向信仰編程》,他的 Go 編譯系列文章,非常有深度,直接深入編譯器源代碼,我是看了很多遍了。博客鏈接可以從參考資料里獲取。
理想很大,實現的難度也是非常大。為了避免砸了“深度解密”這個牌子,這次起了個更溫和的名字。
下面是文章的目錄:
引入
我們從一個 HelloWorld 的例子開始:
package main
import "fmt"
funcmain{
fmt.Println("hello world")
}
當我用我那價值 1800 元的 cherry 鍵盤瀟灑地敲完上面的 hello world 代碼時,保存在硬盤上的 hello.go 文件就是一個字節序列了,每個字節代表一個字符。
用 vim 打開 hello.go 文件,在命令行模式下,輸入命令:
:%!xxd
就能在 vim 里以十六進制查看文件內容:
最左邊的一列代表地址值,中間一列代表文本對應的 ASCII 字符,最右邊的列就是我們的代碼。再在終端里執行 man ascii:
和 ASCII 字符表一對比,就能發現,中間的列和最右邊的列是一一對應的。也就是說,剛剛寫完的 hello.go 文件都是由 ASCII 字符表示的,它被稱為 文本文件,其他文件被稱為 二進制文件。
當然,更深入地看,計算機中的所有數據,像磁盤文件、網絡中的數據其實都是一串比特位組成,取決于如何看待它。在不同的情景下,一個相同的字節序列可能表示成一個整數、浮點數、字符串或者是機器指令。
而像 hello.go 這個文件,8 個 bit,也就是一個字節看成一個單位(假定源程序的字符都是 ASCII 碼),最終解釋成人類能讀懂的 Go 源碼。
Go 程序并不能直接運行,每條 Go 語句必須轉化為一系列的低級機器語言指令,將這些指令打包到一起,并以二進制磁盤文件的形式存儲起來,也就是可執行目標文件。
從源文件到可執行目標文件的轉化過程:
完成以上各個階段的就是 Go 編譯系統。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名為 GNU 編譯器套裝,它支持像 C,C++,Java,Python,Objective-C,Ada,Fortran,Pascal,能夠為很多不同的機器生成機器碼。
可執行目標文件可以直接在機器上執行。一般而言,先執行一些初始化的工作;找到 main 函數的入口,執行用戶寫的代碼;執行完成后,main 函數退出;再執行一些收尾的工作,整個過程完畢。
在接下來的文章里,我們將探索 編譯和 運行的過程。
編譯鏈接概述
Go 源碼里的編譯器源碼位于 src/cmd/compile 路徑下,鏈接器源碼位于 src/cmd/link 路徑下。
編譯過程
我比較喜歡用 IDE(集成開發環境)來寫代碼, Go 源碼用的 Goland,有時候直接點擊 IDE 菜單欄里的“運行”按鈕,程序就跑起來了。這實際上隱含了編譯和鏈接的過程,我們通常將編譯和鏈接合并到一起的過程稱為構建(Build)。
編譯過程就是對源文件進行詞法分析、語法分析、語義分析、優化,最后生成匯編代碼文件,以 .s 作為文件后綴。
之后,匯編器會將匯編代碼轉變成機器可以執行的指令。由于每一條匯編語句幾乎都與一條機器指令相對應,所以只是一個簡單的一一對應,比較簡單,沒有語法、語義分析,也沒有優化這些步驟。
編譯器是將高級語言翻譯成機器語言的一個工具,編譯過程一般分為 6 步:掃描、語法分析、語義分析、源代碼優化、代碼生成、目標代碼優化。下圖來自《程序員的自我修養》:
詞法分析
通過前面的例子,我們知道,Go 程序文件在機器看來不過是一堆二進制位。我們能讀懂,是因為 Goland 按照 ASCII 碼(實際上是 UTF-8)把這堆二進制位進行了編碼。例如,把 8個 bit 位分成一組,對應一個字符,通過對照 ASCII 碼表就可以查出來。
當把所有的二進制位都對應成了 ASCII 碼字符后,我們就能看到有意義的字符串。它可能是關鍵字,例如:package;可能是字符串,例如:“Hello World”。
詞法分析其實干的就是這個。輸入是原始的 Go 程序文件,在詞法分析器看來,就是一堆二進制位,根本不知道是什么東西,經過它的分析后,變成有意義的記號。簡單來說,詞法分析是計算機科學中將字符序列轉換為標記(token)序列的過程。
我們來看一下維基百科上給出的定義:
詞法分析(lexical analysis)是計算機科學中將字符序列轉換為標記(token)序列的過程。進行詞法分析的程序或者函數叫作詞法分析器(lexical analyzer,簡稱lexer),也叫掃描器(scanner)。詞法分析器一般以函數的形式存在,供語法分析器調用。
.go 文件被輸入到掃描器(Scanner),它使用一種類似于 有限狀態機的算法,將源代碼的字符系列分割成一系列的記號(Token)。
記號一般分為這幾類:關鍵字、標識符、字面量(包含數字、字符串)、特殊符號(如加號、等號)。
例如,對于如下的代碼:
slice[i] = i * (2 + 6)
總共包含 16 個非空字符,經過掃描后:
記號 | 類型 |
---|---|
slice | 標識符 |
[ | 左方括號 |
i | 標識符 |
] | 右方括號 |
= | 賦值 |
i | 標識符 |
* | 乘號 |
( | 左圓括號 |
2 | 數字 |
+ | 加號 |
6 | 數字 |
) | 右圓括號 |
上面的例子源自《程序員的自我修養》,主要講解編譯、鏈接相關的內容,很精彩,推薦研讀。
Go 語言(本文的 Go 版本是 1.9.2)掃描器支持的 Token 在源碼中的路徑:
src/cmd/compile/internal/syntax/token.go
感受一下:
var tokstrings = [...]string{
// source control
_EOF: "EOF",
// names and literals
_Name: "name",
_Literal: "literal",
// operators and operations
_Operator: "op",
_AssignOp: "op=",
_IncOp: "opop",
_Assign: "=",
_Define: ":=",
_Arrow: "<-",
_Star: "*",
// delimitors
_Lparen: "(",
_Lbrack: "[",
_Lbrace: "{",
_Rparen: ")",
_Rbrack: "]",
_Rbrace: "}",
_Comma: ",",
_Semi: ";",
_Colon: ":",
_Dot: ".",
_DotDotDot: "...",
// keywords
_Break: "break",
_Case: "case",
_Chan: "chan",
_Const: "const",
_Continue: "continue",
_Default: "default",
_Defer: "defer",
_Else: "else",
_Fallthrough: "fallthrough",
_For: "for",
_Func: "func",
_Go: "go",
_Goto: "goto",
_If: "if",
_Import: "import",
_Interface: "interface",
_Map: "map",
_Package: "package",
_Range: "range",
_Return: "return",
_Select: "select",
_Struct: "struct",
_Switch: "switch",
_Type: "type",
_Var: "var",
}
還是比較熟悉的,包括名稱和字面量、操作符、分隔符和關鍵字。
而掃描器的路徑是:
src/cmd/compile/internal/syntax/scanner.go
其中最關鍵的函數就是 next 函數,它不斷地讀取下一個字符(不是下一個字節,因為 Go 語言支持 Unicode 編碼,并不是像我們前面舉得 ASCII 碼的例子,一個字符只有一個字節),直到這些字符可以構成一個 Token。
func (s *scanner) next{
// ……
redo:
// skip white space
c := s.getr
for c == ' ' || c == '\t' || c == '\n' && !nlsemi || c == '\r' {
c = s.getr
}
// token start
s.line, s.col = s.source.line0, s.source.col0
if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) {
s.ident
return
}
switch c {
// ……
case '\n':
s.lit = "newline"
s.tok = _Semi
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
s.number(c)
// ……
default:
s.tok = 0
s.error(fmt.Sprintf("invalid character %#U", c))
goto redo
return
assignop:
if c == '=' {
s.tok = _AssignOp
return
}
s.ungetr
s.tok = _Operator
}
代碼的主要邏輯就是通過 c:=s.getr 獲取下一個未被解析的字符,并且會跳過之后的空格、回車、換行、tab 字符,然后進入一個大的 switch-case 語句,匹配各種不同的情形,最終可以解析出一個 Token,并且把相關的行、列數字記錄下來,這樣就完成一次解析過程。
當前包中的詞法分析器 scanner 也只是為上層提供了 next 方法,詞法解析的過程都是惰性的,只有在上層的解析器需要時才會調用 next 獲取最新的 Token。
語法分析
上一步生成的 Token 序列,需要經過進一步處理,生成一棵以 表達式為結點的 語法樹。
比如最開始的那個例子, slice[i]=i*(2+6),得到的一棵語法樹如下:
整個語句被看作是一個賦值表達式,左子樹是一個數組表達式,右子樹是一個乘法表達式;數組表達式由 2 個符號表達式組成;乘號表達式則是由一個符號表達式和一個加號表達式組成;加號表達式則是由兩個數字組成。符號和數字是最小的表達式,它們不能再被分解,通常作為樹的葉子節點。
語法分析的過程可以檢測一些形式上的錯誤,例如:括號是否缺少一半, + 號表達式缺少一個操作數等。
語法分析是根據某種特定的形式文法(Grammar)對 Token 序列構成的輸入文本進行分析并確定其語法結構的一種過程。
語義分析
語法分析完成后,我們并不知道語句的具體意義是什么。像上面的 * 號的兩棵子樹如果是兩個指針,這是不合法的,但語法分析檢測不出來,語義分析就是干這個事。
編譯期所能檢查的是靜態語義,可以認為這是在“代碼”階段,包括變量類型的匹配、轉換等。例如,將一個浮點值賦給一個指針變量的時候,明顯的類型不匹配,就會報編譯錯誤。而對于運行期間才會出現的錯誤:不小心除了一個 0 ,語義分析是沒辦法檢測的。
語義分析階段完成之后,會在每個節點上標注上類型:
Go 語言編譯器在這一階段檢查常量、類型、函數聲明以及變量賦值語句的類型,然后檢查哈希中鍵的類型。實現類型檢查的函數通常都是幾千行的巨型 switch/case 語句。
類型檢查是 Go 語言編譯的第二個階段,在詞法和語法分析之后我們得到了每個文件對應的抽象語法樹,隨后的類型檢查會遍歷抽象語法樹中的節點,對每個節點的類型進行檢驗,找出其中存在的語法錯誤。
在這個過程中也可能會對抽象語法樹進行改寫,這不僅能夠去除一些不會被執行的代碼對編譯進行優化提高執行效率,而且也會修改 make、new 等關鍵字對應節點的操作類型。
例如比較常用的 make 關鍵字,用它可以創建各種類型,如 slice,map,channel 等等。到這一步的時候,對于 make 關鍵字,也就是 OMAKE 節點,會先檢查它的參數類型,根據類型的不同,進入相應的分支。如果參數類型是 slice,就會進入 TSLICE case 分支,檢查 len 和 cap 是否滿足要求,如 len <= cap。最后節點類型會從 OMAKE 改成 OMAKESLICE。
中間代碼生成
我們知道,編譯過程一般可以分為前端和后端,前端生成和平臺無關的中間代碼,后端會針對不同的平臺,生成不同的機器碼。
前面詞法分析、語法分析、語義分析等都屬于編譯器前端,之后的階段屬于編譯器后端。
編譯過程有很多優化的環節,在這個環節是指源代碼級別的優化。它將語法樹轉換成中間代碼,它是語法樹的順序表示。
中間代碼一般和目標機器以及運行時環境無關,它有幾種常見的形式:三地址碼、P-代碼。例如,最基本的 三地址碼是這樣的:
x = y op z
表示變量 y 和 變量 z 進行 op 操作后,賦值給 x。op 可以是數學運算,例如加減乘除。
前面我們舉的例子可以寫成如下的形式:
t1 = 2 + 6
t2 = i * t1
slice[i] = t2
這里 2 + 6 是可以直接計算出來的,這樣就把 t1 這個臨時變量“優化”掉了,而且 t1 變量可以重復利用,因此 t2 也可以“優化”掉。優化之后:
t1 = i * 8
slice[i] = t1
Go 語言的中間代碼表示形式為 SSA(Static Single-Assignment,靜態單賦值),之所以稱之為單賦值,是因為每個名字在 SSA 中僅被賦值一次。
這一階段會根據 CPU 的架構設置相應的用于生成中間代碼的變量,例如編譯器使用的指針和寄存器的大小、可用寄存器列表等。中間代碼生成和機器碼生成這兩部分會共享相同的設置。
在生成中間代碼之前,會對抽象語法樹中節點的一些元素進行替換。這里引用《面向信仰編程》編譯原理相關博客里的一張圖:
例如對于 map 的操作 m[i],在這里會被轉換成 mapacess 或 mapassign。
Go 語言的主程序在執行時會調用 runtime 中的函數,也就是說關鍵字和內置函數的功能其實是由語言的編譯器和運行時共同完成的。
中間代碼的生成過程其實就是從 AST 抽象語法樹到 SSA 中間代碼的轉換過程,在這期間會對語法樹中的關鍵字在進行一次更新,更新后的語法樹會經過多輪處理轉變最后的 SSA 中間代碼。
目標代碼生成與優化
不同機器的機器字長、寄存器等等都不一樣,意味著在不同機器上跑的機器碼是不一樣的。最后一步的目的就是要生成能在不同 CPU 架構上運行的代碼。
為了榨干機器的每一滴油水,目標代碼優化器會對一些指令進行優化,例如使用移位指令代替乘法指令等。
這塊實在沒能力深入,幸好也不需要深入。對于應用層的軟件開發工程師來說,了解一下就可以了。
鏈接過程
編譯過程是針對單個文件進行的,文件與文件之間不可避免地要引用定義在其他模塊的全局變量或者函數,這些變量或函數的地址只有在此階段才能確定。
鏈接過程就是要把編譯器生成的一個個目標文件鏈接成可執行文件。最終得到的文件是分成各種段的,比如數據段、代碼段、BSS段等等,運行時會被裝載到內存中。各個段具有不同的讀寫、執行屬性,保護了程序的安全運行。
這部分內容,推薦看《程序員的自我修養》和《深入理解計算機系統》。
Go 程序啟動
仍然使用 hello-world 項目的例子。在項目根目錄下執行:
go build -gcflags "-N -l" -o hello src/main.go
-gcflags"-N -l" 是為了關閉編譯器優化和函數內聯,防止后面在設置斷點的時候找不到相對應的代碼位置。
得到了可執行文件 hello,執行:
[qcrao@qcrao hello-world]$ gdb hello
進入 gdb 調試模式,執行 info files,得到可執行文件的文件頭,列出了各種段:
同時,我們也得到了入口地址:0x450e20。
(gdb) b *0x450e20
Breakpoint 1 at 0x450e20: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
這就是 Go 程序的入口地址,我是在 linux 上運行的,所以入口文件為 src/runtime/rt0_linux_amd64.s,runtime 目錄下有各種不同名稱的程序入口文件,支持各種操作系統和架構,代碼為:
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
LEAQ 8(SP), SI // argv
MOVQ 0(SP), DI // argc
MOVQ $main(SB), AX
JMP AX
主要是把 argc,argv 從內存拉到了寄存器。這里 LEAQ 是計算內存地址,然后把內存地址本身放進寄存器里,也就是把 argv 的地址放到了 SI 寄存器中。最后跳轉到:
TEXT main(SB),NOSPLIT,$-8
MOVQ $runtime·rt0_go(SB), AX
JMP AX
繼續跳轉到 runtime·rt0_go(SB),位置:/usr/local/go/src/runtime/asm_amd64.s,代碼:
TEXT runtime·rt0_go(SB),NOSPLIT,>TEXT runtime·rt0_go(SB),NOSPLIT,$0 <
// 省略很多 CPU 相關的特性標志位檢查的代碼
// 主要是看不懂,^_^
// ………………………………
// 下面是最后調用的一些函數,比較重要
// 初始化執行文件的絕對路徑
CALL runtime·args(SB)
// 初始化 CPU 個數和內存頁大小
CALL runtime·osinit(SB)
// 初始化命令行參數、環境變量、gc、??臻g、內存管理、所有 P 實例、HASH算法等
CALL runtime·schedinit(SB)
// 要在 main goroutine 上運行的函數
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ >PUSHQ $0 // arg size< // arg size
// 新建一個 goroutine,該 goroutine 綁定 runtime.main,放在 P 的本地隊列,等待調度
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// 啟動M,開始調度goroutine
CALL runtime·mstart(SB)
MOVL >MOVL $0xf1, 0xf1 // crash
RET
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL runtime·mainPC(SB),RODATA,
參考文獻里的一篇文章【探索 golang 程序啟動過程】研究得比較深入,總結下:
檢查運行平臺的CPU,設置好程序運行需要相關標志。
TLS的初始化。
runtime.args、runtime.osinit、runtime.schedinit 三個方法做好程序運行需要的各種變量與調度器。
runtime.newproc創建新的goroutine用于綁定用戶寫的main方法。
runtime.mstart開始goroutine的調度。
最后用一張圖來總結 go bootstrap 過程吧:
main 函數里執行的一些重要的操作包括:新建一個線程執行 sysmon 函數,定期垃圾回收和調度搶占;啟動 gc;執行所有的 init 函數等等。
上面是啟動過程,看一下退出過程:
當 main 函數執行結束之后,會執行 exit(0) 來退出進程。若執行 exit(0) 后,進程沒有退出,main 函數最后的代碼會一直訪問非法地址:
exit(0)
for {
var x *int32
*x = 0
}
正常情況下,一旦出現非法地址訪問,系統會把進程殺死,用這樣的方法確保進程退出。
關于程序退出這一段的闡述來自群聊《golang runtime 閱讀》,又是一個高階的讀源碼的組織,Github 主頁見參考資料。
當然 Go 程序啟動這一部分其實還會涉及到 fork 一個新進程、裝載可執行文件,控制權轉移等問題。還是推薦看前面的兩本書,我覺得我不會寫得更好,就不敘述了。
GoRoot 和 GoPath
GoRoot 是 Go 的安裝路徑。mac 或 unix 是在 /usr/local/go 路徑上,來看下這里都裝了些什么:
bin 目錄下面:
pkg 目錄下面:
Go 工具目錄如下,其中比較重要的有編譯器 compile,鏈接器 link:
GoPath 的作用在于提供一個可以尋找 .go 源碼的路徑,它是一個工作空間的概念,可以設置多個目錄。Go 官方要求,GoPath 下面需要包含三個文件夾:
src
pkg
bin
src 存放源文件,pkg 存放源文件編譯后的庫文件,后綴為 .a;bin 則存放可執行文件。
Go 命令詳解
直接在終端執行:
go
就能得到和 go 相關的命令簡介:
和編譯相關的命令主要是:
go build
go install
go run
go build
go build 用來編譯指定 packages 里的源碼文件以及它們的依賴包,編譯的時候會到 $GoPath/src/package 路徑下尋找源碼文件。go build 還可以直接編譯指定的源碼文件,并且可以同時指定多個。
通過執行 go help build 命令得到 go build 的使用方法:
usage: go build [-o output] [-i] [build flags] [packages]
-o 只能在編譯單個包的時候出現,它指定輸出的可執行文件的名字。
-i 會安裝編譯目標所依賴的包,安裝是指生成與代碼包相對應的 .a 文件,即靜態庫文件(后面要參與鏈接),并且放置到當前工作區的 pkg 目錄下,且庫文件的目錄層級和源碼層級一致。
至于 build flags 參數, build,clean,get,install,list,run,test 這些命令會共用一套:
我們知道,Go 語言的源碼文件分為三類:命令源碼、庫源碼、測試源碼。
命令源碼文件:是 Go 程序的入口,包含 func main 函數,且第一行用 packagemain 聲明屬于 main 包。
庫源碼文件:主要是各種函數、接口等,例如工具類的函數。
測試源碼文件:以 _test.go 為后綴的文件,用于測試程序的功能和性能。
注意, go build 會忽略 *_test.go 文件。
我們通過一個很簡單的例子來演示 go build 命令。我用 Goland 新建了一個 hello-world 項目(為了展示引用自定義的包,和之前的 hello-world 程序不同),項目的結構如下:
最左邊可以看到項目的結構,包含三個文件夾:bin,pkg,src。其中 src 目錄下有一個 main.go,里面定義了 main 函數,是整個項目的入口,也就是前面提過的所謂的命令源碼文件;src 目錄下還有一個 util 目錄,里面有 util.go 文件,定義了一個可以獲取本機 IP 地址的函數,也就是所謂的庫源碼文件。
中間是 main.go 的源碼,引用了兩個包,一個是標準庫的 fmt;一個是 util 包,util 的導入路徑是 util。所謂的導入路徑是指相對于 Go 的源碼目錄 $GoRoot/src 或者 $GoPath/src 的下的子路徑。例如 main 包里引用的 fmt 的源碼路徑是 /usr/local/go/src/fmt,而 util 的源碼路徑是 /Users/qcrao/hello-world/src/util,正好我們設置的 GoPath = /Users/qcrao/hello-world。
最右邊是庫函數的源碼,實現了獲取本機 IP 的函數。
在 src 目錄下,直接執行 go build 命令,在同級目錄生成了一個可執行文件,文件名為 src,使用 ./src 命令直接執行,輸出:
hello world!
Local IP: 192.168.1.3
我們也可以指定生成的可執行文件的名稱:
go build -o bin/hello
這樣,在 bin 目錄下會生成一個可執行文件,運行結果和上面的 src 一樣。
其實,util 包可以單獨被編譯。我們可以在項目根目錄下執行:
go build util
編譯程序會去 $GoPath/src 路徑找 util 包(其實是找文件夾)。還可以在 ./src/util 目錄下直接執行 go build 編譯。
當然,直接編譯庫源碼文件不會生成 .a 文件,因為:
go build 命令在編譯只包含庫源碼文件的代碼包(或者同時編譯多個代碼包)時,只會做檢查性的編譯,而不會輸出任何結果文件。
為了展示整個編譯鏈接的運行過程,我們在項目根目錄執行如下的命令:
go build -v -x -work -o bin/hello src/main.go
-v 會打印所編譯過的包名字, -x 打印編譯期間所執行的命令, -work 打印編譯期間生成的臨時文件路徑,并且編譯完成之后不會被刪除。
執行結果:
從結果來看,圖中用箭頭標注了本次編譯過程涉及 2 個包:util,command-line-arguments。第二個包比較詭異,源碼里根本就沒有這個名字好嗎?其實這是 go build 命令檢測到 [packages] 處填的是一個 .go 文件,因此創建了一個虛擬的包:command-line-arguments。
同時,用紅框圈出了 compile, link,也就是先編譯了 util 包和 main.go 文件,分別得到 .a 文件,之后將兩者進行鏈接,最終生成可執行文件,并且移動到 bin 目錄下,改名為 hello。
另外,第一行顯示了編譯過程中的工作目錄,此目錄的文件結構是:
可以看到,和 hello-world 目錄的層級基本一致。command-line-arguments 就是虛擬的 main.go 文件所處的包。exe 目錄下的可執行文件在最后一步被移動到了 bin 目錄下,所以這里是空的。
整體來看, go build 在執行時,會先遞歸尋找 main.go 所依賴的包,以及依賴的依賴,直至最底層的包。這里可以是深度優先遍歷也可以是寬度優先遍歷。如果發現有循環依賴,就會直接退出,這也是經常會發生的循環引用編譯錯誤。
正常情況下,這些依賴關系會形成一棵倒著生長的樹,樹根在最上面,就是 main.go 文件,最下面是沒有任何其他依賴的包。編譯器會從最左的節點所代表的包開始挨個編譯,完成之后,再去編譯上一層的包。
這里,引用郝林老師幾年前在 github 上發表的 go 命令教程,可以從參考資料找到原文地址。
從代碼包編譯的角度來說,如果代碼包 A 依賴代碼包 B,則稱代碼包 B 是代碼包 A 的依賴代碼包(以下簡稱依賴包),代碼包 A 是代碼包 B 的觸發代碼包(以下簡稱觸發包)。
執行 go build 命令的計算機如果擁有多個邏輯 CPU 核心,那么編譯代碼包的順序可能會存在一些不確定性。但是,它一定會滿足這樣的約束條件:依賴代碼包 -> 當前代碼包 -> 觸發代碼包。
順便推薦一個瀏覽器插件 Octotree,在看 github 項目的時候,此插件可以在瀏覽器里直接展示整個項目的文件結構,非常方便:
到這里,你一定會發現,對于 hello-wrold 文件夾下的 pkg 目錄好像一直沒有涉及到。
其實,pkg 目錄下面應該存放的是涉及到的庫文件編譯后的包,也就是一些 .a 文件。但是 go build 執行過程中,這些 .a 文件放在臨時文件夾中,編譯完成后會被直接刪掉,因此一般不會用到。
前面我們提到過,在 go build 命令里加上 -i 參數會安裝這些庫文件編譯的包,也就是這些 .a 文件會放到 pkg 目錄下。
在項目根目錄執行 go build-i src/main.go 后,pkg 目錄里增加了 util.a 文件:
darwin_amd64 表示的是:
GOOS 和 GOARCH。這兩個環境變量不用我們設置,系統默認的。
GOOS 是 Go 所在的操作系統類型,GOARCH 是 Go 所在的計算架構。
Mac 平臺上這個目錄名就是 darwin_amd64。
生成了 util.a 文件后,再次編譯的時候,就不會再重新編譯 util.go 文件,加快了編譯速度。
同時,在根目錄下生成了名稱為 main 的可執行文件,這是以 main.go 的文件名命令的。
hello-world 這個項目的代碼已經上傳到了 github 項目 Go-Questions,這個項目由問題導入,企圖串連 Go 的所有知識點,正在完善,期待你的 star。地址見參考資料【Go-Questions hello-world項目】。
go install
go install 用于編譯并安裝指定的代碼包及它們的依賴包。相比 go build,它只是多了一個“安裝編譯后的結果文件到指定目錄”的步驟。
還是使用之前 hello-world 項目的例子,我們先將 pkg 目錄刪掉,在項目根目錄執行:
go install src/main.go
或者
go install util
兩者都會在根目錄下新建一個 pkg 目錄,并且生成一個 util.a 文件。
并且,在執行前者的時候,會在 GOBIN 目錄下生成名為 main 的可執行文件。
所以,運行 go install 命令,庫源碼包對應的 .a 文件會被放置到 pkg 目錄下,命令源碼包生成的可執行文件會被放到 GOBIN 目錄。
go install 在 GoPath 有多個目錄的時候,會產生一些問題,具體可以去看郝林老師的 Go命令教程,這里不展開了。
go run
go run 用于編譯并運行命令源碼文件。
在 hello-world 項目的根目錄,執行 go run 命令:
go run -x -work src/main.go
-x 可以打印整個過程涉及到的命令,-work 可以看到臨時的工作目錄:
從上圖中可以看到,仍然是先編譯,再連接,最后直接執行,并打印出了執行結果。
第一行打印的就是工作目錄,最終生成的可執行文件就是放置于此:
main 就是最終生成的可執行文件。
總結
這次的話題太大了,困難重重。從編譯原理到 go 啟動時的流程,到 go 命令原理,每個話題單獨抽出來都可以寫很多。
幸好有一些很不錯的書和博客文章可以去參考。這篇文章就作為一個引子,你可以跟隨參考資料里推薦的一些內容去發散。
參考資料
【《程序員的自我修養》全書】https://book.douban.com/subject/3652388/
【面向信仰編程 編譯過程概述】https://draveness.me/golang-compile-intro
【golang runtime 閱讀】https://github.com/zboya/golangruntimereading
【Go-Questions hello-world項目】https://github.com/qcrao/Go-Questions/tree/master/examples/hello-world
【雨痕大佬的 Go 語言學習筆記】https://github.com/qyuhen/book
【vim 以 16 進制文本】https://www.cnblogs.com/meibenjin/archive/2012/12/06/2806396.html
【Go 編譯命令執行過程】https://halfrost.com/go_command/
【Go 命令執行過程】https://github.com/hyper0x/gocommandtutorial
【Go 詞法分析】https://ggaaooppeenngg.github.io/zh-CN/2016/04/01/go-lexer-%E8%AF%8D%E6%B3%95%E5%88%86%E6%9E%90/
【曹大博客 golang 與 ast】http://xargin.com/ast/
【Golang 詞法解析器,scanner 源碼分析】https://blog.csdn.net/zhaoruixiang1111/article/details/89892435
【Gopath Explained】https://flaviocopes.com/go-gopath/
【Understanding the GOPATH】https://www.digitalocean.com/community/tutorials/understanding-the-gopath
【討論】https://stackoverflow.com/questions/7970390/what-should-be-the-values-of-gopath-and-goroot
【Go 官方 Gopath】https://golang.org/cmd/go/#hdr-GOPATHenvironmentvariable
【Go package 的探索】https://mp.weixin.qq.com/s/OizVLXfZ6EC1jI-NL7HqeA
【Go 官方 關于 Go 項目的組織結構】https://golang.org/doc/code.html
【Go modules】https://www.melvinvivas.com/go-version-1-11-modules/
【Golang Installation, Setup, GOPATH, and Go Workspace】https://www.callicoder.com/golang-installation-setup-gopath-workspace/
【編譯、鏈接過程鏈接】https://mikespook.com/2013/11/%E7%BF%BB%E8%AF%91-go-build-%E5%91%BD%E4%BB%A4%E6%98%AF%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C%E7%9A%84%EF%BC%9F/
【1.5 編譯器由 go 語言完成】https://www.infoq.cn/article/2015/08/go-1-5
【Go 編譯過程系列文章】https://www.ctolib.com/topics-3724.html
【曹大 go bootstrap】https://github.com/cch123/golang-notes/blob/master/bootstrap.md
【golang 啟動流程】https://blog.iceinto.com/posts/go/start/
【探索 golang 程序啟動過程】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2golang%E7%A8%8B%E5%BA%8F%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B/
【探索 goroutine 的創建】http://cbsheng.github.io/posts/%E6%8E%A2%E7%B4%A2goroutine%E7%9A%84%E5%88%9B%E5%BB%BA/
版權聲明:本文為CSDN博主「qcrao」的原創文章。
【END】
1024程序員節如期而至,CSDN Blink小姐姐的關愛來了!
掃描領取小姐姐的專屬福利!
程序員邂逅鼓勵師的正確姿勢!掃描前往福利現場>>
者 | Erik-Jan van Baaren
譯者 | 彎月,責編 | 屠敏
以下為譯文:
元旦過完了,我們都紛紛回到了各自的工作崗位。新的一年新氣象,我想借本文為大家獻上 Python 語言的30個最佳實踐、小貼士和技巧,希望能對各位勤勞的程序員有所幫助,并希望大家工作順利!
1. Python 版本
在此想提醒各位:自2020年1月1日起,Python 官方不再支持 Python 2。本文中的很多示例只能在 Python 3 中運行。如果你仍在使用 Python 2.7,請立即升級。
2. 檢查 Python 的最低版本
你可以在代碼中檢查 Python 的版本,以確保你的用戶沒有在不兼容的版本中運行腳本。檢查方式如下:
if not sys.version_info > (2, 7):
# berate your user for running a 10 year
# python version
elif not sys.version_info >= (3, 5):
# Kindly tell your user (s)he needs to upgrade
# because you're using 3.5 features
3. IPython
IPython 本質上就是一個增強版的shell。就沖著自動補齊就值得一試,而且它的功能還不止于此,它還有很多令我愛不釋手的命令,例如:
%cd:改變當前的工作目錄
%edit:打開編輯器,并關閉編輯器后執行鍵入的代碼
%env:顯示當前環境變量
%pip install [pkgs]:無需離開交互式shell,就可以安裝軟件包
%time 和 %timeit:測量執行Python代碼的時間
完整的命令列表,請點擊此處查看(https://ipython.readthedocs.io/en/stable/interactive/magics.html)。
還有一個非常實用的功能:引用上一個命令的輸出。In 和 Out 是實際的對象。你可以通過 Out[3] 的形式使用第三個命令的輸出。
IPython 的安裝命令如下:
pip3 install ipython
4. 列表推導式
你可以利用列表推導式,避免使用循環填充列表時的繁瑣。列表推導式的基本語法如下:
[ expression for item in list if conditional ]
舉一個基本的例子:用一組有序數字填充一個列表:
mylist = [i for i in range(10)]
print(mylist)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
由于可以使用表達式,所以你也可以做一些算術運算:
squares = [x**2 for x in range(10)]
print(squares)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
甚至可以調用外部函數:
def some_function(a):
return (a + 5) / 2
my_formula = [some_function(i) for i in range(10)]
print(my_formula)
# [2, 3, 3, 4, 4, 5, 5, 6, 6, 7]
最后,你還可以使用 ‘if’ 來過濾列表。在如下示例中,我們只保留能被2整除的數字:
filtered = [i for i in range(20) if i%2==0]
print(filtered)
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
5. 檢查對象使用內存的狀況
你可以利用 sys.getsizeof 來檢查對象使用內存的狀況:
import sys
mylist = range(0, 10000)
print(sys.getsizeof(mylist))
# 48
等等,為什么這個巨大的列表僅包含48個字節?
因為這里的 range 函數返回了一個類,只不過它的行為就像一個列表。在使用內存方面,range 遠比實際的數字列表更加高效。
你可以試試看使用列表推導式創建一個范圍相同的數字列表:
import sys
myreallist = [x for x in range(0, 10000)]
print(sys.getsizeof(myreallist))
# 87632
6. 返回多個值
Python 中的函數可以返回一個以上的變量,而且還無需使用字典、列表或類。如下所示:
def get_user(id):
# fetch user from database
# ....
return name, birthdate
name, birthdate = get_user(4)
如果返回值的數量有限當然沒問題。但是,如果返回值的數量超過3個,那么你就應該將返回值放入一個(數據)類中。
7. 使用數據類
Python從版本3.7開始提供數據類。與常規類或其他方法(比如返回多個值或字典)相比,數據類有幾個明顯的優勢:
數據類的代碼量較少
你可以比較數據類,因為數據類提供了 __eq__ 方法
調試的時候,你可以輕松地輸出數據類,因為數據類還提供了 __repr__ 方法
數據類需要類型提示,因此可以減少Bug的發生幾率
數據類的示例如下:
from dataclasses import dataclass
@dataclass
class Card:
rank: str
suit: str
card = Card("Q", "hearts")
print(card == card)
# True
print(card.rank)
# 'Q'
print(card)
Card(rank='Q', suit='hearts')
詳細的使用指南請點擊這里(https://realpython.com/python-data-classes/)。
8. 交換變量
如下的小技巧很巧妙,可以為你節省多行代碼:
a = 1
b = 2
a, b = b, a
print (a)
# 2
print (b)
# 1
9. 合并字典(Python 3.5以上的版本)
從Python 3.5開始,合并字典的操作更加簡單了:
dict1 = { 'a': 1, 'b': 2 }
dict2 = { 'b': 3, 'c': 4 }
merged = { **dict1, **dict2 }
print (merged)
# {'a': 1, 'b': 3, 'c': 4}
如果 key 重復,那么第一個字典中的 key 會被覆蓋。
10. 字符串的首字母大寫
如下技巧真是一個小可愛:
mystring = "10 awesome python tricks"
print(mystring.title)
'10 Awesome Python Tricks'
11. 將字符串分割成列表
你可以將字符串分割成一個字符串列表。在如下示例中,我們利用空格分割各個單詞:
mystring = "The quick brown fox"
mylist = mystring.split(' ')
print(mylist)
# ['The', 'quick', 'brown', 'fox']
12. 根據字符串列表創建字符串
與上述技巧相反,我們可以根據字符串列表創建字符串,然后在各個單詞之間加入空格:
mylist = ['The', 'quick', 'brown', 'fox']
mystring = " ".join(mylist)
print(mystring)
# 'The quick brown fox'
你可能會問為什么不是 mylist.join(" "),這是個好問題!
根本原因在于,函數 String.join 不僅可以聯接列表,而且還可以聯接任何可迭代對象。將其放在String中是為了避免在多個地方重復實現同一個功能。
13. 表情符
有些人非常喜歡表情符,而有些人則深惡痛絕。我在此鄭重聲明:在分析社交媒體數據時,表情符可以派上大用場。
首先,我們來安裝表情符模塊:
pip3 install emoji
安裝完成后,你可以按照如下方式使用:
import emoji
result = emoji.emojize('Python is :thumbs_up:')
print(result)
# 'Python is '
# You can also reverse this:
result = emoji.demojize('Python is ')
print(result)
# 'Python is :thumbs_up:'
更多有關表情符的示例和文檔,請點擊此處(https://pypi.org/project/emoji/)。
14. 列表切片
列表切片的基本語法如下:
a[start:stop:step]
start、stop 和 step 都是可選項。如果不指定,則會使用如下默認值:
start:0
end:字符串的結尾
step:1
示例如下:
# We can easily create a new list from
# the first two elements of a list:
first_two = [1, 2, 3, 4, 5][0:2]
print(first_two)
# [1, 2]
# And if we use a step value of 2,
# we can skip over every second number
# like this:
steps = [1, 2, 3, 4, 5][0:5:2]
print(steps)
# [1, 3, 5]
# This works on strings too. In Python,
# you can treat a string like a list of
# letters:
mystring = "abcdefdn nimt"[::2]
print(mystring)
# 'aced it'
15. 反轉字符串和列表
你可以利用如上切片的方法來反轉字符串或列表。只需指定 step 為 -1,就可以反轉其中的元素:
revstring = "abcdefg"[::-1]
print(revstring)
# 'gfedcba'
revarray = [1, 2, 3, 4, 5][::-1]
print(revarray)
# [5, 4, 3, 2, 1]
16. 顯示貓貓
我終于找到了一個充分的借口可以在我的文章中顯示貓貓了,哈哈!當然,你也可以利用它來顯示圖片。首先你需要安裝 Pillow,這是一個 Python 圖片庫的分支:
pip3 install Pillow
接下來,你可以將如下圖片下載到一個名叫 kittens.jpg 的文件中:
然后,你就可以通過如下 Python 代碼顯示上面的圖片:
from PIL import Image
im = Image.open("kittens.jpg")
im.show
print(im.format, im.size, im.mode)
# JPEG (1920, 1357) RGB
Pillow 還有很多顯示該圖片之外的功能。它可以分析、調整大小、過濾、增強、變形等等。完整的文檔,請點擊這里(https://pillow.readthedocs.io/en/stable/)。
17. map
Python 有一個自帶的函數叫做 map,語法如下:
map(function, something_iterable)
所以,你需要指定一個函數來執行,或者一些東西來執行。任何可迭代對象都可以。在如下示例中,我指定了一個列表:
def upper(s):
return s.upper
mylist = list(map(upper, ['sentence', 'fragment']))
print(mylist)
# ['SENTENCE', 'FRAGMENT']
# Convert a string representation of
# a number into a list of ints.
list_of_ints = list(map(int, "1234567")))
print(list_of_ints)
# [1, 2, 3, 4, 5, 6, 7]
你可以仔細看看自己的代碼,看看能不能用 map 替代某處的循環。
18. 獲取列表或字符串中的唯一元素
如果你利用函數 set 創建一個集合,就可以獲取某個列表或類似于列表的對象的唯一元素:
mylist = [1, 1, 2, 3, 4, 5, 5, 5, 6, 6]
print (set(mylist))
# {1, 2, 3, 4, 5, 6}
# And since a string can be treated like a
# list of letters, you can also get the
# unique letters from a string this way:
print (set("aaabbbcccdddeeefff"))
# {'a', 'b', 'c', 'd', 'e', 'f'}
19. 查找出現頻率最高的值
你可以通過如下方法查找出現頻率最高的值:
test = [1, 2, 3, 4, 2, 2, 3, 1, 4, 4, 4]
print(max(set(test), key = test.count))
# 4
你能看懂上述代碼嗎?想法搞明白上述代碼再往下讀。
沒看懂?我來告訴你吧:
max 會返回列表的最大值。參數 key 會接受一個參數函數來自定義排序,在本例中為 test.count。該函數會應用于迭代對象的每一項。
test.count 是 list 的內置函數。它接受一個參數,而且還會計算該參數的出現次數。因此,test.count(1) 將返回2,而 test.count(4) 將返回4。
set(test) 將返回 test 中所有的唯一值,也就是 {1, 2, 3, 4}。
因此,這一行代碼完成的操作是:首先獲取 test 所有的唯一值,即{1, 2, 3, 4};然后,max 會針對每一個值執行 list.count,并返回最大值。
這一行代碼可不是我個人的發明。
20. 創建一個進度條
你可以創建自己的進度條,聽起來很有意思。但是,更簡單的方法是使用 progress 包:
pip3 install progress
接下來,你就可以輕松地創建進度條了:
from progress.bar import Bar
bar = Bar('Processing', max=20)
for i in range(20):
# Do some work
bar.next
bar.finish
21. 在交互式shell中使用_(下劃線運算符)
你可以通過下劃線運算符獲取上一個表達式的結果,例如在 IPython 中,你可以這樣操作:
In [1]: 3 * 3
Out[1]: 9In [2]: _ + 3
Out[2]: 12
Python Shell 中也可以這樣使用。另外,在 IPython shell 中,你還可以通過 Out[n] 獲取表達式 In[n] 的值。例如,在如上示例中,Out[1] 將返回數字9。
22. 快速創建Web服務器
你可以快速啟動一個Web服務,并提供當前目錄的內容:
python3 -m http.server
當你想與同事共享某個文件,或測試某個簡單的HTML網站時,就可以考慮這個方法。
23. 多行字符串
雖然你可以用三重引號將代碼中的多行字符串括起來,但是這種做法并不理想。所有放在三重引號之間的內容都會成為字符串,包括代碼的格式,如下所示。
我更喜歡另一種方法,這種方法不僅可以將多行字符串連接在一起,而且還可以保證代碼的整潔。唯一的缺點是你需要明確指定換行符。
s1 = """Multi line strings can be put
between triple quotes. It's not ideal
when formatting your code though"""
print (s1)
# Multi line strings can be put
# between triple quotes. It's not ideal
# when formatting your code though
s2 = ("You can also concatenate multiple\n" +
"strings this way, but you'll have to\n"
"explicitly put in the newlines")
print(s2)
# You can also concatenate multiple
# strings this way, but you'll have to
# explicitly put in the newlines
24. 條件賦值中的三元運算符
這種方法可以讓代碼更簡潔,同時又可以保證代碼的可讀性:
[on_true] if [expression] else [on_false]
示例如下:
x = "Success!" if (y == 2) else "Failed!"
25. 統計元素的出現次數
你可以使用集合庫中的 Counter 來獲取列表中所有唯一元素的出現次數,Counter 會返回一個字典:
from collections import Counter
mylist = [1, 1, 2, 3, 4, 5, 5, 5, 6, 6]
c = Counter(mylist)
print(c)
# Counter({1: 2, 2: 1, 3: 1, 4: 1, 5: 3, 6: 2})
# And it works on strings too:
print(Counter("aaaaabbbbbccccc"))
# Counter({'a': 5, 'b': 5, 'c': 5})
26. 比較運算符的鏈接
你可以在 Python 中將多個比較運算符鏈接到一起,如此就可以創建更易讀、更簡潔的代碼:
x = 10
# Instead of:
if x > 5 and x < 15:
print("Yes")
# yes
# You can also write:
if 5 < x < 15:
print("Yes")
# Yes
27. 添加顏色
你可以通過 Colorama,設置終端的顯示顏色:
from colorama import Fore, Back, Style
print(Fore.RED + 'some red text')
print(Back.GREEN + 'and with a green background')
print(Style.DIM + 'and in dim text')
print(Style.RESET_ALL)
print('back to normal now')
28. 日期的處理
python-dateutil 模塊作為標準日期模塊的補充,提供了非常強大的擴展,你可以通過如下命令安裝:
pip3 install python-dateutil
你可以利用該庫完成很多神奇的操作。在此我只舉一個例子:模糊分析日志文件中的日期:
from dateutil.parser import parse
logline = 'INFO 2020-01-01T00:00:01 Happy new year, human.'
timestamp = parse(log_line, fuzzy=True)
print(timestamp)
# 2020-01-01 00:00:01
你只需記?。寒斢龅匠R?Python 日期時間功能無法解決的問題時,就可以考慮 python-dateutil !
29.整數除法
在 Python 2 中,除法運算符(/)默認為整數除法,除非其中一個操作數是浮點數。因此,你可以這么寫:
# Python 2
5 / 2 = 2
5 / 2.0 = 2.5
在 Python 3 中,除法運算符(/)默認為浮點除法,而整數除法的運算符為 //。因此,你需要這么寫:
Python 3
5 / 2 = 2.5
5 // 2 = 2
這項變更背后的動機,請參閱 PEP-0238(https://www.python.org/dev/peps/pep-0238/)。
30. 通過chardet 來檢測字符集
你可以使用 chardet 模塊來檢測文件的字符集。在分析大量隨機文本時,這個模塊十分實用。安裝方法如下:
pip install chardet
安裝完成后,你就可以使用命令行工具 chardetect 了,使用方法如下:
chardetect somefile.txt
somefile.txt: ascii with confidence 1.0
你也可以在編程中使用該庫,完整的文檔請點擊這里(https://chardet.readthedocs.io/en/latest/usage.html)。
如上就是我為各位奉上的新年禮物,希望各位喜歡!如果你有其他的技巧、貼士和實踐,請在下方留言!
原文:https://towardsdatascience.com/30-python-best-practices-tips-and-tricks-caefb9f8c5f5
本文為 CSDN 翻譯,轉載請注明來源出處。
器之心整理
參與:一鳴、路
Python 是一門廣受好評的編程語言,每個版本的更新都會對開發社區帶來一定影響。近日,Python 3.8 已進入 beta 2 版本的測試中,各項新特性已經添加完畢,最終版本預計于今年 10 月發布。在發布即將到來前,機器之心總結了 Python 3.8 中幾大值得關注的新功能和改進。
從事計算機領域工作的讀者朋友對 Python 編程語言應該非常熟悉了。這是一門廣受好評的動態編程語言,其靈活和語法簡易的特點使得這門語言在腳本工具、數據分析、Web 后端都有廣泛的應用。Python 開發社區也非常活躍,3.x 的版本迭代速度非???。2018 年 6 月底,Python 3.7 問世,之后 Python 3.8 的開發和測試工作也已經展開。近日,Python 軟件基金會公開了 3.80b2 的說明文檔,向公眾展示了 beta 版本的測試進展,以及 Python 3.8 版本的新特性和功能改進。
目前,Python 3.8 的 beta 測試流程正在進行中。今年 6 月初,官方發布了第一個 beta 版本——Python 3.80b1,緊接著一個月后發布了 Python 3.80b2。第二個 beta 版本發布后,Python 3.8 新特性已經添加完畢。官方目前已公布最終版本的發布時間,預計在今年的 10 月份。
那么,新的 Python 3.8 版本有哪些新特性和功能呢?機器之心根據 Python 基金會公開的文檔,整理出了以下值得期待的新特性和功能改進。
文檔地址:https://docs.python.org/zh-cn/3.8/whatsnew/3.8.html#summary-release-highlights
新的語法
Python 3.8 中最值得關注的部分在于其語法的變化,這些新語法有助于提升效率,減少代碼工作量。
海象運算符( := )
這個「:=」橫過來看是不是有點像海象的臉?這是一個新的 Python 語法,可以在進行條件判斷時直接為變量賦值。
過去我們需要首先對某個變量進行賦值,然后進行條件判斷。
m = re.match(p1, line)if m: return m.group(1)else: m = re.match(p2, line)if m: return m.group(2)else: m = re.match(p3, line) ...
而使用海象運算符后,我們可以直接為變量賦值:
if m := re.match(p1, line): return m.group(1)elif m := re.match(p2, line): return m.group(2)elif m := re.match(p3, line):
還有一個在循環中使用的例子,過去在對某個變量進行循環前必須首先賦值:
ent = obj.next_entry()while ent: ... # process ent ent = obj.next_entry()
現在可以一邊循環一邊賦值:
while ent := obj.next_entry(): ... # process ent
代碼調試中支持 f-string
f-string(或者稱為「格式化字符串」)在 Python 3.6 版本中加入的,雖然這一特性非常方便,但是開發者發現 f-string 對調試沒有幫助。因此,Eric V. Smith 為 f-string 添加了一些語法結構,使其能夠用于調試。
在過去,f-string 可以這樣使用:
print(f'foo={foo} bar={bar}')
在 Python 3.8 中,只需使用如下代碼(更加簡潔):
print(f'{foo=} {bar=}')
兩種情況下,輸出都是:
>>> foo = 42>>> bar = 'answer ...'>>> print(f'{foo=} {bar=}') foo=42 bar=answer ...
此外,可以通過在賦值符號后增加「!s」和「!f」命令,規定輸出結果的格式,例如:
>>> import datetime>>> now = datetime.datetime.now()>>> print(f'{now=} {now=!s}') now=datetime.datetime(2019, 7, 16, 16, 58, 0, 680222) now=2019-07-16 16:58:00.680222
>>> import math>>> print(f'{math.pi=!f:.2f}') math.pi=3.14
如代碼所示,第二個「now」變量的等號后增加了「!s」字符,使得 now 從「datetime」類型的輸出變成了字符串類型的輸出。在「pi」的輸出中,由于加入了「!f:.2f」,使得 pi 的輸出變為了保留小數點后兩位。
而花括號中的格式也會影響打印結果的格式,例如:
>>> a = 37>>> print(f'{a = }, {a = }') a = 37, a = 37
花括號中的等號前后間距不同,打印結果中的間距也不一樣。
不需要 Keyword 的變量輸入
這一語法使得函數在定義輸入變量時可以規定只能輸入 Value,而不在 Value 前加上 Keyword。
例如,在使用 pow() 函數時:
>>> pow(2, 3) 8>>> pow(x=2, y=3) ... TypeError: pow() takes no keyword arguments
第一種方法是合法的,但是第二種則非法。
為了保證第一種純粹的 Python 函數方法,開發者可以在定義函數時使用「/」規定哪些變量的輸入必須按照第一種格式。例如:
def pow(x, y, z=None, /): r = x**y if z is not None: r %= z return r
在定義了所有變量之后額外增加一個「/」字符,表示所有的 Python 變量輸入必須按照 pow(x, y, z) 的方式進行。
當然,也可以在變量之間插入「/」,正斜杠之前的變量按照純粹的 Python 輸入方法,而正斜杠之后的按照定義好的方法執行。例如,定義如下函數:
def fun(a, b, /, c, d, *, e, f): ...
則以下一些表達式合法的,但另一些非法:
fun(1, 2, 3, 4, e=5, f=6) # legalfun(1, 2, 3, d=4, e=5, f=6) # legalfun(a=1, b=2, c=3, d=4, e=5, f=6) # illegal
其他特性
除了一些語法方面的改進,Python 3.8 版本還有一些其他的變化。
可移動的「__pycache__」
__pycache__目錄是由 Python3 解釋器創建的,用于保存.pyc 文件。這些文件保存著解釋器編譯.py 文件之后的字節碼(byte code)。之前的 Python 版本僅僅只是為每個.py 文件創建一個.pyc 文件,但是新版本會有所變化。
為了支持多版本的 Python,包括一些不是 CPython 的版本(如 PyPy),現在庫文件會為每個 Python 版本創建對應的.pyc 文件,格式形如「name.interp-version.pyc」。例如,某個 foo.py 文件在第一次使用的時候會創建一個對應的 pyc 文件,其路徑為「__pycache__/foo.cpython-37.pyc」,這個 pyc 文件會定義使用的 Python 版本。
其他改進
Python 3.8 會為 C 語言擴展加入更快的調用方式,這種方式原本屬于 CPython。在 Python3.8 中,這一功能是實驗性的,最終的完成版本會出現在 Python3.9 中。
同時,編譯器中的初始化配置處理也得到了清理,使得 Python 可以更好地嵌入其他程序,不需要依賴環境變量或在已有的 Python 系統中增加導致沖突的其他組件。
此外,大量的 Python 自帶模塊得到了改進和調整,如「os.path」、「shutil」、「math」、「ssl」等。
Python 3.8 什么時候與大家見面?
目前發布時間仍在探討,但考慮到 Python 3.9 的發布計劃(大約在 2020 年 6 月),官方認為 Python3.8 的發布時間不應當晚于今年 10 月份。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。