天天看點

Python程式設計-子產品和包

本文介紹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 *