- Python之光:Python編程入門與實戰
- 李慶輝
- 7526字
- 2023-11-13 15:11:06
1.4 了解Python
Python和社會語言一樣,有著自己的表達方式和表達邏輯,只不過它是“說”給計算機聽的,我們在閱讀和編寫Python代碼之前需要先了解它的思考和表達方式。
之前我們簡單介紹了Python的基本用法并搭建了Python的開發環境。在本節,我們將介紹Python的一些基礎語法和運行機制,為今后編寫Python代碼建立底層認知。
本節可能稍顯晦澀難懂,初學時可以略讀或者跳過,待有一定代碼經驗后再來精讀,相信會給你豁然開朗的感覺。
1.4.1 代碼行
我們編寫的未被Python解釋器解析執行的代碼叫源代碼。我們在編寫代碼時是一行一行編寫的,幾乎所有的IDE左邊都會有行號(JupyterLab可從View菜單中開啟行號顯示),行號對應一個物理代碼行或物理行,但Python解釋器在分析源代碼時,會將多個物理行解析成一個代碼行,我們稱之為邏輯代碼行或邏輯行。Python按邏輯行執行代碼。
新增一個物理行只要按回車鍵即可,通過拼接可以將多個物理行轉換為一個邏輯行,此時Python會認為它們是一個完整的邏輯。
物理行拼接有顯式拼接和隱式拼接兩種方式。顯式拼接指在物理行尾用反斜杠(\)拼接,如以下判斷一個日期是否有效的代碼,由于表達式過長,可以通過反斜杠將多個物理行轉換為一個邏輯行。

反斜杠只能用在代碼物理行尾和字符串字面值中,在字符串字面值中可以將兩個物理行的字符串拼接在一起,下文會講到這個規則。除此之外,反斜杠在其他地方都是非法的。
另一種拼接方式是隱式拼接。圓括號(元組、表達式和調用傳參等)、方括號(列表)、花括號(字典和集合)中的代碼可以分成多個物理行,而不必使用反斜杠,這屬于隱式拼接。這樣做的好處是代碼更加易讀,且能在物理行尾增加注釋(反斜杠后不能加注釋)。以下代碼演示了這些情況。


物理行拼接可以幫助我們避免編寫超寬的物理行。在Python的PEP8規范(一個Python官方倡議的代碼編寫風格)中,一個物理行一般不能超過79個字符。如果你所在團隊有相應的約束,也應當遵守。
我們的代碼是寫給計算機看的,而注釋是寫給人看的。注釋的讀者是閱讀你代碼的其他人和你自己。物理行中井號(#)之后的所有內容都是注釋,Python不會解析。井號可以在物理行的開頭也可以在行后,即支持單獨一行注釋和一行后半部分注釋(參見上文代碼)。再次強調,Python只有單行注釋,沒有多行注釋,多行注釋需要用三引號包裹多行字符串間接實現。
和注釋一樣,空白行(僅包含空格符、制表符、換頁符)的邏輯行也會被Python忽略。在標準的交互模式下,完全空白的邏輯行(即連空格或注釋都沒有)將結束多行復合語句。
有時你在閱讀代碼時可能會看到Python文件第一、二行有類似以下的注釋:

這是在聲明Python的解釋器和代碼文件編碼,不過這是Python的歷史遺留問題,在Python 3中默認的編碼已經是utf-8編碼了,因此我們不必增加這兩行內容。
1.4.2 縮進
縮進是Python的特色,當其他大部分語言用花括號({})組織邏輯層次時,Python為了讓代碼更加簡潔而設計了縮進方式。PEP8建議在上一個邏輯行后縮4個空格,但所有的Python編輯器已經支持自動縮進,當語法需要縮進時,回車換行會幫助我們自動縮進,如果需要手動縮進可以按Ta b鍵,按一次縮進一層。
常用的定義函數、for循環等語句都需要縮進,例如:

函數say()中的print()相對于def關鍵字縮進一層,for語句中的if等相對于for關鍵字縮進一層,print()相對于if等又縮進一層,從而表達代碼的邏輯關系。哪些語句需要縮進,本書后續會一一介紹。
對于初學者來說縮進是比較難掌握的知識點,學會正確處理縮進意味著你已經邁入Python的門檻。在代碼編寫中,我們要結合代碼編輯器的自動格式化功能嚴格布局縮進和上下行空格,讓代碼更加美觀易讀。
1.4.3 標識符
我們說過,在Python中,一切皆是對象,而對象存在某個內存地址里,在使用對象進行操作時,為了方便起見我們給這個內存地址起一個名,這個名的符號表達就是標識符,因此標識符又稱名稱。
標識符是內存地址的名稱,當然也可以把標識符換為別的內存地址,標識符與內存地址綁定的過程就是賦值操作。以下代碼演示了一個名稱的使用過程:

在賦值表達式中,對象10先綁定了名稱a,用Python內置函數id()獲取其內存地址,然后將a換綁(再次賦值)到對象20,再看它的內存地址已經發生了變化。這里的10和20是整型,它們都是對象,占用不同的內存空間。我們定義的函數say()的名稱是say,它所指向的函數對象也占有一定的內存空間。甚至內置函數id()也是一個對象,我們可以使用id(id)傳入它的名稱id,也能返回它的內存地址。
Python標識符和其他語言中的變量是不同的,比如C語言中的變量表示一段固定的內存地址,而Python的標識符更像一個標簽,可以貼在任何對象上。盡管這樣,很多人還是習慣將Python的標識符稱為變量,今后當你聽到Python的變量時,應該知道它指的就是標識符或者名稱。
在不同的場景下,標識符、名稱、名字、變量名等其實是一回事。
1.4.4 標識符命名
標識符用于給對象起名,即所謂的變量、函數、類等的名字都是標識符。標識符起名必須符合一定的規范。有效標識符可以包含大小寫字母、下劃線(_)、數字0~9,但不能以數字開頭,也不能包含其他特殊符號(如.、!、@、#、$、%等)。Python 3.0引入了ASCII(American Standard Code for Information Interchange,美國信息交換標準代碼,一套電腦編碼系統,主要用于顯示現代英語)之外的更多字符,如漢字、其他語種的字符,不過還是建議用英文作為標識符。
標識符是區分大小寫的,如age、Age、agE是不同的標識符。另外,Python的關鍵字如if、is、import等作為保留標識符號,不能用于我們自定義的標識符。可以導入內置模塊keyword查看Python的關鍵字:

另外還有一些被稱為軟關鍵字的關鍵字,它們只在特定上下文中作為關鍵字,如上述列表中沒有的match,只有在模式匹配操作中連同case以及下劃線一起它才具有關鍵字的語義。
以下列出了一些合法和不合法的標識符,合法的標識符并不一定是符合規范的。

除了關鍵字,內置函數名、內置庫或者知名庫的名稱,以及這些庫的常量、方法名、內置異常對象等,無論大小寫,也不要作為標識符,如果一定要用的話,應該在后面加一個下劃線,如len_、bool_。還可以使用與關鍵字相似的單詞(或者是讀音、其他語言派生等)代替,如class用klass代替。也可以用約定俗成的foo、bar、baz、qux等為不特定對象命名。
判斷字符串是不是合法的標識符,可以用字符串的isidentifier()方法:

雖然關鍵字是合法的標識符,但我們不能自己使用。可以用keyword.iskeyword()來判斷一個標識符是不是關鍵字。
在實踐中,標識符的起名應該遵照以下約定。
? 多個單詞組合時用下劃線連接,每個單詞都用小寫,如user_name。
? 特別地,類名以大寫字母開頭,不用下劃線連接,如CamelCase。
? 使用下劃線作為第一個字符來聲明私有標識符。
? 不要在標識符中使用下劃線同時作為前導和尾隨字符(如_name_),因為Python內置類型已經使用了這種表示法。
? 正式代碼中避免使用只有一個字符的名稱,而應使用有意義的名字。
? 常量(執行過程中永遠不會變化的量)的所有字母都大寫。
? 不使用內置命名空間的名稱,可導入builtins模塊(import builtins)并用dir(builtins)查看。
? 學習PEP8規范,里面有更為詳細的建議。
另外,還有一些特殊場景下的標識符命名規則和注意事項。
? 腳本中名稱以下劃線開頭的全局對象(_*)用from module import *導入時,不會被導入。
? 開頭和結尾雙下劃線的名稱(__*__)是Python中對象的特殊方法,用于定義內置函數和操作所執行的方法,如len(x),就是執行對象x的__len__()方法。
? 類中的方法如果是雙下劃線名稱(__*),會被自動轉換為_類名__標識符,以免與有繼承關系的私有屬性發生沖突。
以上這些我們現在可能不是太理解,后文在介紹面向對象編程時還會詳細講解。在命名時,需要特別注意單下劃線開頭和兩端雙下劃線的名稱。
1.4.5 名稱的使用
Python標識符包含很多用戶定義的名稱,這些名稱通過綁定來表示變量、函數、類、模塊或任何其他對象。如果在Python中為一個對象實體指定了某個名稱,那么就說它是一個標識符,在后續的代碼中可以通過名稱來使用它。在Python中,標識符的命名規則都是一樣的。
準確地說,這些名稱在以下場景會進行綁定,大家可以先大概了解一下,后續會詳細介紹這些操作。
? 賦值語句,定義一個對象,如a=10中的a。
? 定義函數,函數的名稱,如def func()中的func。
? 函數的形式參數,如def func(name, age)中的name和age。
? 定義類,類和名稱,如class Person()中的Person。
? import語句中的as別名,如import numpy as np中的np。
? for循環的循環頭,如for item in items中的item。
? 賦值表達式(海象運算符操作),如if(n:=len(a))>10中的n。
? with、except語句as后面的名稱。
? match case模式匹配語句中的模式原型。
名稱通過綁定來指代一個對象,這個名稱可以應用在任何使用這個對象的地方。之前我們也講過名稱可以換綁成其他對象,原對象會處于無名稱的境地。del語句可以解除對象與名稱的綁定關系,也會讓對象沒有名稱。對象沒有了名稱,我們就再也無法使用它,對象將等待垃圾回收機制進行回收,對象最后會消亡,釋放相應內存。
下面是一個簡單的示例,我們先綁定了名稱,最后解除綁定,程序無法再引用此名稱,拋出一個NameError名稱錯誤。

最后一個比較特殊的名稱是單下劃線(_)。對于Python而言它并沒有什么特別的,也是合法的標識符,但是交互式解釋器會將最后一次求值的結果放到這個變量(_)中,這個標識符存儲在內置模塊builtins中。
以下為打開終端,執行python命令進入純凈的Python交互式環境中的測試代碼:

用dir()獲取builtins的所有屬性,可以看到有一個單下劃線名稱。這個機制讓我們在交互模式下查看最后一次求值結果時,不用寫其名稱,直接輸入一個下劃線即可。在JupyterLab上,由于其解釋器內核是IPython,它雖然不將下劃線指定在builtins中,但它對下劃線做了增強,同時支持兩個和三個下劃線,分別是倒數第二、倒數第三個求值結果。
下劃線的另一個常見用途是偽裝名稱,表示不會使用的變量(有人喜歡用useless)。有些場景下,我們不需要接納相應的對象或者只是臨時接納一個對象,可以用下劃線來代替。參考以下代碼:

在以上場景下,下劃線是一個非常好用的名稱。
1.4.6 常量和字面量
在編程中,需要區分字面量、變量、常量這三個概念,其中常量是我們構建基礎數據的來源,是對生命周期全局進行控制的量,變量(標識符)是方便我們操作各種對象的引用。
字面量(literal),或者叫作字面值,就是表示它本來的意思,也就是字面意思。比如字符串'hello'、數字888。Python中的字面量有字符串、字節串、數值(整數、浮點數、虛數)、省略號(...)這幾種,字面量是內置類型常量值的表示法。
定義一個字面量就是創建一個對應類型的對象,這個對象可以再去參與構造元組、列表、字典等類型的數據對象,成為這些容器中的元素。字符串、字節串可以拼接,數值可以計算,從而形成新的字面量。我們在后面講這些數據類型時會詳細介紹字面量的寫法。
列表、集合、字典以及相應的推導式(一種在它們內部寫for循環的方法而生成對應類型的寫法)所使用的以方括號([])和花括號({})來表示容器直接構造數據的方式,Python官方文檔稱為顯示(display),這是一種特殊的句法。
變量(variable)在Python語境下表達會變化的量,給人一種感覺是用來保存字面量、對象等內容,比如age=18中的age經常被稱為變量,但在Python中它是標識符,并不存儲內容,而用于指示對象的內存地址。當新的對象指向age時,如age='20歲',age的輸出值換成了一個字符串,給人感覺age的存儲內容是變化的,但其實只是它的指向發生了變化。
常量(constant)是指不會變化的量,在程序執行過程中值始終保持不變。程序級別的常量在程序執行的周期內不會發生改變,比如在一個爬蟲程序中,變量URL設置為要抓取的網址,在運行過程中URL的值保持不變,但在下次執行時URL可以更換為其他網址。語言級別的常量是語言自身定義的,編寫代碼的人不能修改,無論何種情況它都取這個值。
Python的內置常量見表1-5。
表1-5 Python的內置常量

這些常量在內置的命名空間中,不能給它們賦值,否則會引發SyntaxError(語法錯誤)。它們中有些還是關鍵字,因此我們無須定義,可以直接使用。
1.4.7 表達式
Python代碼由各種表達式(expression)和語句(statement)構成。表達式是可以求出某個值的語法單元,一般包含字面值、名稱、屬性訪問、運算符或函數調用等,它們最終都會返回一個值。
語句是一個代碼塊,可以由表達式或關鍵字(如if、while或for)構成。因此,并非所有的語言構件都是表達式,還存在不能被用作表達式的語句,如while。賦值屬于語句而非表達式。
Python的主要表達式見表1-6。
表1-6 Python的主要表達式

在交互模式下,如果結果值不為None,它會通過內置的repr()函數轉換為一個字符串,以單獨一行的形式輸出,如果結果為None,則不產生任何輸出。
表達式從左到右求值,要注意的是賦值操作求值時,右側會先于左側被求值。如下示例中,expr后邊的數字是它們的求值順序:

各個表達式如果同時存在的話,會存在優先級的問題。表1-7按從高到低、從上到下和從左到右的順序列出了Python運算符的優先級。
表1-7 Python運算符的優先級

注意,-1其實是一個位運算,數字沒有負數的字面量。冪運算是從右至左分組,如2**-1值為0.5,先計算一元位運算-1得到負值,再與2進行冪運算。%運算符也被用于字符串格式化,此時它們的優先級相同。
如果自定義數據類型要支持這些運算符,可以在類中實現對應的特殊方法(對象方法標識符以兩端下劃線命名),比如,對于加號運算符,需要實現obj.__add__(self, other)特殊方法。
關于各個表達式的編程意義和詳細語法,后文會詳細介紹。了解了表達式,我們再來看看Python的語句。
1.4.8 語句
表達式專指能夠計算出值的語句,它可以用等號賦值給一個變量,但我們日常說的語句側重于做某件事,邏輯更加復雜。Python的語句分為兩大類:簡單語句和復合語句。
簡單語句由一個單獨的邏輯行構成,表達式就屬于簡單語句。Python的簡單語句見表1-8。
表1-8 Python的簡單語句

多條簡單語句可以存在于同一行內并以分號分隔,這樣我們寫簡單語句時無須進行物理行換行,比如a=1; print(a)或者import math; math.pi。
賦值語句支持增強賦值,如a+=1表示a加1再賦值給a,在等號前增加了運算符。支持的運算符有+=、-=、*=、/=、//=、%=、@=、&=、|=、^=、>>=、<<=、**=。
賦值語句還支持帶標注賦值,如a:int=9,在標識符后增加了冒號和類型名稱,表示引用的值的類型,這將使得我們在閱讀代碼時知道變量的類型,現代IDE也會更好地為我們提示代碼。
以下是賦值語句的一些示例:


復合語句一般包含多個語句,是指包含、影響或控制一組語句的代碼。通常復合語句(如if、try和class語句等)會跨多行,雖然在某些簡單形式下整個復合語句也可能只占一行。
if、while和for語句用來實現一般的流程控制;try語句用來指定異常處理的機制和清理代碼;with語句代碼塊在開始前和結束后兩處執行初始化與終結化代碼;我們經常使用的函數和類定義在語法上也屬于復合語句。
Python的復合語句見表1-9。
表1-9 Python的復合語句

一條復合語句由一個或多個子句組成,子句包含句頭和句體,句頭處于相同的縮進層級。每個子句頭以一個作為唯一標識的關鍵字開始并以一個冒號結束。子句體是由一個子句控制的一組語句,可以是在子句頭的冒號之后與其同處一行的一條語句或由分號分隔的多條簡單語句,也可以是在其之后縮進的一行或多行語句。
關于各個語句的具體語法,我們會在后文中詳細介紹。
1.4.9 命令行執行
之前我們介紹過在交互模式下運行Python程序,你可以在終端中執行python或ipython(需要用pip安裝)命令進入交互模式,還可以在JupyterLab提供的瀏覽器界面中完成交互式代碼的執行。如果你使用的是VS Code或PyCharm這樣的IDE,會讓你創建一個擴展名為py的Python腳本文件。它們提供了運行(一般叫run)按鈕,單擊該按鈕它們會自動在終端中用命令執行腳本文件。
如果想自己在終端中執行腳本,實現一些功能,就有必要了解如何用命令行執行腳本。關于如何進入終端可以參考1.3.3節。
啟動執行腳本文件的最常見的命令是:

執行命令后,就能看到執行結果。如果腳本不在當前位置或者不想使用相對定位,可以先用cd命令并配合ls命令定位查看,確定腳本所在的目錄。假設我們編寫了一個名為hello.py的腳本:

保存后,在終端執行(輸入命令后按回車鍵),就會輸出結果:

腳本執行完成后會自動退出執行,如果執行過程中想退出,在macOS系統中按Ctrl+D組合鍵,在Windows系統中先按Ctrl+Z組合鍵,再按回車鍵。有時,需要保存腳本的輸出供以后分析用,可以執行以下命令中的一個:

如果output.txt不存在,則會自動創建;如果已經存在,第一行中,內容將被每次新的輸出替換,第二行將輸出添加到output.txt的末尾。
之前講過的內置模塊builtins中,有一個__name__名稱。如果模塊被用import語句等方式導入,那么它的值是模塊名(文件名);如果直接執行這個模塊(此文件),那么它的值就是__main__。這也是我們經常在Python腳本文件中看到以下代碼的原因。

如果是單一的腳本文件,我們也可以不判斷它的__name__,不增加這段代碼。不過,有了這段代碼,我們可以在命令行執行時不寫文件擴展名,用python-m hello來運行這個代碼中的邏輯。這就允許我們用命令行來使用一些內置的以及第三方庫的功能,比如:

以下是一些通用的功能:

我們在編寫腳本時還可以支持在命令行中傳入一些參數作為控制變量,這些內容將在后文中專門介紹。
1.4.10 執行模型
接下來探究一下Python是怎么執行的,雖然我們不需要知道太多細節,但是理解Python的執行機制能讓我們在寫代碼時做到心中有數。
簡單來說,Python解釋器會將源代碼分塊,并將這些代碼塊放在一個叫幀(frame)的容器里,幀包含它的前續幀、后繼幀以及調試信息,這些幀按照調用次序疊放在一個棧(stack)的結構里。棧的特點是后進先出(Last In First Out, LIFO),就如同一堆盤子一樣,使用時先取頂部的,而頂部的總是后堆放的。
代碼塊是被作為一個單元來執行的一段Python程序文本。被認為是代碼塊的內容大致如下:
? 交互模式下執行的每條命令都是一個代碼塊;
? 模塊、函數體和類定義分別是一個代碼塊;
? 腳本文件整體是一個代碼塊;
? 內置函數eval()和exec()被傳入的字符串與代碼對象是一個代碼塊。
Python中的對象內容存放在一個叫堆(heap)的地方,堆是沒有順序的,系統會通過復雜的算法,根據程序的需要合理分配對象存放的地方,讓內存使用更高效。棧和堆都是內存中開辟出的一塊區域。訪問堆里的對象只能通過與之綁定的名稱。Python有著良好的垃圾回收機制,對對象數據進行有效的生命周期管理,及時釋放內存空間。如圖1-6所示,名稱和對象分別在棧和堆里,其中名稱a和b都指向同一個對象。

圖1-6 堆、棧區分及功能圖
Python解釋代碼時會從代碼的邏輯入口開始,根據調用關系從上到下分析代碼,最終形成調用棧,再從頂部開始執行這個調用棧。我們來看以下代碼及其執行過程:


Python Tutor(http://www.pythontutor.com)是一個對Python運行原理進行可視化分析的工具,我們將以上代碼放到Python Tutor上測試它的執行步驟。圖1-7是代碼執行步驟的拆解。

圖1-7 代碼執行步驟拆解
首先將所有的名稱按順序載入,然后根據代碼中邏輯調用關系形成的調用棧main()→fun_a()→fun_b()來執行,右邊為棧的頂端,main()在棧底,最后由main()返回數據。
正是Python做了相關的優化、適配、調試等工作,我們才能無須關心一些計算機的底層細節,將精力放在邏輯代碼的編寫上。如果想更加詳細地了解相關內容,可以訪問https://www.gairuo.com/p/python-program-work-principle。
1.4.11 小結
本節中我們了解了Python語言最基礎的語法——代碼行、縮進和標識符,這些內容是Python在語法上最大的特色,也是我們學習Python首先要掌握的。關于PEP8規范可以參閱https://www.gairuo.com/p/python-pep8。
雖然對于初學者來說本節中有很多地方難以理解,但可以按照本節中的大綱在后邊的內容中深入學習,筑牢Python基礎,為今后的進階學習、編程應用提供強有力的保障。
- C語言程序設計實踐教程(第2版)
- oreilly精品圖書:軟件開發者路線圖叢書(共8冊)
- Learn Swift by Building Applications
- Python Network Programming Cookbook(Second Edition)
- Unity Shader入門精要
- Microsoft System Center Orchestrator 2012 R2 Essentials
- Java EE 7 Performance Tuning and Optimization
- Visual Basic程序設計實踐教程
- 微服務架構深度解析:原理、實踐與進階
- Visualforce Developer’s guide
- Scratch3.0趣味編程動手玩:比賽訓練營
- 時空數據建模及其應用
- Visual Studio Code 權威指南
- PhoneGap 4 Mobile Application Development Cookbook
- 自己動手構建編程語言:如何設計編譯器、解釋器和DSL