iT邦幫忙

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

30天python雜談系列 第 25

import雜談之四———來trace一下site模組

python import雜談之四

認真囉唆一下:
這篇文章獻給那些沒有看過sourece code並畏懼它的人,很多人覺得soure code很可怕通常是因為覺得深奧,或是以為trace code就是要把細節都讀懂,但實際上trace code是推理與猜測並行的。

這是什麼意思呢?意思是說我們一方面要做一些猜測與假設,假設這段source code只有某些部份是重要的,而其他認為不重要的就不要看他,或是假設某個function我從名稱就大概知道他在做什麼,不用特別去挖出他的內容來細讀,必須大膽的猜,然後才去仔細推敲自己覺得重要的部份。

以下是我昨天到今天一邊trace code一邊寫出來的文章,若是全都細看,我肯定寫不完這篇XD,所以就拿這個當範例來跟大家分享一下我trace code的思路,但因為是trace code,所以內容還算有些硬,建議願意讀下去的人可以邊讀邊寫筆記,至於code你們可以自己找自己的site.py來互相比對,應該原理都差不多。至於如何找你們site.py可以看看昨天的文章。

同時我們也必須注意,有的時候trace code不一定能得到完全正確的知識,但通常懂大概原理還是有很大幫助的。

昨天把site模組的部份原始碼PO了出來,今天要來好好trace code一下,先來做個前情提要,我們已經知道sys.path會先添加一個空字串(意指執行檔當下目錄)以及環境變數PYTHONPATH所指定的路徑,之後才是藉由site.py來添加sys.path,那我們現在要稍微深入site.py的添加路徑機制,以及探討昨天所說的pth檔是在哪裡被運作的。

再貼一下程式碼哈哈:

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

先來看一下addusersitepackages,在一開始會先呼叫getusersitepackages尋找所謂的usersitepackage,若忽略getusersitepackages中較不重要的資訊,那就剩下以下部份:

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

因為我們的系統是ubuntu(sys.platform=='ubuntu'),前面的if判斷式也不用理他了,所以最後回傳的就是get_path('purelib', '%s_user' % os.name):

In python3 shell:
>>> from sysconfig import get_path
>>> import os
>>> get_path('purelib', '%s_user' % os.name)
'/home/shnovaj30101/.local/lib/python3.4/site-packages'
>>> os.name
'posix'
>>> 

阿哈,經過測試之後出現一堆我不懂的名詞,我開始有點後悔trace code,然後我發現'/home/shnovaj30101/.local/lib/python3.4/site-packages'並不在我的路徑中存在呢,那...我們就先記下來(疑點一),然後丟在一邊XD,繼續看下去,因為getusersitepackages回傳值並不是我系統中真實的路徑,所以我的addusersitepackages就只剩下:

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

這裡就很好看了,他就是在檢驗"{USER_BASE}/lib/python3.4/dist-packages"和"{USER_BASE}/local/lib/python3.4/dist-packages"這兩個路徑存不存在而已,若存在就加進known_paths,秉持著昨天說的懶惰法則,雖然不一定全對,但我們估且就認定說加進了known_paths就相當於之後會被加進sys.path吧(疑點二),那USER_BASE中是存取什麼值呢?我們也先記著丟在一邊,先把他當成"/usr"吧(疑點三),若這個假設正確,那我們就找到'/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages'被加進sys.path的原因了。

接下來來看看addsitepackages,他會先從getsitepackages取得一個路徑的list,在這個list中的路徑若真實存在,則會被加進known_paths裏面,我們也假設相當於把這些路徑在以後會加進sys.path裏面,那麼getsitepackages是在幹嘛呢,先列出這段code的重要部份?

if prefixes is None:
    prefixes = PREFIXES

for prefix in prefixes:
    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"))

return sitepackages

其他不符合我系統的條件的code已經刪去,然後seen這個變數不知道也沒啥用,刪掉,然後我們第一個要探討的是全域變數PREFIXES在幹嘛,查了一下site.py其他地方,發現PREFIXES=[sys.prefix, sys.exec_prefix]:

In python3 shell:
>>> import sys
>>> sys.prefix
'/usr'
>>> sys.exec_prefix
'/usr'
>>> # PREFIXES裏面裝的只有'/usr'

第二個問題是,"if 'VIRTUAL_ENV' in os.environ or sys.base_prefix != sys.prefix:",這是麼意思呢,容我很大膽的猜測,因為有'VIRTUAL_ENV'的字眼,所以我猜想應該是在虛擬環境下呼叫python的時候這個條件才會滿足,因為我沒有建虛擬環境,所以這個條件不會滿足,而且確實我昨天列出我sys.path的內容,沒有"site-packages"這個路徑,從這點能證明這個條件在我呼叫python的時候並不會滿足,但還是把這個虛擬環境假設記下來(疑點四),若不知道虛擬環境是什麼,我以後會講。

剩下的code就簡單了,就是連續添加4個路徑:

"/usr/local/lib/python3.4/dist-packages"
"/usr/lib/python3/dist-packages"
"/usr/lib/python3.4/dist-packages"
"/usr/lib/dist-python"

在這裡突然發現了一個問題,我們的疑點三的假設似乎出錯了,因為'/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages'是在上面被加進去的,那疑點三的USER_BASE是什麼意思呢?來trace一下code,搜尋一下USER_BASE出現的位置,發現他在getuserbase中被設定,然後getuserbase這個函式在addusersitepackages有被呼叫到(看貼最上面的code就能知道這件事):

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

def getuserbase():
    """Returns the `user base` directory path.

    The `user base` directory can be used to store data. If the global
    variable ``USER_BASE`` is not initialized yet, this function will also set
    it.
    """
    global USER_BASE
    if USER_BASE is not None:
        return USER_BASE
    from sysconfig import get_config_var
    USER_BASE = get_config_var('userbase')
    return USER_BASE

來看看上面的註解吧,trace code時多看註解有益身心健康(認真),"user base directory can be used to store data.",也就是說USER_BASE這個路徑可以被用來儲存資料,好八我暫時不知道在幹嘛...但可以先看看USER_BASE到底是什麼:

In python3 shell:
>>> from sysconfig import get_config_var
>>> get_config_var('userbase')
'/home/shnovaj30101/.local'
>>>

恩恩,以前完全沒注意這個資料夾,來估狗估狗,看到了一篇不錯的文:https://askubuntu.com/questions/14535/whats-the-local-folder-for-in-my-home-directory ,好八,從這篇文我只能理解到,這是給我這個作業系統的一些程式存放一些保存程式狀態(state)的資料,以免都存在$HOME資料夾讓資料夾看起來很亂,可能我的python有時候要存一些資料來保持狀態,然後下次開啟的時候就會去載入這些資料,就會得到上次關閉時的狀態,目前做這樣的猜想(疑點五),好,懶的追根究底了。

應該這樣就大概講完了...阿阿那請問pth勒,pth指定路徑是在那邊被執行的怎麼沒看到,我剛剛要更新文章的時候才發現這件事哈哈,我來看看搜尋一下pth是在哪裡出現的:

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

def addsitedir(sitedir, known_paths=None):
    """Add 'sitedir' argument to sys.path if missing and handle .pth files in
    'sitedir'"""
    if known_paths is None:
        known_paths = _init_pathinfo()
        reset = 1
    else:
        reset = 0
    sitedir, sitedircase = makepath(sitedir)
    if not sitedircase in known_paths:
        sys.path.append(sitedir)        # Add path component
        known_paths.add(sitedircase)
    try:
        names = os.listdir(sitedir)
    except OSError:
        return
    names = [name for name in names if name.endswith(".pth")]
    for name in sorted(names):
        addpackage(sitedir, name, known_paths)
    if reset:
        known_paths = None
    return known_paths

找到了,在addsitedir出現pth檔的操作,然後在addusersitepackages和addsitepackages都有呼叫addsitedir,我倒是漏看了,在addsitedir中,前面會先確實的把sitedir加進sys.path,然後把sitedir中的file_list加進names並濾掉非pth的file,而這些剩下pth檔案就會在addpackage被處理,把裏面指定的路徑明都加入sys.path,addpackage的內容就不細究了XD,反正應該就是做pth檔在做的事。

好拉,總結一下,site.py對於sys.path的添加的順序如下:

addusersitepackages(known_paths)會試著添加
"/home/shnovaj30101/.local/lib/python3.4/site-packages"
"/home/shnovaj30101/.local/lib/python3.4/dist-packages"
"/home/shnovaj30101/.local/local/lib/python3.4/dist-packages"
等等路徑,並尋找裏面的pth檔。

addsitepackages(known_paths)會試著添加
"/usr/local/lib/python3.4/dist-packages"
"/usr/lib/python3/dist-packages"
"/usr/lib/python3.4/dist-packages"
"/usr/lib/dist-python"
等等路徑,並尋找裏面的pth檔。

所以我們發現了usersitepackages去尋找"/home/shnovaj30101/.local/"裏面的package,至於什麼時候packagec會加進這個路徑我也不知道,現在再來比對一下我的sys.path的內容:

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']
>>> 

因為site.py中加入的內容只有'/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages'真實存在在我的目錄中,所以只有這兩個路徑顯示在sys.path中,但是前面的'/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload'是怎麼出現的...怎麼辦我也不知道XD,沒辦法編寫文章邊trace code本來就沒辦法得到完全正確詳盡的資訊,但至少大致原理都稍微懂了,這才是trace code的意義阿,我在想那個未知的3個路徑應該是在site.py的其他地方指定的,但我累了XD,有興趣的同學自己trace看看吧~

我有點擔心沒人會看這篇文章...


上一篇
import雜談之三———sys.path的洪荒之時
下一篇
decorator與closure雜談之一———初探decorator與函數對象
系列文
30天python雜談30

尚未有邦友留言

立即登入留言