認真囉唆一下:
這篇文章獻給那些沒有看過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看看吧~
我有點擔心沒人會看這篇文章...