1. 程式人生 > >python模組與包揭祕

python模組與包揭祕

python模組揭祕

簡介

模組是最高級別的程式組織單元,它將程式程式碼和資料封裝起來以便重用。類似於c語言中include進來的標頭檔案。在python中,每一個檔案就是一個模組,並且模組匯入其它模組之後就可以使用匯入模組定義的變數名。
為什麼引入模組呢?
1. 程式碼重用
2. 系統名稱空間的劃分
3. 實現共享服務和資料

import如何工作

在閱讀本文之前,想必你一定編寫過這樣的程式碼:

import math
print math.sqrt(4)

我們首先匯入了math模組,然後利用math模組中的sqrt函式計算了4的平方根。讓我們再細緻一點來看待這個問題。
我在之前提到過,其實import也是執行了一個賦值操作,它把我們需要匯入的目標模組物件賦值給了對應的變數名,例如上例就是把math模組物件賦值給了math這個變數名,然後math所指向的模組物件中的內容(函式、最外層的變數)都可以認為是math這個物件的屬性(方法),所以我們可以用object.attr的形式來訪問。


這裡寫圖片描述
math模組物件所有的屬性

寫過c語言程式的人都喜歡把python中的import比作c中的#include,其實這是不太正確的,因為import不只是把一個檔案插入到另一個檔案,匯入時執行時的運算,程式第一次匯入指定檔案是,會執行三個步驟:

  1. 找到模組檔案
  2. 編譯成位碼(需要時)
  3. 執行模組的程式碼來建立其定義的物件

上面三個步驟都比較好理解,第三步需要記住,第一次匯入模組檔案時,模組檔案是會執行的,所以如果你的模組檔案中又print語句之類的就要注意它們是否影響你的程式了,而關於模組的搜尋路徑也需要提一下,如果你不知道可能會出一些小bug。
模組檔案的搜尋路徑:
1. 程式主目錄
2. pythonpath目錄(python的環境變數)
3. 標準連結庫目錄
4. 任何.pth檔案的內容
pythonpath就是python得一個環境變數,安裝了python的朋友應該都配置了的吧:
這裡寫圖片描述


我之前還真遇到一個與搜尋路徑相關的一個bug,當時自己也是啥都不懂(加上有點腦殘),也沒學python,我就隨便編寫了一個檔案儲存為random.py,然後這個檔案中有這麼一段程式碼:

import random
a = [1,2,3,4,5]
print random.choice(a)

random是一個內建模組,random.choice(a)就是從a列表當中任意取出一個值。本來是很簡單的程式碼,但是就是報錯,提示就是random模組沒有choice這個方法,我當時糾結了好久,真的是腦殘,現在大家應該不會再犯這種錯誤了吧。
想要知道你的機器上python模組的搜尋路徑可以檢視sys.path

import sys
print sys.path

這裡寫圖片描述

匯入模組時的一些細節

我們都知道匯入模組有import與from兩個語句,這兩個語句的區別有必要再次囉嗦一下。首先我們已經知道匯入是一種賦值操作。
import語句我們前面也說了,模組物件被賦值給了一個變數名,然後那個模組中的內容都是通過【模組名.屬性】的形式訪問的,也就是被匯入模組的名稱空間與當前檔案的名稱空間是獨立的,沒有相互汙染。
這裡寫圖片描述
其中hello模組的程式碼如下:

def test():
    print 'hello world'

from與import還是有所差異的,從某種角度來說,這種差異有時候會導致很麻煩的問題。這個差異就體現在,通過from匯入的變數名(不應該說是變數名,但是我不知道怎麼形容更好),在當前檔案中可以直接訪問,不需要再通過先前說的【模組名.屬性】的形式,例如上面的例子用from語句重寫:

from hello import test
test()

這裡寫圖片描述
這就意味著被匯入模組中的變數名可能會與當前模組中的變數名衝突(如果使用from … import * 語句尤其危險),這在一些大型專案中需要特別注意。

主要是注意from … import * 語句

實際上from只是在import的基礎上多做了一次賦值操作而已,例如下面語句:

from mdule import name1,name2

等價於

import module
name1 = module.name1
name2 = module.name2
del module

過載模組

模組只會匯入一次,如果想要再次匯入需要使用imp.reload函式。注意這裡的過載不是oop中的過載。reload函式主要是讓我們的程式變得更加動態:

  • 只會在第一次匯入時,載入和執行該模組程式碼
  • 之後得匯入只會使用已載入得模組物件
  • reload函式會強制已載入得模組得程式碼重新載入並重新執行。

reload函式可以使程式更加動態,這個動態性體現在哪裡呢?
reload可以只是修改程式的一部分,而無須停止整個程式。不知道大家是否對自己的計算機做過一些配置,該配置需要開機重啟後才能生效?這就是非動態的,如果做成動態的,那麼我們進行的配置,就會在立即生效(或者隔一定時間)。

我們通過一個例子來更直觀的看一下reload的效果:

#hello.py
print 'I am hello.py'
#hello2.py
import hello
from imp import reload
print 'I am hello2.py'
reload(hello)

這裡寫圖片描述
可以看到hello.py的內容在reload後又執行了一次!


休息一下
這裡寫圖片描述

前面我們也認識了模組的常見用法及內部機理,是時候看看包了。實際上python程式碼的目錄就稱為包,因此匯入目錄就是匯入包。事實上,包匯入是把計算機上的目錄程式設計另一個python名稱空間,而屬性則對應與目錄中所包含的子目錄或模組檔案。
現在我有如下的目錄結構:

python
    test.py
    dir0
        __init__.py
        dir1
            __init__.py
            dir2
                __init__.py
                mask.py

mask.py內容:

print 'I am mask.py'

test.py內容:

from dir0.dir1.dir2 import mask
print 'I am test.py'

執行test.py效果如下:
這裡寫圖片描述
通過結果可以看到,我們成功匯入了mask.py,而且是通過我們之前沒有接觸過的方式,其實from dir0.dir1.dir2 import mask的意思就是從from後面的目錄中匯入import語句後面的模組,這樣做的好處就是可以增加確定性,否則當你的工程過大的時候,在不同目錄下存在同名檔案,那麼使用之前的模組匯入的方式就會出現問題。
這個問題是怎麼產生的呢?例如當我自定義了一個檔案叫做string.py,然後我另一個檔案中有import string這麼一條語句,目的是匯入內建模組string,但是卻匯入了我自己寫的string.py,這是由於在python2.7中預設先搜尋當前目錄,然後再去pythonpath裡尋找,所以當程式執行到import string時,先找到了我自己寫的string.py。

關於包還有一個比較重要的地方,可能大家從我上面給的例子中也發現了這麼一個檔案__init__.py,這個檔案是把一個目錄變成一個python包的關鍵所在,它裡面可以沒有任何內容,但是必須得存在!否則在進行匯入得時候會發生錯誤。那這個檔案到底是幹嘛的呢?

其實從它的名字就可以知道它是用作初始化的,python在首次進行包匯入時,都會執行相應的__init__.py,所以如果你在這個檔案中賦值了一些變數,那麼在包匯入之後,這些變數會出現在該包的名稱空間中,而且在__init__.py中我們還常定義__all__列表。這個列表中的值指出了執行from ... import *語句時會被匯入的屬性。例如在目錄test下有如下__init__.py

#__init__.py
__all__ = ['x','y','z']

那麼在執行from test import *時,該目錄下的x,y,z模組都會被匯入。當然也可以不設定__all__,它的作用就是可以自定義哪些檔案或變數可以使用from *語句匯入(否則預設匯入該目錄下的所有)。

包相對匯入

讀者一定知道相對路徑與絕對路徑吧。這裡的相對匯入與絕對匯入其實就與他們類似。前面我們說的就是絕對匯入,直接給出一個絕對的路徑(其實也是相對路徑,看你怎麼理解),現在我們不想那麼寫,就給出一個相對於當前目錄的包路徑,這樣做就就更加明確了模組位置

from . import reverse #相對於當前目錄匯入reverse
from .. import test #相對於上級目錄匯入test
from ..hello import x #上級目錄的hello 模組下匯入x

其它

  • 前面提到了通過自定義__all__可以確定哪些模組再from *時會被匯入,其實還有一種方法,可以避免哪些模組或變數被匯入,那就是將他們命名為單下劃線開頭的名稱。
  • 還有一個用的比較廣泛的技巧:每個模組都有一個__name__屬性,如果一個檔案是作為模組被匯入的那麼__name__的值就是它的檔名,如果一個檔案是作為主程式執行的(也就是不是被匯入的),那麼它的__name__就為__main__
  • sys.path也就是模組搜尋路徑是可以被程式動態修改的
  • import與from語句都有一個as功能,就是給匯入的包起一個簡短的別名,如:
from bs4 import BeautifulSoup as bp
#bp是不是比BeautifulSoup短多了!


這裡寫圖片描述
關注web安全與python