iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 24
0
Data Technology

30天python雜談系列 第 24

import雜談之三———sys.path的洪荒之時

python import雜談之三

前兩天用模組建構的角度來探討import的機制,現在我們就一個使用者的角度來思考究竟python的import應該俱備什麼功能。

議題一:今天我想要去import別人寫好的一個module,但他不存在當下的工作目錄底下,那我應該有什麼方法可以得到這個module呢?

python其實有很多個方法可以達成這件事,第一個最簡單的方法就是用把欲加入的module的路徑手動加到sys.path這個list裏面:

In python3 shell:
>>> import sys
>>> sys.path
['', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']

從印出sys.path就可以知道平時python在load module的時候是從哪裡尋找module的位置,一般的操作指令都可以很簡單的手動加入新的路徑到sys.path裏面:

sys.path.insert(0, 'some path')
sys.path.append('some path')
sys.path.extend(['some path','some path'....])

但這方法的缺點在於我們會把路徑寫死在程式碼裏面,當我們把這個被引入的模組更換一下路徑,那所有寫死路徑的程式碼都要被叫出來改掉,萬一這個模組有剛好是很通用的模組,被一堆不同部份的code所import,那真的是改路徑改到人仰馬翻。

既然在程式碼中加入module可能會遇到這種麻煩的問題,那只能訴諸程式碼外的解決方式了。

其中一個是利用設定PYTHONPATH的方式來新增尋找module的路徑:

In bash:
$ env PYTHONPATH='/home/shnovaj30101' python3
Python 3.4.3 (default, Nov 17 2016, 01:08:31) 
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/home/shnovaj30101', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']
>>> 

當我們在呼叫python的程式之前,先將'/home/shnovaj30101'這個路徑加入環境變數PYTHONPATH中,然後就會發現sys.path已經把'/home/shnovaj30101'這個路徑放在裏面了!

還有另外一個方法就是把路徑放在一個.pth檔裏面,這個.pth檔的作用呢,就是在python初始化時,為sys.path添加額外的變數,那這個.pth檔要放在哪裡呢?

In python3 shell:
>>> import sys
>>> sys.path
['', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']

通常都會把pth檔加在site-python/或是site-packages/裏面,但是我的路徑好像沒有這些資料夾,看了一下source code裏面發現dist-packages裏面也可,所以現在就隨便建一個測試的pth檔吧:

In test.pth:
/home/shnovaj30101

然後把這個test.pth檔加進'/usr/local/lib/python3.4/dist-packages'裏面。

>>> import sys
>>> sys.path
['', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/home/shnovaj30101', '/usr/lib/python3/dist-packages']
>>> 

果不其然,'/home/shnovaj30101'因為我剛剛寫的pth檔,所以在初始化的時候被加進去了。

議題二(充字數again):來講一下議題一的原理吧,到底python的初始化過程式如何設定sys.path呢?

在python初始化的過程中,第一個被加進sys.path的元素是一個空字串'',這個空字串指的是呼叫python當下所在的工作目錄,所以放置在工作目錄的模組一定可以被import進程式當中。

接著就是我們議題一的第二個方法,python會讀取環境變數PYTHONPATH所指定的路徑名稱,從議題一的範例也可以證明,PYTHONPATH內含的路徑名稱,是第二個被加進sys.path的路徑,因為他們就正好就位於空字串的後面。

第三個被引入的路徑,會由python的site模組來分配,同時我們的pth檔之所以能夠新增路徑到sys.path,也是因為site模組的關係,那如果我們要更深入研究site模組是如何分配路徑,我們必須先找到他的原始碼site.py,然後看看他是怎麼實作的:

In python3 shell:
>>> import site
>>> site # 尋找site.py的位置
<module 'site' from '/usr/lib/python3.4/site.py'>
>>> 

經過網路查閱得知當site模組被import進來就會自己添加路徑到sys.path,那我們要先從'/usr/lib/python3.4/site.py'中尋找當site.py被import時會做出什麼行為:

In /usr/lib/python3.4/site.py (line 559):

def main():
    """Add standard site-specific directories to the module search path.

    This function is called automatically when this module is imported,
    unless the python interpreter was started with the -S flag.
    """
    global ENABLE_USER_SITE

    abs_paths()
    known_paths = removeduppaths()
    known_paths = venv(known_paths)
    if ENABLE_USER_SITE is None:
        ENABLE_USER_SITE = check_enableusersite()
    known_paths = addusersitepackages(known_paths)
    known_paths = addsitepackages(known_paths)
    setquit()
    setcopyright()
    sethelper()
    enablerlcompleter()
    aliasmbcs()
    execsitecustomize()
    if ENABLE_USER_SITE:
        execusercustomize()

# Prevent edition of sys.path when python was started with -S and
# site is imported later.
if not sys.flags.no_site:
    main()

在site.py裏面,幾乎所有程式碼都是函式定義,唯一會呼叫的就是上面代碼中最下面的兩行程式,所以我們知道了當site.py被import進來時最先被呼叫的入口函式就是main()。

礙於篇幅不夠所以我只挑重要的講,基本上每個function最前面都有一個註釋,看了註釋覺得和sys.path最相關的應該就是addusersitepackages和addsitepackages了:

In /usr/lib/python3.4/site.py (line 256):

def getusersitepackages():
    """Returns the user-specific site-packages directory path.

    If the global variable ``USER_SITE`` is not initialized yet, this
    function will also set it.
    """
    global USER_SITE
    user_base = getuserbase() # this will also set USER_BASE

    if USER_SITE is not None:
        return USER_SITE

    from sysconfig import get_path

    if sys.platform == 'darwin':
        from sysconfig import get_config_var
        if get_config_var('PYTHONFRAMEWORK'):
            USER_SITE = get_path('purelib', 'osx_framework_user')
            return USER_SITE

    USER_SITE = get_path('purelib', '%s_user' % os.name)
    return USER_SITE

def addusersitepackages(known_paths):
    """Add a per user site-package to sys.path

    Each user has its own python directory with site-packages in the
    home directory.
    """
    # get the per user site-package path
    # this call will also make sure USER_BASE and USER_SITE are set
    user_site = getusersitepackages()

    if ENABLE_USER_SITE and os.path.isdir(user_site):
        addsitedir(user_site, known_paths)
    if ENABLE_USER_SITE:
        for dist_libdir in ("lib", "local/lib"):
            user_site = os.path.join(USER_BASE, dist_libdir,
                                     "python" + sys.version[:3],
                                     "dist-packages")
            if os.path.isdir(user_site):
                addsitedir(user_site, known_paths)
    return known_paths
In /usr/lib/python3.4/site.py (line 300):

def getsitepackages(prefixes=None):
    """Returns a list containing all global site-packages directories
    (and possibly site-python).

    For each directory present in ``prefixes`` (or the global ``PREFIXES``),
    this function will find its `site-packages` subdirectory depending on the
    system environment, and will return a list of full paths.
    """
    sitepackages = []
    seen = set()

    if prefixes is None:
        prefixes = PREFIXES

    for prefix in prefixes:
        if not prefix or prefix in seen:
            continue
        seen.add(prefix)

        if os.sep == '/':
            if 'VIRTUAL_ENV' in os.environ or sys.base_prefix != sys.prefix:
                sitepackages.append(os.path.join(prefix, "lib",
                                                 "python" + sys.version[:3],
                                                 "site-packages"))
            sitepackages.append(os.path.join(prefix, "local/lib",
                                             "python" + sys.version[:3],
                                             "dist-packages"))
            sitepackages.append(os.path.join(prefix, "lib",
                                             "python3",
                                             "dist-packages"))
            # this one is deprecated for Debian
            sitepackages.append(os.path.join(prefix, "lib",
                                             "python" + sys.version[:3],
                                             "dist-packages"))
            sitepackages.append(os.path.join(prefix, "lib", "dist-python"))
        else:
            sitepackages.append(prefix)
            sitepackages.append(os.path.join(prefix, "lib", "site-packages"))
        if sys.platform == "darwin":
            # for framework builds *only* we add the standard Apple
            # locations.
            from sysconfig import get_config_var
            framework = get_config_var("PYTHONFRAMEWORK")
            if framework:
                sitepackages.append(
                        os.path.join("/Library", framework,
                            sys.version[:3], "site-packages"))
    return sitepackages

def addsitepackages(known_paths, prefixes=None):
    """Add site-packages (and possibly site-python) to sys.path"""
    for sitedir in getsitepackages(prefixes):
        if os.path.isdir(sitedir):
            if "site-python" in sitedir:
                import warnings
                warnings.warn('"site-python" directories will not be '
                              'supported in 3.5 anymore',
                              DeprecationWarning)
            addsitedir(sitedir, known_paths)

    return known_paths

看起來落落長阿,沒關係且聽我明天細細說明,不想在一天中塞太多內容,這樣只會讓我自己累死XD。

先來說明一下看source code的心法,其實沒什麼,就是一個懶字而已,切記當一個source code牽涉到的東西比較複雜時,很多東西能忽略就忽略,能假設就假設,不要一次把他全看完,注意對自己重要的東西就好。

與其辛苦的把他從頭讀完,一次就讀到懂,不如只看重要的東西,然後看很多次,發現還是有不懂的地方,就在看細一點,這樣比較不會喪失焦點,也不會太耗腦力,更能省時間。


上一篇
import雜談之二———export機制以及namespace package
下一篇
import雜談之四———來trace一下site模組
系列文
30天python雜談30

尚未有邦友留言

立即登入留言