天天看點

python子產品化包管理與import機制1. 子產品化程式設計2. Python中的子產品3. import語句4. 動态導入5. 搜尋路徑6.相對導入與絕對導入7. 交叉引用(導入循環)8. 安裝第三方包

我們在學 python的時候,大多數都是從

print("hello,world")

開始,這一行代碼,敲開了每一位工程師新世界的大門!

然後我們開始學文法、變量、函數、條件控制、資料結構、面向對象,然後迫不及待的與bug過招。在這個過程中,有一位朋友一直默默陪伴着我們,但是我們卻從來沒有關注過它。他就是我們的

import

兄弟

import os
           

在我們的代碼中,它扮演這不可或缺的角色。但是卻很少有人真正懂它。正是因為有了他,python的強大之處才能得以發揮,今天讓我們一起好好了解一下它。

1. 子產品化程式設計

在我們真實的項目中,代碼量可能達到幾十萬、幾百萬行。如果我們把這幾十萬、幾百萬行代碼都寫在一個檔案裡面。那後果是非常嚴重的,首先我們的代碼檔案将會非常大,其次是想通過肉眼去找到我們的代碼,難度也是非常大,更别說看懂其中的代碼邏輯了。

出現了子產品化程式設計的想法。子產品化程式設計有助于開發者統籌兼顧和分工協作,并提升代碼靈活性和可維護性。比如說衆多工程師共同開發同一個系統。A做登入功能的邏輯,B做注冊功能的邏輯,C做使用者管理的邏輯。這些代碼是分離的,通過子產品化組裝的方式把他繼承到同一個系統中。

子產品化程式設計:将一個完整系統的代碼,拆分成一個一個小子產品

舉個例子:

(1)非子產品化項目:項目裡面隻有一個py檔案 main.py

def login():
    #這裡完成登入邏輯的代碼編輯
    pass

def register():
    # 這裡完成注冊邏輯的代碼編寫
    pass

def user_manage():
    # 這裡完成使用者管理的代碼編寫
    pass

if __main__ == "__main__":
    login()
    register()
    user_manage()
           

(2)子產品化的項目:項目中包含

main.py

login.py

register.py

user_manage.py

  • login.py
    def login():
        #這裡完成登入邏輯的代碼編輯
        pass
               
  • register.py
    def register():
        # 這裡完成注冊邏輯的代碼編寫
        pass
               
  • user_manage.py
    def user_manage():
        # 這裡完成使用者管理的代碼編寫
        pass
               
  • main.py
    from login import login
    from register import register
    from user_manage import user_manage
    
    if __main__ == "__main__":
        login()
        register()
        user_manage()
               

其實,Python之是以這麼強大,這個特性發揮了很大的作用。我們需要的很多功能不用自己去寫,通過導入别人寫好的子產品,我們可以直接使用,這樣可以大大提供效率。比如我們想要寫一個爬蟲,假設沒有子產品化的助力,我們需要從0開始編寫,http請求、TCP連接配接、傳回處理、底層資料包封裝、解析。但是有了子產品化,我們隻需要一行代碼,開箱即用,是不是非常友善呢

import requests
           

2. Python中的子產品

說了子產品化程式設計,那麼在Python中,我們的子產品到底指的是什麼呢?在Python中,我們需要區分幾個概念:

  1. 子產品:一個字尾為

    .py

    的代碼檔案就是一個子產品
  2. 包:一個包含很多

    .py

    檔案的檔案夾。(Python3.3之前要求這個檔案夾中必須含有

    __init__.py

    檔案)
  3. 庫:可能由多個包和子產品組成,可以認為是一個完整項目的打包。

3. import語句

3.1 從子產品導入

python子產品化包管理與import機制1. 子產品化程式設計2. Python中的子產品3. import語句4. 動态導入5. 搜尋路徑6.相對導入與絕對導入7. 交叉引用(導入循環)8. 安裝第三方包
python子產品化包管理與import機制1. 子產品化程式設計2. Python中的子產品3. import語句4. 動态導入5. 搜尋路徑6.相對導入與絕對導入7. 交叉引用(導入循環)8. 安裝第三方包

(1)全量導入:

全量導入會将子產品内的所有全局變量、函數、類等等全部都導入進來。

全量導入一個子產品的所有内容有兩種方式:

import xxx

import test

print(dir(test)
# 檢視導入了什麼
# 發現我們定義的Hello類、hello函數、name變量全部都被導入進來了
# 并且還有一些其他的東西
['Hello', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'hello', 'name']

test.hello()
print(test.name)
# 上面方法需求使用:子產品名.變量名(方法名)引用,因為隻是引入子產品整體,并沒有把子產品裡面的内容單獨引入
           

from xxx import *

from test import *

hello()
Hello()
print(name)
# 以上可以直接使用,因為 from test import * 已經将test子產品的所有内容單獨導入進來。
           

(2)局部導入

from xxx import xxx

from test import hello  # 隻從test子產品中導入hello函數,别的不導入
from test import hello, Hello  # 導入多個

hello()
           

給導入的内容設定别名:

from xxx import xxx as yyy

from test import hello as hello_func  # 從test子產品中導入hello函數,并且設定别名為:hello_func

hello_func()
           

3.2 從python包中導入

python子產品化包管理與import機制1. 子產品化程式設計2. Python中的子產品3. import語句4. 動态導入5. 搜尋路徑6.相對導入與絕對導入7. 交叉引用(導入循環)8. 安裝第三方包
|——test_package
	|——__init__.py
	|——test.py
	|——test2.py
|——main.py
           

main.py:

大家猜猜,下面這兩段代碼能運作嗎?

import test_package

test_package.test.hello()

Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 8, in <module>
    test_package.test.hello()
AttributeError: module 'test_package' has no attribute 'test'
           
from test_package import *

test.hello()

Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 10, in <module>
    test.hello()
AttributeError: module 'test' has no attribute 'hello'
           

看到這裡,可能大家臉上都出現了三個問号???為什麼導入子產品的方法放在導入包這裡不好使了呢?

其實啊,我們的python在導入一個子產品的時候,會把我們的子產品

.py

檔案執行一遍,然後生成一個子產品對象,子產品中我們定義的函數、變量、類會添加到這個子產品對象的屬性裡面:這就是為什麼我們可以通過

test.hello()

,因為hello()是test的一個屬性.

那導入包的時候呢?我們知道python的包本質上是一個檔案夾,檔案夾是不能被編譯執行的。那為什麼還能import呢?實際上,我們在import一個包的時候,執行的是這個包裡面的

__init__.py

檔案,也可以了解為導入包的時候,隻是導入了

__init__.py

因為我們

__init__.py

是空的,是以我們導入了個寂寞。

(1)通過導入子產品的方式:

from test_package import test
test.hello()

from test_package.test import hello
hello()
           

(2)通過添加

__all__

屬性:

__init__.py

檔案: 添加

__all__

屬性

__all__ = ["test", "test2"]
           

main.py檔案:

from test_package import *

# 通過在 __init__.py中定義了 __all__屬性,在導入的時候,可以把該屬性列出的子產品全部導入 
test.hello()
test2.hello2()
           

__all__

是針對子產品公開接口的一種約定,以提供了”白名單“的形式暴露接口。如果定義了

__all__

,其他檔案中使用

from xxx import *

導入該檔案時,隻會導入

__all__

列出的成員

__all__

僅對于使用

from module import *

這種情況适用。

4. 動态導入

上面我們介紹了比較主流的

import

語句導入,其實在python中還有其他的導入方式

4.1

__import__()

__import__()

函數可用于導入子產品。其實當我們使用import導入Python子產品的時候,預設調用的是

__import__()

函數。直接使用該函數的情況很少見,一般用于動态加載子產品。

__import__(name, globals=None, locals=None, fromlist=(), level=0)

參數:

  • name:要導入的子產品名,可使用變量
  • globals和locals:通常使用預設值。使用給定的globals和locals變量來決定如何在一個包上下文中解析name
  • fromlist:指定要導入的子子產品名或對象名,它們會按名稱從子產品導入
  • leve:指定導入子產品的方式。level為0則絕對導入; level為正值則表示相對于調用__import __()的子產品目錄,要搜尋的父目錄數
os_obj = __import__("os")

print(os_obj.getcwd())
           
os_obj = __import__("test_package.test")

os_obj.test.hello()
           

4.2 importlib

importlib 是 Python 中的一個标準庫,importlib 能提供的功能非常全面

import importlib
myos=importlib.import_module("os")
myos.getcwd()
           

4.3 一個使用場景

一個定時任務的場景,資料庫存了許多這樣的定時任務:cmdb.tasks.get_host_info,它表示的是調用cmdb包下的tasks子產品下的get_host_info函數。我們怎麼實作,通過這種格式的字元串,去調用相應的函數呢?

大家可以思考思考怎麼做。

def exec_task(task_name):
	if not task_name:
		return -1, "task name must not None"
	try:
    	module_name = task_name.rsplit(".", 1)[0]
        method_name = task_name.rsplit(".", 1)[1]
        
        # 動态導入tasks子產品
        module_obj = __import__(module_name)
        if not hasattr(module_obj, method_name):
			return -1, "function not found"
    except:
    	return -1, "has Error"
        
    task = getattr(module_obj, method_name)
    task()
           

5. 搜尋路徑

不知道大家有沒有思考過這樣一個問題,當我們導入一個子產品或者導入一個包的時候,python是去哪裡尋找這個子產品的呢?

Python搜尋子產品的路徑是由四部分構成的:

  • 程式的主目錄、
  • PATHONPATH目錄
  • 标準連結庫目錄
  • .pth檔案的目錄,

這四部分的路徑都存儲在sys.path 清單中。

pth檔案:pth檔案用于添加額外的sys.path即python檢索路徑,一般在github上下載下傳的程式包會有一個setup.py,執行該檔案會在(目前python環境下的site-packages檔案夾生成)一個.pth檔案
import sys
print(sys.path)

[
    '/app/util-python/python-module',    # 目前程式所在的目錄 
    '/usr/lib/python37.zip', 
    '/usr/lib/python3.7', 
    '/usr/lib/python3.7/lib-dynload', 
    '/app/python-virtualenv/aioms-env/lib/python3.7/site-packages'  # 下載下傳的第三方包目錄
]
           

當我們的 導入一個包的時候,python解釋器依次在這些目錄搜尋。如果這些目錄中沒有找到,程式就會報錯

假設有一個檔案:

/app/test_pack.py

我們現在的程式路徑為:

/app/util-python/python-module/main.py

import test_pack

Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 5, in <module>
    import test_pack
ModuleNotFoundError: No module named 'test_pack'
           

沒有任何意外,報錯了,找不到這個包

import sys
sys.path.append('/app')

import test_pack

test_pack.hello()
# 測試導入自定義路徑

print(sys.path)
[
    '/app/util-python/python-module',
    '/usr/lib/python37.zip', 
    '/usr/lib/python3.7', 
    '/usr/lib/python3.7/lib-dynload', 
    '/app/python-virtualenv/aioms-env/lib/python3.7/site-packages', 
    '/app' # 我們發現多了一個搜尋目錄,它在這個目錄下找到了我們的test_pack.py
]
           
是以:子產品與包的搜尋路徑不是固定不變的,我們可以自定義它,當然上面的方法隻是暫時的

是以,當我們程式出現這個錯誤的時候:

ModuleNotFoundError: No module named 'test_pack'

問題排查兩部曲:

  1. 檢視下載下傳安裝的包的路徑在什麼地方
  2. 使用sys.path看看,下載下傳完成的包在不在這裡面

6.相對導入與絕對導入

假設包結構:

main.py
packageA/
    __init__.py
    moduleA.py
    moduleB.py
packageB/
	__init__.py
	moduleA.py
	moduleB.py
           

對于

packageA/moduleA.py

絕對導入:所有的子產品import都從“根節點”開始。根節點的位置由sys.path中的路徑決定

from packageA import moduleB
           

相對導入:隻關心相對自己目前目錄的子產品位置

  • . 目前目錄同級查找
  • .. 目前目錄上級查找
from . import moduleB
           

7. 交叉引用(導入循環)

什麼叫做交叉導入,就是兩個包互相導入

main.py
package/
    __init__.py
    moduleA.py
    moduleB.py
           
from . import moduleB
           

這裡不想拓展太多了,推薦大家都使用絕對導入就完事了!免得給自己挖坑

moduleA.py:

from moduleB import hello_b
           

moduleB.py:

from moduleA import hello_a
           

在這裡面:子產品A調用了子產品B的某方法、而子產品B也調用了子產品A的某方法,這個就叫交叉導入

首先我們看看這樣會導緻什麼問題

ImportError: cannot import name 'hello_b' from 'moduleB'
           

這樣将會抛出異常。異常的原因是:在導入的時候,moduleA需要通路moduleB的hello_b,但是沒有hello_b還沒有初始化完成。是以就會抛出異常

通常來說:這是由于大型的Python工程中,架構設計不當,出現的子產品間互相引用的情況

解決辦法:

  1. 架構優化,解除互相引用的關系
  2. import語句放置在子產品的最後
  3. import語句放置在函數中

8. 安裝第三方包

什麼是第三方包呢?在程式設計的圈子裡面流行着這麼一句話:“不要重複造輪子!”。這裡的輪子其實就是我們的第三方包。當我們想要制造一輛小汽車,我們直接使用現有的零件拼裝起來就行。就不需要再從0開始,造輪子、發動機等等。因為别人已經造好了。

在計算機行業裡面,别人造好了輪子,總要有地方存起來,可以讓其他使用者看到和使用。這就需要一個權威的三方機構去管理這些輪子。

PyPI就是這麼一個角色, PyPI(Python Package Index)是python官方的第三方庫的倉庫,所有人都可以下載下傳第三方庫或上傳自己開發的庫到PyPI。

那怎麼去管理這些三方包呢?市面上有很多方法。但是使用最廣泛的是

pip

pip 是一個現代的,通用的 Python 包管理工具,該工具提供了對Python 包的查找、下載下傳、安裝、解除安裝的功能

7.1 pip 安裝

# 檢視pip版本,可以判斷pip是否安裝
pip -v
           

一般情況下,我們從官網上下載下傳的python安裝以後,會自帶這個工具,我們無需過多操心,但是,如果很不巧,你的Python版本下恰好沒有pip這個工具,怎麼辦呢?

對于linux使用者:

$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py   # 下載下傳安裝腳本
$ sudo python get-pip.py    # 運作安裝腳本
           
sudo apt-get install python-pip
           

window使用者類似,隻需要去官網上下載下傳pip安裝包,再使用python安裝即可

7.2 安裝第三方包

pip install Django  # 安裝最新版本
pip install Django==1.0.4  # 指定版本
pip install Django>=1.0.1  # 指定最小版本
pip install Django<=1.0.1  # 指定最大版本
           

由于 PyPI鏡像源是國外的,有時候下載下傳會非常緩慢,這個時候我們可以使用國内鏡像源

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple Django
           
  • 清華:https://pypi.tuna.tsinghua.edu.cn/simple
  • 阿裡雲:http://mirrors.aliyun.com/pypi/simple/
  • 豆瓣:http://pypi.douban.com/simple/
  • ......還有其他可自行搜尋

7.3 其他操作

pip uninstall Django  # 解除安裝已安裝的庫
pip install --upgrade Djano==2.0.1  # 更新已安裝的包
pip list  # 列出已安裝的庫
pip freeze > requirements.txt  # 将目前的項目依賴導出的文本檔案中
pip install -r requirements.txt  # 根據上面導出的文本檔案裡面的依賴進行安裝