《最值得收藏的python3語法彙總》之模塊與包機制

目錄

關於這個系列

1、組織你的代碼

2、Import導入模塊

三種導入方式

__name__的作用

搜索路徑

循環導入問題

3、構造包

4、Import導入包

5、預編譯的模塊


 

關於這個系列

《最值得收藏的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找不到。

我們通過下圖來分析一下整個過程:

 

我們解釋一下上圖的過程:

  1. 在我們的主文件裏面執行導入語句 import a。解釋器會到sys模塊的modules變量裏面去查找,是否已經存在a。sys的modules是一個字典數據類型的變量,它存儲了當前運行環境中所以已經加載過的modules。如果a從未被加載過,那麼會創建一個module a的對象,並存儲在sys.modules中。
  2. 填充module a的__dict__變量,這個變量是用來存儲module a的屬性的,包括變量、函數、類等等。解釋器從a.py的第一條語句開始解析,由於第一條語句是from b import func_b,所以需要先導入模塊b。
  3. 同樣,解釋器會到sys模塊的modules變量裏面去查找,是否已經存在b。由於是第一次加載,所以需要新創建module b對象,並保存在sys.modules中。
  4. module b的__dict__變量同樣爲空,所以需要填充。同模塊a的過程一樣,解釋器會去逐條解析b.py,由於第一條語句是 from a import func_a,所以又需要先導入模塊a。到這裏,其實已經形成了一個循環。
  5. 此時,再導入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__之前就被引用了。所以,我們要儘量避免這種情況的發生。

 

通常我們有幾種方法來解決這個問題:

  1. 從代碼組織架構層面,儘量避免出現模塊的循環導入。這是解決問題的最高級的方法,也是我們代碼架構的一個重要原則。
  2. 在真正需要使用的時候才導入模塊,這樣可以把導入這個動作儘量滯後,從而讓屬性得到解析。
  3. 不要用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視頻教程》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章