目錄
關於這個系列
《最值得收藏的python3語法彙總》,是我爲了準備公衆號“跟哥一起學python”上面視頻教程而寫的課件。整個課件將近200頁,10w字,幾乎囊括了python3所有的語法知識點。
你可以關注這個公衆號“跟哥一起學python”,獲取對應的視頻和實例源碼。
這是我和幾位老程序員一起維護的個人公衆號,全是原創性的乾貨編程類技術文章,歡迎關注。
1、組織你的代碼
通常,我們的程序不會只有一個函數,如果功能需求稍微複雜的程序,也不會都寫在一個.py文件裏面。這就涉及到一個問題,我們該如何組織這些.py文件呢?
正如我們日常工作中將我們的文檔通過文件夾分類一樣,我們也通過.py文件以及文件夾的方式進行分類組織。一個大的項目可能有數百個.py文件,這種分類手段是有必要的,而且是必需的。
我們將.py文件稱作python的模塊(module),而將這種文件夾(帶__init__.py),稱之爲python的包(package)。
我們利用模塊和包,可以更好地組織我們項目的代碼。同時,它們也提供了類似函數那樣的代碼複用能力。
我們來看python官方手冊提供的一個典型案例,下面是一個關於“聲音處理”的包目錄層次結構:
sound/ Top-level package
__init__.py Initialize the sound package
formats/ Subpackage for file format conversions
__init__.py
wavread.py
wavwrite.py
aiffread.py
aiffwrite.py
auread.py
auwrite.py
...
effects/ Subpackage for sound effects
__init__.py
echo.py
surround.py
reverse.py
...
filters/ Subpackage for filters
__init__.py
equalizer.py
vocoder.py
karaoke.py
...
“聲音處理”包含了很多功能,比如對各種聲音格式的支持、音效增強、過濾器等等。我們將這些功能進行分層拆分,分別放在不同的.py和package中實現。
這裏的sound是一個頂層的package,而formats、filters、effects是它的子package,package支持這樣的層層嵌套,和文件夾目錄一樣。需要注意的是,只有包含了__init__.py的目錄纔會被認爲是一個package,否則它就是一個普通的文件夾。__init__.py可以爲空,後面我們會詳細介紹它的寫法。
這裏面的所有.py文件都是模塊。模塊裏面的內容沒有要求,可以包含變量、函數、類,也可以爲空。
Python解釋器本身提供了龐大的標準庫,其本質就是模塊或者包。我們使用比較多的re、datetime、copy、array、enum、os、sys、io等等都是標準庫。
你可以通過查詢官方的標準庫參考手冊:
https://docs.python.org/zh-cn/3/library/index.html
後面我們也會重點介紹一些常用的標準庫。
2、Import導入模塊
-
三種導入方式
模塊和包,是可以被複用的。比如上面的sound包,我們可以在其它的python程序中導入它,並調用它裏面的函數、變量或者類。
這一過程,通過Import機制來實現,如下是最簡單的方式:
# author: Tiger, 關注公衆號“跟哥一起學python”,ID:tiger-python
# file: ./10/10_1.py
# 模塊
import keyword
print(keyword.kwlist)
我們通過import導入了一個模塊keyword,並接下來獲取了它的一個變量kwlist打印出來。
Import語句需要寫在使用它的代碼之前,而通常我們是建議所有的import語句都寫在文件的開頭。
我們看看keyword.py是什麼樣的?它非常簡單:
# LIB/keyword.py
__all__ = ["iskeyword", "kwlist"]
kwlist = [
'False',
'None',
'True',
'and',
'as',
'assert',
'async',
'await',
'break',
'class',
'continue',
'def',
'del',
'elif',
'else',
'except',
'finally',
'for',
'from',
'global',
'if',
'import',
'in',
'is',
'lambda',
'nonlocal',
'not',
'or',
'pass',
'raise',
'return',
'try',
'while',
'with',
'yield'
]
iskeyword = frozenset(kwlist).__contains__
第一行,給一個叫__all__的變量進行了賦值。這個變量有特殊含義,它是給python解釋器看的,它表示在import該模塊時需要導入的符號列表。
第二行,定義了一個變量kwlist,是一個列表,它裏面存了python的所有保留字。
第三行,定義iskeyword是frozenset的一個內建函數__contains__,它判斷一個字符串是不是kwlist裏面的保留字。
我們還可以通過from xxx import xxx的方式導入:
from keyword import kwlist
print(kwlist)
這種導入比前面的要精確,範圍更小。這種導入方式適合你明確知道你需要的功能的時候,比如這個例子中,我明確知道我只需要打印kwlist,所以可以指定只導入kwlist。
這種方式在使用時會顯得更加簡潔,我們只需要使用kwlist即可,而不用像上面那樣寫keyword.kwlist。
另一個好處是,第一種方式會把這個模塊的所有符號一股腦的全部導入進來,而第二種方式則更加精確。當我們需要使用同一個模塊中的多個函數或者變量時,我們可以採用第一種方式,因爲採用第二種方式的話我們會寫多次from xxx import xxx。
無論上面哪種方式,它們都存在命名衝突的風險。比如我們自己的程序中可能已經存在一個變量叫做keyword或者kwlist了。這時候我們需要爲我們導入的模塊或者函數起一個別名,如下:
import keyword as kwmodule
from keyword import kwlist as kwall
print(kwmodule.kwlist)
print(kwall)
我們通過as xxx來起別名,如上kwmodule就是keyword模塊的別名,而kwall就是變量kwlist的別名。在下面的使用中,我們就只能使用這個別名。這就解決了命名重複的問題。
第二種導入方式也可以導入模塊中所有的符號(變量、函數、類等),需要使用*通配符來表示。它和第一種方式的全部導入,是有區別的。第一種方式導入的符號是keyword,模塊中的所有符號都包含在keyword裏面,所有調用方式都必須是keyword.xxx的方式。而採用*號通配符,則是將模塊中的符號全部導入進來,可以直接調用。
我們可以通過dir(obj)內建函數,打印出某個對象的符號表,如果不帶參數,則打印當前模塊的符號表。
import keyword
print(dir())
print(dir(keyword))
輸出爲:
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'keyword']
['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'iskeyword', 'kwlist']
前面那些__XXX__的大家先不用關注,他們是python對象自帶的符號。我們看到,這種import方式,它只導入了keyword。而keyword裏面包含了iskeyword和kwlist。
from keyword import *
print(dir())
輸出爲:
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'iskeyword', 'kwlist']
這種方式是直接把模塊中的符號導入進來了,平鋪在當前符號表中。
*號通配符的方式,通常不被推薦使用。一個重要的原因是,如果模塊中的符號很多的時候,你很難保證不出現重名的情況,重名導致的覆蓋會讓你的程序變得很難看懂,而且容易出錯。另外,我們的程序中通常會導入多個外部模塊,要保證這些模塊中的符號相互之間不命名衝突,幾乎是不可能的。
-
__name__的作用
一個模塊只會被導入一次,不管你執行了多少次import。這樣可以防止導入模塊被一遍又一遍地執行。
在第一次導入時,模塊中的代碼會被依次執行。你可以理解爲,Python解釋器在import的時候,將對應的模塊解析並執行了一遍。
我們定義了一個模塊,用於管理“水果清單”,命名爲fruits.py。
# author: Tiger, 關注公衆號“跟哥一起學python”,ID:tiger-python
# file: ./10/
# 這是一個用於演示模塊的例子
fruits = {'apple': 10, 'pear': 5}
print(f"use module: fruits. {fruits}")
def get_fruits():
return fruits
def add_fruits(*args):
if len(args) == 0:
return
for item in args:
fruits[item[0]] = item[1]
def del_fruit_one(name):
fruits.pop(name)
我們在另外一個文件中,用不同的方式導入這個模塊。
# author: Tiger, 關注公衆號“跟哥一起學python”,ID:tiger-python
# file: ./10/10_2.py
# 模塊
import fruits
import fruits as ff
from fruits import add_fruits
add_fruits(['orange', 8])
print(fruits.fruits)
輸出爲:
use module: fruits. {'apple': 10, 'pear': 5}
{'apple': 10, 'pear': 5, 'orange': 8}
我們可以看到,我們導入了三次,但是模塊中的這行打印語句print(f"use module: fruits. {fruits}")只被執行了一次。
這樣的設計是很有用的,在一些複雜的項目中,有些模塊會被層層導入。比如:A.py 導入了B.py,而C.py同時導入了A.py和B.py,對於C來說,B是被重複導入了的。在C語言中,我們需要給每個頭文件定義一個宏,就是爲了防止被重複導入。Python已經幫我們解決了這個問題。
模塊中的可執行代碼通常用於模塊的一些初始化工作,比如給一些全局變量賦值等。但是對於某些代碼,我們可能不太希望在導入時被執行,比如對模塊功能的一些測試代碼。對於這種代碼,我們可以通過判斷__name__等於__main__來避免被執行。
如下,我們給fruits模塊添加了一些不希望被執行的測試代碼:
# author: Tiger, 關注公衆號“跟哥一起學python”,ID:tiger-python
# file: ./10/
# 這是一個用於演示模塊的例子
fruits = {'apple': 10, 'pear': 5}
print(f"use module: fruits. {fruits}")
def get_fruits():
return fruits
def add_fruits(*args):
if len(args) == 0:
return
for item in args:
fruits[item[0]] = item[1]
def del_fruit_one(name):
fruits.pop(name)
# for test, 在被其它程序導入時,不會被執行
if __name__ == '__main__':
add_fruits(['orange', 8])
del_fruit_one('apple')
print(get_fruits())
當我們直接運行fruits.py這個模塊時,它的輸出爲:
use module: fruits. {'apple': 10, 'pear': 5}
{'pear': 5, 'orange': 8}
可以看到,最後那段測試代碼是被執行了的。而當我們import模塊時,這部分代碼是不會被執行的。
if __name__ == '__main__'
這個語句被廣泛使用,它並不是什麼特殊的語法,它就是一個普通的if條件判斷。只不過我們利用了__name__這個屬性的特點。所有的模塊都有__name__這個屬性,它表示當前模塊的名字。如果當前是主運行模塊(就是python解釋器直接運行的那個.py文件),那麼它的__name__就是“__main__”。而對於import的模塊,__name__則是模塊名,本實例中爲‘fruits’。
所以,當fruits.py被直接運行時,那段測試代碼可以被執行。當作爲模塊被導入時,則不會執行。我們同樣可以通過斷點來查看:
寫習慣了C語言的同學一定知道,C語言的程序入口是main()函數。而python是解釋性語言,它是從.py文件的第一行依次往下解析並執行的。如果你也想在python中定義一個執行入口,那麼可以使用這個__name__的條件判斷。
做法是,將除了全局變量定義、函數定義、類定義等定義相關的代碼,都放到這個條件判斷裏面。這樣,python解釋器在解析前面的定義代碼的時候,不會實質性執行任何邏輯。只有到了這裏纔會執行,相當於是一個程序執行的入口。
這種寫法是非常普遍的,大家一定要學會。
-
搜索路徑
當我們import某個模塊的時候,系統是如何知道這個模塊放在哪裏的呢?這就是import的搜索路徑問題。
Python將搜索路徑存儲在sys模塊的path變量中,它是一個列表結構,裏面存儲了多個路徑。Import時,系統會依次在這些路徑下面去找對應的模塊。我們可以將這個path變量打印出來看看,因爲這個比較簡單,我們直接在cmd中輸出:
>>> import sys
>>> sys.path
['',
'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\python38.zip', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\DLLs', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\lib', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python38\\lib\\site-packages']
Sys.path裏面定義了多個路徑,其中第一個路徑是一個空字符串,這表示在當前目錄下搜索(運行python解釋器的路徑)。在import時,首先會在python內置庫中進行搜索,找不到才從path裏面逐個搜索。
Sys.path是可以修改的,我們可以把我們期望的搜索路徑加入到列表裏面去。比如,我們新建一個目錄叫pyfruits,然後把fruits.py剪切到這個目錄下。這樣,我們import fruits時,是找不到模塊的,會報錯。
ModuleNotFoundError: No module named 'fruits'
這時,我們可以將pyfruits路徑加入到sys.path中去。
import sys
sys.path.append('D:\\跟我一起學python\\練習\\10\\pyfruits')
import fruits
這個代碼就不會報錯,因爲系統會自動到pyfruits目錄下面去搜索該模塊。
-
循環導入問題
Python支持模塊之間的循環導入,如下:
# a.py
import b
def func_a():
pass
# b.py
import a
def func_b():
pass
# author: Tiger, 關注公衆號“跟哥一起學python”,ID:tiger-python
# file: ./10/10_4.py
# 模塊間循環導入
import a
a.func_a()
上面定義了兩個模板a.py和b.py,它們之間相互導入對方。這就是循環導入,python支持這種導入方式。
循環導入很容易出錯,所以我們需要深入去理解python導入模塊的過程,才能避免這些錯誤,比如我們將上面的導入方式改爲 from xxx import xxx,就會報錯:
# a.py
from b import func_b
def func_a():
pass
# b.py
from a import func_a
def func_b():
pass
# author: Tiger, 關注公衆號“跟哥一起學python”,ID:tiger-python
# file: ./10/10_4.py
# 模塊間循環導入
import a
a.func_a()
這種循環導入會導致異常:
ImportError: cannot import name 'func_a' from partially initialized module 'a' (most likely due to a circular import)
它的意思是,由於循環導入的原因,模塊a沒有完全初始化,所以func_a找不到。
我們通過下圖來分析一下整個過程:
我們解釋一下上圖的過程:
- 在我們的主文件裏面執行導入語句 import a。解釋器會到sys模塊的modules變量裏面去查找,是否已經存在a。sys的modules是一個字典數據類型的變量,它存儲了當前運行環境中所以已經加載過的modules。如果a從未被加載過,那麼會創建一個module a的對象,並存儲在sys.modules中。
- 填充module a的__dict__變量,這個變量是用來存儲module a的屬性的,包括變量、函數、類等等。解釋器從a.py的第一條語句開始解析,由於第一條語句是from b import func_b,所以需要先導入模塊b。
- 同樣,解釋器會到sys模塊的modules變量裏面去查找,是否已經存在b。由於是第一次加載,所以需要新創建module b對象,並保存在sys.modules中。
- module b的__dict__變量同樣爲空,所以需要填充。同模塊a的過程一樣,解釋器會去逐條解析b.py,由於第一條語句是 from a import func_a,所以又需要先導入模塊a。到這裏,其實已經形成了一個循環。
- 此時,再導入a時,我們在sys.modules中能查詢到module a了。因爲我們真正要導入的是模塊a中的func_a這個屬性,所以還需要去查詢module a的__dict__裏面是否存在func_a。很顯然,__dict__還是空的,沒有找到func_a,所以就報出來前面的異常信息。
事實上,解釋器還沒來得及解析a.py裏面的func_a語句,就被打斷了而去導入b。這就是循環導入導致這個錯誤的根本原因。
這個例子和我們第一個例子的區別在於,第一個例子我們是import module,而第二個例子我們是from module import attr,可以認爲它除了import module之外,還需要導入屬性attr,所以它比第一個例子多了一個查找__dict__的動作。這也就是爲什麼第二個例子報錯,而第一個例子沒有報錯的原因。
我們拋開這個具體的例子來看,循環導入錯誤的根本原因是:模塊的屬性在還沒有被解析到__dict__之前就被引用了。所以,我們要儘量避免這種情況的發生。
通常我們有幾種方法來解決這個問題:
- 從代碼組織架構層面,儘量避免出現模塊的循環導入。這是解決問題的最高級的方法,也是我們代碼架構的一個重要原則。
- 在真正需要使用的時候才導入模塊,這樣可以把導入這個動作儘量滯後,從而讓屬性得到解析。
- 不要用from module import attr,而用import module。它的原理和第2點其實本質上是一樣的。
對於上面的第2點解決方案,我們可以看下面的例子:
# a.py
from b import y
x = 100
def func_a():
print(f"this is func_a, y is {y}")
pass
# b.py
from a import x
y = 200
def func_b():
print(f"this is func_b, x is {x}")
pass
# file: ./10/10_4.py
# 模塊間循環導入
import a
a.func_a()
根據我們前面的分析,這樣寫會報錯,因爲找不到屬性x。
X和y兩個屬性其實都是在函數func_a和func_b裏面使用的,所以我們可以把import語句放到函數裏面去執行。
# a.py
# from b import y
x = 100
def func_a():
from b import y
print(f"this is func_a, y is {y}")
pass
# b.py
# from a import x
y = 200
def func_b():
from a import x
print(f"this is func_b, x is {x}")
pass
# file: ./10/10_4.py
# 模塊間循環導入
import a
a.func_a()
這樣就可以正常運行。因爲module b是在我們真正調用func_a時才被加載的,這就達到了所謂的延遲加載效果。我們可以通過下面代碼測試一下:
import a
from sys import modules
print(f"module a is loaded? {'a' in modules}")
print(f"module b is loaded? {'b' in modules}")
a.func_a()
print(f"module a is loaded? {'a' in modules}")
print(f"module b is loaded? {'b' in modules}")
輸出爲:
module a is loaded? True
module b is loaded? False
this is func_a, y is 200
module a is loaded? True
module b is loaded? True
可以看到,在a.func_a()之前,module b是沒有被加載的。
循環導入容易出錯,而且不好理解影響代碼可讀性。所以大家在模塊設計階段應該儘可能避免出現循環導入的情況。如果實在無法避免,那麼要保證屬性在解析後纔會被引用。
3、構造包
前面我們講了,python的包其實對應的就是文件系統裏面的文件夾。但是,文件夾不全是python包。要形成python包,還必須滿足一個條件,就是這個文件夾裏面必須包含一個__init__.py文件,這個文件可以爲空,但是不能沒有。
前面的例子中,我們的目錄結構如下:
pyfruits/ dir
fruits.py module
這裏面沒有包含__init__.py,所以它沒有形成python包。
我們在裏面新建一個空的__init__.py,讓它成爲一個包。
pyfruits/ dir
__init__.py Initialize the package
fruits.py module
當然,__init__.py可以不爲空,那麼這個文件到底有什麼作用呢?
這個文件可以設置一個很重要的變量__all__,它是一個列表結構。大家還記得import模塊的時候,我們可以使用通配符*來導入該模塊的所有符號嗎?這個__all__就是用來指定可以導入的符號列表的。
同樣,在導入包的時候,我們也可以通過通配符*來導入該包下面的所有模塊。在__init__.py裏面設置__all__變量,可以指定可以導入的模塊列表。
比如,我們看python的一個標準庫email,它的__init__.py文件中設置了__all__,如下:
__all__ = [
'base64mime',
'charset',
'encoders',
'errors',
'feedparser',
'generator',
'header',
'iterators',
'message',
'message_from_file',
'message_from_binary_file',
'message_from_string',
'message_from_bytes',
'mime',
'parser',
'quoprimime',
'utils',
]
當我們不設置__all__時,系統只會確保__init__.py裏面的代碼被執行,如果這個文件中定義了變量或者函數之類的符號,也可以被導入。但是這個包裏面的模塊不會被導入。
__init__.py裏面也可以寫功能代碼,但是通常我們不會這樣做,功能代碼我們會放在單獨的功能模塊裏面實現。我們要儘量保證這個文件是輕量級的。
除了__all__以爲,你可以在這個文件裏面定義一些諸如版本號、作者等變量信息,也可以做一些簡單的API封裝。
__all__也可以直接寫在.py模塊的頭部,用於指定該模塊中哪些符號可以被導入。但是它的默認導入行爲和包是不一致的,模塊不設置__all__,默認全部導入。
4、Import導入包
所有的編程語言,幾乎都使用一個點號(.)來表達層次關係。Package本質上是將模塊組織成了一種層次化的結構。所以,我們採用點號來描述package和module之間的這種層次關係。
類似於模塊的導入,如果我們要從包中導入fruits模塊,也有三種方式。
方式一、
import pyfruits.fruits
print(pyfruits.fruits.fruits)
這種直接導入的方式,在調用時需要寫完整的從包到模塊的路徑。顯得很冗長,如果有多層包結構,這個調用名字會更長,所以我們也可以給它取一個別名。
import pyfruits.fruits as f
print(f.fruits)
這樣的代碼就會顯得清爽很多了。
方式二、
from pyfruits import fruits
print(fruits.get_fruits())
當然你也可以from到模塊那一層,然後import模塊裏面的一個符號。這個和模塊的導入是一模一樣的。
from pyfruits.fruits import get_fruits
print(get_fruits())
方式三、
from pyfruits import *
print(fruits.fruits)
注意我們需要將fruits模塊添加到__init__.py中:
# __init__.py
__all__ = ['fruits']
同樣,採用通配符*導入所有模塊的方式是不推薦的,除非你的程序要使用該包下面的大部分模塊。
如果我們在__init__.py中定義了符號(變量或者函數等),那麼它們可以被視作是包的屬性,比如:
# __init__.py
__version__ = '1.0.0'
__author__ = 'tiger'
def hello():
return 'hello'
import pyfruits
print(pyfruits.hello())
print(pyfruits.__version__)
print(pyfruits.__author__)
python包是可以層層嵌套的,就像文件系統的目錄結構一樣。不同層級的包定義和導入規則都是一樣的,我們就不展開贅述了。
5、預編譯的模塊
我們知道python是一門解釋性的語言,它不會像C語言那樣,會先編譯成一個二進制的可執行文件。但是,這並不意味着python就沒有編譯過程。
Python代碼的運行也會先進行編譯,只是它生成的是一個叫字節碼的文件.pyc,這個文件裏面存的不是機器碼,所以計算機是無法直接運行的。這個文件需要通過解釋器逐行解釋並運行。
當我們import一個模塊的時候,會先將這個模塊進行編譯生成.pyc文件,然後加載。爲了提高這個加載過程的效率,這些編譯過的.pyc文件會被緩存在__pycache__目錄下面。當這個模塊再次被加載,就直接加載.pyc文件,這樣就提升了模塊加載的性能。
緩存的.pyc文件以這樣的方式命名:模塊名.解釋器-版本號.pyc。
系統會自動檢查是否需要重新編譯模塊,它會以.py文件的更新時間作爲判斷依據。
需要注意的是,.pyc的緩存機制,只會提升模塊加載階段的性能,對運行性能沒有任何影響。
.pyc是一種字節碼文件,這些字節碼只有python解釋器自己能理解。它是操作系統無關的,也就是說我們在windows上面編譯的.pyc可以拿到linux上面去運行。這有點類似於JAVA的.class文件。所以,我們把.pyc的運行環境也可以稱作python虛擬機PVM,類似於JAVA虛擬機JVM。
在實際的項目發佈時,如果我們不想讓自己的源代碼暴露給用戶,也可以預編譯出這些模塊的字節碼文件,將它們提供給用戶。我們可以手動生成這些.pyc文件,一種方法是在cmd命令行中帶-m參數:
python -m fruits.py
也可以通過py_compile模塊來編譯。
# author: Tiger, 關注公衆號“跟哥一起學python”,ID:tiger-python
# file: ./10/10_2.py
# 批量編譯模塊
import py_compile
py_compile.compile('./pyfruits/fruits.py')
關於項目發佈的話題,我們會在整個教程的最後給大家講解。
本文有視頻講解,視頻和實例源碼下載方式:點擊->我的主頁,查看個人簡介。
我儘量堅持每日更新一節。
更多python教程,請查看我的專欄《0基礎學python視頻教程》