本文介紹python程式設計的子產品和包
一、子產品
1.什麼是子產品?
一個子產品就是一個包含了python定義和聲明的檔案,檔案名就是子產品名字加上.py的字尾。
2.為何要使用子產品?
如果你退出python解釋器然後重新進入,那麼你之前定義的函數或者變量都将丢失,是以我們通常将程式寫到檔案中以便永久儲存下來,需要時就通過python test.py方式去執行,此時test.py被稱為腳本script。
随着程式的發展,功能越來越多,為了友善管理,我們通常将程式分成一個個的檔案,這樣做程式的結構更清晰,友善管理。這時我們不僅僅可以把這些檔案當做腳本去執行,還可以把他們當做子產品來導入到其他的子產品中,實作了功能的重複利用。
使用子產品是大大提高了代碼的可維護性。其次,編寫代碼不必從零開始。當一個子產品編寫完畢,就可以被其他地方引用。我們在編寫程式的時候,也經常引用其他子產品,包括Python内置的子產品和來自第三方的子產品。
使用子產品還可以避免函數名和變量名沖突。相同名字的函數和變量完全可以分别存在不同的子產品中,是以,我們自己在編寫子產品時,不必考慮名字會與其他子產品沖突。但是也要注意,盡量不要與内置函數名字沖突。
3.子產品的分類
- 自定義子產品
- 第三方子產品
- 内置子產品
4.使用子產品
(1)導入子產品方式
import module
from module.xx.xx import xx
from module.xx.xx import xx as rename
from module.xx.xx import *
導入子產品其實就是告訴Python解釋器去解釋那個py檔案
- 導入一個py檔案,解釋器解釋該py檔案
- 導入一個包,解釋器解釋該包下的 init.py 檔案
導入子產品時是根據那個路徑作為基準來進行的呢?
import sys
print sys.path
如果sys.path路徑清單沒有你想要的路徑,可以通過 sys.path.append('路徑') 添加:
import sys
import os
project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(project_path)
(2)import
示例檔案:spam.py,檔案名spam.py,子產品名spam
#spam.py
print('from the spam.py')
money=1000
def read1():
print('spam->read1->money',money)
def read2():
print('spam->read2 calling read')
read1()
def change():
global money
money=0
子產品可以包含可執行的語句和函數的定義,這些語句的目的是初始化子產品,它們隻在子產品名第一次遇到導入import語句時才執行
注意:
import語句是可以在程式中的任意位置使用的,且針對同一個子產品很import多次,為了防止你重複導入,python的優化手段是:第一次導入後就将子產品名加載到記憶體了,後續的import語句僅是對已經加載大記憶體中的子產品對象增加了一次引用,不會重新執行子產品内的語句。
import test #隻在第一次導入時才執行test.py内代碼,此處的顯式效果是隻列印一次'from the test.py',當然其他的頂級代碼也都被執行了,隻不過沒有顯示效果.
import test
import test
import test
運作結果:
from the spam.py
我們可以從sys.module中找到目前已經加載的子產品,sys.module是一個字典,内部包含子產品名與子產品對象的映射,該字典決定了導入子產品時是否需要重新導入。
每個子產品都是一個獨立的名稱空間,定義在這個子產品中的函數,把這個子產品的名稱空間當做全局名稱空間,這樣我們在編寫自己的子產品時,就不用擔心我們定義在自己子產品中全局變量會在被導入時,與使用者的全局變量沖突。
測試一:money與spam.money不沖突
import spam
money=10
print(spam.money)
執行結果:
from the spam.py
1000
測試二:read1與spam.read1不沖突
import spam
def read1():
print('========')
spam.read1()
執行結果:
from the spam.py
spam->read1->money 1000
測試三:執行spam.change()操作的全局變量money仍然是spam中的
import spam
money=1
spam.change()
print(money)
執行結果:
from the spam.py
1
總結:
首次導入子產品spam時會做三件事:
1.為源檔案(spam子產品)建立新的名稱空間,在spam中定義的函數和方法若是使用到了global時通路的就是這個名稱空間。
2.在新建立的命名空間中執行子產品中包含的代碼,見初始導入import spam
3.建立名字spam來引用該命名空間
這個名字和變量名沒什麼差別,都是‘第一類的’,且使用spam.名字的方式可以通路spam.py檔案中定義的名字,spam.名字與test.py中的名字來自兩個完全不同的地方。
提示:導入子產品時到底執行了什麼?
In fact function definitions are also ‘statements’ that are ‘executed’; the execution of a module-level function definition enters the function name in the module’s global symbol table.
事實上函數定義也是“被執行”的語句,子產品級别函數定義的執行将函數名放入子產品全局名稱空間表,用globals()可以檢視
(3)别名
import spam as sm
print(sm.money)
導入子產品時,還可以使用别名,這樣,可以在運作時根據目前環境選擇最合适的子產品。比如Python标準庫一般會提供StringIO和cStringIO兩個庫,這兩個庫的接口和功能是一樣的,但是cStringIO是C寫的,速度更快,是以,你會經常看到這樣的寫法:
try:
import cStringIO as StringIO
except ImportError: # 導入失敗會捕獲到ImportError
import StringIO
這樣就可以優先導入cStringIO。如果有些平台不提供cStringIO,還可以降級使用StringIO。導入cStringIO時,用import ... as ...指定了别名StringIO,是以,後續代碼引用StringIO即可正常工作。
為已經導入的子產品起别名的方式對編寫可擴充的代碼很有用,假設有兩個子產品xmlreader.py和csvreader.py,它們都定義了函數read_data(filename):用來從檔案中讀取一些資料,但采用不同的輸入格式。可以編寫代碼來選擇性地挑選讀取子產品,例如:
if file_format == 'xml':
import xmlreader as reader
elif file_format == 'csv':
import csvreader as reader
data=reader.read_date(filename)
(4)from ... import...
對比import spam,會将源檔案的名稱空間'spam'帶到目前名稱空間中,使用時必須是spam.名字的方式。
而from語句相當于import,也會建立新的名稱空間,但是它将spam中的名字直接導入到目前的名稱空間中,在目前名稱空間中,直接使用名字就可以了。
測試一:導入的函數read1,執行時仍然回到spam.py中尋找全局變量money
from spam import read1
money=1000
read1()
執行結果:
from the spam.py
spam->read1->money 1000
測試二:導入的函數read2,執行時需要調用read1(),仍然回到spam.py中找read1()
from spam import read2
def read1():
print('==========')
read2()
執行結果:
from the spam.py
spam->read2 calling read
spam->read1->money 1000
注意:如果目前有重名read1或者read2,那麼會有覆寫效果
測試三:導入的函數read1,被目前位置定義的read1覆寫掉了
from spam import read1
def read1():
print('==========')
read1()
執行結果:
from the spam.py
==========
需要特别強調的一點是:python中的變量指派不是一種存儲操作,而隻是一種綁定關系
from spam import money,read1
money=100 #将目前位置的名字money綁定到了100
print(money) #列印目前的名字
read1() #讀取spam.py中的名字money,仍然為1000
from the spam.py
100
spam->read1->money 1000
(5)from spam import *
把spam中所有的不是以下劃線(_)開頭的名字都導入到目前位置,大部分情況下我們的python程式不應該使用這種導入方式,因為*你不知道你導入什麼名字,很有可能會覆寫掉你之前已經定義的名字。而且可讀性極其的差,在互動式環境中導入時沒有問題。
from spam import * #将子產品spam中所有的名字都導入到目前名稱空間
print(money)
print(read1)
print(read2)
print(change)
執行結果:
from the spam.py
1000
<function read1 at 0x1012e8158>
<function read2 at 0x1012e81e0>
<function change at 0x1012e8268>
如何控制導入的子產品呢?
可以使用__ all __來控制*(用來釋出新版本)
在spam.py中新增一行
all=['money','read1'] #這樣在另外一個檔案中用from spam import *就這能導入清單中規定的兩個名字
(6)特别注意
考慮到性能的原因,每個子產品隻被導入一次,放入字典sys.module中,如果你改變了子產品的内容,你必須重新開機程式,python不支援重新加載或解除安裝之前導入的子產品,有的同學可能會想到直接從sys.module中删除一個子產品不就可以解除安裝了嗎,注意了,你删了sys.module中的子產品對象仍然可能被其他程式的元件所引用,因而不會被清楚。
特别的對于我們引用了這個子產品中的一個類,用這個類産生了很多對象,因而這些對象都有關于這個子產品的引用。
如果隻是你想互動測試的一個子產品,使用importlib.reload(), import importlib; importlib.reload(modulename),這隻能用于測試環境。
測試:
def func1():
print('func1')
import time,importlib
import aa
time.sleep(20)
# importlib.reload(aa)
aa.func1()
在20秒的等待時間裡,修改aa.py中func1的内容,等待test.py的結果。
打開importlib注釋,重新測試
5.作用域
在一個子產品中,我們可能會定義很多函數和變量,但有的函數和變量我們希望給别人使用,有的函數和變量我們希望僅僅在子產品内部使用。在Python中,是通過下劃線字首來實作的。
正常的函數和變量名是公開的(public),可以被直接引用,比如:abc,x123,PI等;
類似--xxx--這樣的變量是特殊變量,可以被直接引用,但是有特殊用途,比如上面的--author--,--name--就是特殊變量,hello子產品定義的文檔注釋也可以用特殊變量--doc--通路,我們自己的變量一般不要用這種變量名;
類似-xxx和--xxx這樣的函數或變量就是非公開的(private),不應該被直接引用,比如_abc,--abc等;
之是以我們說,private函數和變量“不應該”被直接引用,而不是“不能”被直接引用,是因為Python并沒有一種方法可以完全限制通路private函數或變量,但是,從程式設計習慣上不應該引用private函數或變量。
def _private_1(name):
return 'Hello, %s' % name
def _private_2(name):
return 'Hi, %s' % name
def greeting(name):
if len(name) > 3:
return _private_1(name)
else:
return _private_2(name)
我們在子產品裡公開greeting()函數,而把内部邏輯用private函數隐藏起來了,這樣,調用greeting()函數不用關心内部的private函數細節,這也是一種非常有用的代碼封裝和抽象的方法,即:
外部不需要引用的函數全部定義成private,隻有外部需要引用的函數才定義為public。
6.把子產品當做腳本執行
用來控制.py檔案在不同的應用場景下執行不同的邏輯
def fib(n): # write Fibonacci series up to n
a, b = 0, 1
while b < n:
print(b, end=' ')
a, b = b, a+b
print()
def fib2(n): # return Fibonacci series up to n
result = []
a, b = 0, 1
while b < n:
result.append(b)
a, b = b, a+b
return result
if __name__ == "__main__":
import sys
fib(int(sys.argv[1]))
運作方式:
python fib.py 50
7.深入了解搜尋路徑
在第一次導入某個子產品時(比如spam),會先檢查該子產品是否已經被加載到記憶體中(目前執行檔案的名稱空間對應的記憶體),如果有則直接引用,如果沒有,解釋器則會查找同名的内模組化塊,如果還沒有找到就從sys.path給出的目錄清單中依次尋找spam.py檔案。
子產品的查找順序是:記憶體中已經加載的子產品->内置子產品->sys.path路徑中包含的子產品
搜尋時按照sys.path中從左到右的順序查找,位于前的優先被查找,sys.path中還可能包含.zip歸檔檔案和.egg檔案,python會把.zip歸檔檔案當成一個目錄去處理。
至于.egg檔案是由setuptools建立的包,這是按照第三方python庫和擴充時使用的一種常見格式,.egg檔案實際上隻是添加了額外中繼資料(如版本号,依賴項等)的.zip檔案。
需要強調的一點是:
隻能從.zip檔案中導入.py,.pyc等檔案。使用C編寫的共享庫和擴充塊無法直接從.zip檔案中加載(此時setuptools等打包系統有時能提供一種規避方法),且從.zip中加載檔案不會建立.pyc或者.pyo檔案,是以一定要事先建立他們,來避免加載子產品是性能下降。
8.編譯python檔案
為了提高子產品的加載速度,Python緩存編譯的版本,每個子產品在__pycache__目錄的以module.version.pyc的形式命名,通常包含了python的版本号,如在CPython版本3.3,關于spam.py的編譯版本将被緩存成__pycache__/spam.cpython-33.pyc,這種命名約定允許不同的版本,不同版本的Python編寫子產品共存。
Python檢查源檔案的修改時間與編譯的版本進行對比,如果過期就需要重新編譯。這是完全自動的過程。并且編譯的子產品是平台獨立的,是以相同的庫可以在不同的架構的系統之間共享,即pyc使一種跨平台的位元組碼,類似于JAVA或.NET,是由python虛拟機來執行的,但是pyc的内容跟python的版本相關,不同的版本編譯後的pyc檔案不同,2.5編譯的pyc檔案不能到3.5上執行,并且pyc檔案是可以反編譯的,因而它的出現僅僅是用來提升子產品的加載速度的。
提示:
- 子產品名區分大小寫,foo.py與FOO.py代表的是兩個子產品
- 你可以使用-O或者-OO轉換python指令來減少編譯子產品的大小
- 在速度上從.pyc檔案中讀指令來執行不會比從.py檔案中讀指令執行更快,隻有在子產品被加載時,.pyc檔案才是更快的
- 隻有使用import語句是才将檔案自動編譯為.pyc檔案,在指令行或标準輸入中指定運作腳本則不會生成這類檔案,因而我們可以使用compieall子產品為一個目錄中的所有子產品建立.pyc檔案
二、包
- 無論是import形式還是from...import形式,凡是在導入語句中(而不是在使用時)遇到帶點的,都要第一時間提高警覺:這是關于包才有的導入文法
- 包是目錄級的(檔案夾級),檔案夾是用來組成py檔案(包的本質就是一個包含__init__.py檔案的目錄)
- import導入檔案時,産生名稱空間中的名字來源于檔案,import 包,産生的名稱空間的名字同樣來源于檔案,即包下的__init__.py,導入包本質就是在導入該檔案
包A和包B下有同名子產品也不會沖突,如A.a與B.a來自倆個命名空間
glance/ #Top-level package
├── __init__.py #Initialize the glance package
├── api #Subpackage for api
│ ├── __init__.py
│ ├── policy.py
│ └── versions.py
├── cmd #Subpackage for cmd
│ ├── __init__.py
│ └── manage.py
└── db #Subpackage for db
├── __init__.py
└── models.py
#檔案内容
#policy.py
def get():
print('from policy.py')
#versions.py
def create_resource(conf):
print('from version.py: ',conf)
#manage.py
def main():
print('from manage.py')
#models.py
def register_models(engine):
print('from models.py: ',engine)
1.注意事項
- 關于包相關的導入語句也分為import和from ... import ...兩種,但是無論哪種,無論在什麼位置,在導入時都必須遵循一個原則:凡是在導入時帶點的,點的左邊都必須是一個包,否則非法。可以帶有一連串的點,如item.subitem.subsubitem,但都必須遵循這個原則。
- 對于導入後,在使用時就沒有這種限制了,點的左邊可以是包,子產品,函數,類(它們都可以用點的方式調用自己的屬性)。
- 對比import item 和from item import name的應用場景:如果我們想直接使用name那必須使用後者。
2.import
我們在與包glance同級别的檔案中測試
import glance.db.models
glance.db.models.register_models('mysql')
3.from ... import ...
需要注意的是from後import導入的子產品,必須是明确的一個不能帶點,否則會有文法錯誤,如:from a import b.c是錯誤文法
from glance.db import models
models.register_models('mysql')
from glance.db.models import register_models
register_models('mysql')
4.--init--.py檔案
不管是哪種方式,隻要是第一次導入包或者是包的任何其他部分,都會依次執行包下的--init--.py檔案(我們可以在每個包的檔案内都列印一行内容來驗證一下),這個檔案可以為空,但是也可以存放一些初始化包的代碼。
5.from glance.api import *
在講子產品時,我們已經讨論過了從一個子產品内導入所有,此處我們研究從一個包導入所有。
此處是想從包api中導入所有,實際上該語句隻會導入包api下--init--.py檔案中定義的名字,我們可以在這個檔案中定義--all--:
x=10
def func():
print('from api.__init.py')
__all__=['x','func','policy']
此時我們在于glance同級的檔案中執行from glance.api import *就導入--all--中的内容(versions仍然不能導入)。
5.絕對導入和相對導入
我們的最頂級包glance是寫給别人用的,然後在glance包内部也會有彼此之間互相導入的需求,這時候就有絕對導入和相對導入兩種方式:
絕對導入:以glance作為起始
相對導入:用.或者..的方式最為起始(隻能在一個包中使用,不能用于不同目錄内)
例如:我們在glance/api/version.py中想要導入glance/cmd/manage.py
在glance/api/version.py
#絕對導入
from glance.cmd import manage
manage.main()
#相對導入
from ..cmd import manage
manage.main()
測試結果:注意一定要在于glance同級的檔案中測試
from glance.api import versions
注意:在使用pycharm時,有的情況會為你多做一些事情,這是軟體相關的東西,會影響你對子產品導入的了解,因而在測試時,一定要回到指令行去執行,模拟我們生産環境。
6.單獨導入包
單獨導入包名稱時不會導入包中所有包含的所有子子產品
如:在與glance同級的test.py中
import glance
glance.cmd.manage.main()
執行結果:
AttributeError: module 'glance' has no attribute 'cmd'
解決方法:
#glance/__init__.py
from . import cmd
#glance/cmd/__init__.py
from . import manage
執行:
在于glance同級的test.py中
import glance
glance.cmd.manage.main()
千萬别問:--all--不能解決嗎,--all--是用于控制from...import *