尚未翻译
本页面尚未翻译成中文,因此显示英文原文。 帮助翻译
site Module Complexity¶
The site module runs at interpreter startup, unless Python was started with -S. It adds the
site-packages directories to sys.path and processes the .pth files in them, and from 3.15 the
.start files too. Its work is filesystem work: it lists each site directory it processes, and
checks each .pth path line that names a path not already known for existence.
P is the entries on sys.path when a call starts, e is the entries in a site directory (every
name, not only the .pth files), f is the .pth and .start files in it, L is the lines in
those files, A is the paths the call adds to sys.path, N is the new paths it records - the
site directory and each distinct .pth path line not already known, whether or not it exists -
c is the characters in the largest .pth file, r is the entries in site.PREFIXES, and M
is the modules in sys.modules. For site.main(), e, f, L, A and N are totals over
every site directory it processes. Lines and paths are treated as short, so one stat call or
one path normalisation is O(1). The bounds exclude the code an import line in a .pth file or
an entry point in a .start file runs, and the sitecustomize and usercustomize imports;
those cost whatever their code does.
Complexity Reference¶
Site directories and .pth files¶
| Operation | Time | Space | Notes |
|---|---|---|---|
site.addsitedir(sitedir, known_paths=None) |
O(P + e + f log f + L), plus O(N·(P + A)) from 3.15 | O(P + e + A + c), plus O(L) from 3.15 | Checks every sys.path entry for existence first unless known_paths is given, lists the whole directory, and processes the .pth files in name order. From 3.15 it records the directory's paths and then applies them, scanning sys.path, which grows as paths are appended, once for each new path before appending it; that is the N·(P + A) term. The recorded lines are held until then, the O(L) |
site.main() |
O(P + M + r + e + f log f + L), plus O(N·(P + A)) from 3.15 | O(P + M + r + e + A + c), plus O(L) from 3.15 | Every existing site directory, sharing one set of known paths. Up to 3.14 one addsitedir() each, and in a virtual environment its own site-packages is processed twice. From 3.15 one StartupState batch: every directory's paths are added before any import line or entry point runs, and each directory is processed once. M is paid only when a sys.path entry is relative, duplicated or not normalised |
site.makepath(*paths) |
O(1) | O(1) | Joins the parts and returns the absolute path with its case-normalised form, the pair the known-path checks compare |
StartupState¶
| Operation | Time | Space | Notes |
|---|---|---|---|
site.StartupState(known_paths=None) |
O(P), or O(1) with known_paths |
O(P) | Python 3.15+. Without known_paths, checks every sys.path entry for existence to build the set; a set given is used, and updated, in place |
StartupState.addsitedir(sitedir) |
O(e + f log f + L) | O(e + L + c) | Python 3.15+. Lists the directory and reads its .pth and .start files in name order, recording paths, import lines and entry points; nothing reaches sys.path or runs yet. A directory already in this batch is skipped |
StartupState.addusersitepackages(), StartupState.addsitepackages(prefixes=None) |
O(e + f log f + L), and O(r) more for addsitepackages |
O(e + L + c), and O(r) more for addsitepackages |
Python 3.15+. addsitedir() on the user site directory when it is enabled and exists, or on each existing directory getsitepackages() builds |
StartupState.process() |
O(N·(P + A) + L) | O(1) | Python 3.15+. Appends each recorded path not already on sys.path, reporting a missing one on stderr; then runs the import lines of every .pth file without a .start file of the same name; then calls each .start entry point. The lines and entry points cost whatever their code does |
Locating site directories¶
| Operation | Time | Space | Notes |
|---|---|---|---|
site.getsitepackages() |
O(r) | O(r) | Builds the paths from site.PREFIXES without checking that they exist |
site.getuserbase() |
O(1) | O(1) | Computed on the first call and kept in site.USER_BASE |
site.getusersitepackages() |
O(1) | O(1) | Computed on the first call and kept in site.USER_SITE; main() makes that first call at startup |
Constants¶
| Operation | Time | Space | Notes |
|---|---|---|---|
site.PREFIXES |
O(1) | O(1) | The list getsitepackages() reads |
site.ENABLE_USER_SITE |
O(1) | O(1) | Set by main(): True when the user site directory is enabled, None when it is disabled for security, False otherwise |
site.USER_BASE, site.USER_SITE |
O(1) | O(1) | None until getuserbase() and getusersitepackages() fill them |
Command line¶
| Operation | Time | Space | Notes |
|---|---|---|---|
python -m site |
O(P) | O(1) | The printing, after startup: sys.path an entry at a time, then USER_BASE and USER_SITE and whether each exists. The command also pays site.main() twice, once at startup and once because the module runs as a script |
python -m site --user-base, python -m site --user-site |
O(1) | O(1) | The printing, after startup; the exit status is 0, 1 or 2 for ENABLE_USER_SITE true, false or None. The command also pays site.main() twice |
Processing .pth Files¶
A .pth path line is joined to the site directory and added only if that path exists, so each
new one costs a stat. A path already known is skipped by a set lookup, not a scan of
sys.path; from 3.15 each new path is also checked against sys.path once before it is
appended, and a missing one is reported on stderr. A line starting with import and a space or
tab is executed when it is reached up to 3.14, so it sees the paths the lines above it added;
from 3.15 it runs after every path in the batch is added, and not at all when a .start file
of the same name sits beside the .pth file.
import os
import site
import sys
import tempfile
with tempfile.TemporaryDirectory() as sitedir:
os.mkdir(os.path.join(sitedir, 'plugins'))
with open(os.path.join(sitedir, 'plugins', 'marker.py'), 'w') as module:
module.write('')
lines = [
'# comment lines and blank lines are skipped',
'',
'plugins', # relative to sitedir, and it exists
'missing', # does not exist, so it is not added
'plugins', # already added, so it is skipped
'import marker', # runs when reached, and finds plugins/marker.py
]
with open(os.path.join(sitedir, 'extra.pth'), 'w') as pth:
pth.write('\n'.join(lines) + '\n')
original = list(sys.path)
site.addsitedir(sitedir) # O(P + e + f log f + L)
added = sys.path[len(original):]
sys.path[:] = original
real = os.path.realpath(sitedir)
assert [os.path.realpath(path) for path in added] == [real, os.path.join(real, 'plugins')]
assert 'marker' in sys.modules # the import line ran
The Directory Listing¶
addsitedir() lists every entry in the directory before it filters for .pth files, so a
site-packages directory full of installed packages costs its size even when it holds one .pth
file. The .pth files are then processed in name order, and the directories they add go to the
end of sys.path in that order.
import os
import site
import sys
import tempfile
with tempfile.TemporaryDirectory() as sitedir:
for name in ('b', 'a'):
os.mkdir(os.path.join(sitedir, name))
with open(os.path.join(sitedir, name + '.pth'), 'w') as pth:
pth.write(name + '\n')
for index in range(100):
open(os.path.join(sitedir, f'package{index}.txt'), 'w').close() # listed, then skipped
original = list(sys.path)
site.addsitedir(sitedir) # O(e) to list all 104 entries, not just the two .pth files
added = [os.path.basename(path) for path in sys.path[len(original) + 1:]]
sys.path[:] = original
assert added == ['a', 'b'] # a.pth sorts before b.pth
Startup Cost¶
site.main() runs before your program does, at every start. It lists each site directory,
reads the .pth files in them, runs their import lines, and imports sitecustomize, and
usercustomize when the user site directory is enabled. Up to 3.14, in a virtual environment
the environment's own site-packages goes through that twice, import lines included; from 3.15
main() batches every directory in one StartupState, which processes each directory once and
runs .start entry points after the import lines. Each directory
it adds is also one more sys.path entry that an import miss searches; see
__import__(). Starting Python with -S skips all of it, and
site.main() does the same work later if a program needs it.
import subprocess
import sys
script = "import sys; print('site' in sys.modules, sys.flags.no_site)"
result = subprocess.run([sys.executable, '-S', '-c', script],
capture_output=True, text=True, check=True)
assert result.stdout.split() == ['False', '1'] # no site directory was scanned
Locating Site Directories¶
The lookup functions build paths from prefixes and environment variables, and none of them lists
a directory. getsitepackages() builds a new list on every call; the two user lookups compute
their path once and keep it.
import site
packages = site.getsitepackages() # O(r) - paths built from site.PREFIXES
assert all(isinstance(path, str) for path in packages)
assert site.getsitepackages() is not packages # a new list on every call
user_site = site.getusersitepackages() # O(1) - cached after the first call
assert user_site is site.USER_SITE
assert site.getuserbase() is site.USER_BASE # O(1)
Performance Best Practices¶
✅ Do:
- Keep
.pthimportlines few and cheap: they run at every interpreter start - Start short-lived scripts that need no installed packages with
-S, which skips the site directory scans - Use
sys.path.append()for a directory that holds no.pthfiles;addsitedir()also lists it and checks everysys.pathentry first
❌ Avoid:
- Adding many directories through
.pthfiles: each one lengthens the path an import miss searches - Heavy work in
sitecustomizeorusercustomize, which are imported at every start
Version Notes¶
- Python 3.15+:
.startfiles name entry points to call at startup, and animportline in a.pthfile is skipped when a.startfile of the same name exists;importlines run after every path of the batch is added rather than when reached, and a missing path line is reported on stderr.StartupStateandmakepath()are public,main()processes a virtual environment's site-packages once, and applying the recorded paths scanssys.pathonce per new path, the N·(P + A) term - Python 3.12.4+:
.pthfiles are read whole, which is thecterm, and decoded as UTF-8, a byte order mark accepted, falling back to the locale encoding; earlier releases read them a line at a time in the locale encoding