Add R05, lazy imports - #183
Merged
Merged
Conversation
PEP 810 landed in 3.15 and added one soft keyword to the import statement. `lazy import json` binds the name now and leaves the finding, reading and running of the module until the first time something reads that name back. The lesson weighs what an unused import costs, shows the five line LazyLoader dance you had to write before 3.15, then disassembles four import statements to show that a lazy import compiles to the same IMPORT_NAME opcode with the mode carried in two spare bits of the argument. It inspects the placeholder without resolving it, resolves it with a single bare name read, watches a copy of one escape into another variable, compiles six spellings of the keyword to see which the symbol table refuses, turns the behaviour on without the keyword with both a filter and a `__lazy_modules__` list, and reads the two part traceback a module raises during resolution. Three recordings back the measured parts. Deferring is worth about three quarters of a run on a file that imports twelve modules and uses one. The other two show that waking a placeholder takes the interpreter wide import lock rather than a lock per module name, so on a free threaded build four threads waking four different deferred imports keep 0.98 cores busy where four ordinary imports keep 3.60. Every lazy cell is guarded, because CI runs notebooks on 3.14 and Pyodide is 3.14.2. Also fixes an intermittent R03 failure the probe re-record turned up. The Emscripten filesystem keeps whole second timestamps, so a directory mtime does not always move when a file is written into it and FileFinder keeps serving a stale listing. The cell now falls back to dropping the finder out of sys.path_importer_cache. With six diagrams, three glossary terms, both README rows, and the refreshed citation lock, claims list and probe recording.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
R05 is the lazy imports lesson, and it is the fifth of the runtime lessons on M8. PEP 810 landed in 3.15 and added one soft keyword to the import statement, so
lazy import jsonbinds the name straight away and leaves the finding, reading and running of the module until the first time something reads that name back.The lesson starts by weighing what an unused import costs, then shows the five line
LazyLoaderdance you had to write before 3.15 and why it wakes up on any attribute read at all, including__name__. From there it goes down into the implementation. Four import statements are disassembled to show that a lazy import compiles to the sameIMPORT_NAMEopcode as a plain one, with the name index shifted up by two bits and the two bits underneath carrying the mode, which is whydisprintsjson,json + lazyorjson + eager.The middle of the lesson works in a temporary directory on
sys.path, so every module it looks at is one we wrote in the cell above and nothing else in the process has already imported it. That is what lets the notebook show the placeholder honestly: it is a five field object rather than a module, it is not insys.modules, its name is insys.lazy_modulesuntil it resolves, anddiron it offers exactly one public name. Only two opcodes know about it, so a dict lookup, a membership test and a repr all leave it alone, and a placeholder copied into another variable resolves when that variable is read rather than when the copy was made.Six spellings of the keyword are compiled to see which are refused and with what message. Four of them come from the symbol table rather than the code generator, which is the reason a lazy import inside a
tryblock gets its own error rather than a generic one. The lesson then turns the behaviour on without the keyword, once withsys.set_lazy_imports_filterand once with a__lazy_modules__list, and shows what the filter is actually handed. After that it makes a module raise during resolution and reads the two part traceback that comes back, where theImportErrorcause is rebuilt from the code object and instruction offset the placeholder was carrying.The last two sections are the measured ones.
r05-what-deferring-an-import-is-worthputs the startup saving at about three quarters of a run for a file that imports twelve modules and uses one, with 54 modules insys.modulesrather than 189 and 15 code objects read off disk rather than 110.The finding I did not expect is in the last section. Waking a placeholder does not take the per module name lock that an ordinary import takes.
_PyImport_LoadLazyImportTstatetakes the interpreter wide import lock and holds it for the whole resolution, module body included, which the source itself calls serialising reification. Two recordings on a free threaded build show what that costs: four threads importing four different modules the ordinary way keep 3.60 cores busy, and four threads waking four different deferred imports keep 0.98. That is the one unobservable claim the lesson leans on, and both recordings are checked in.Everything lazy is 3.15 only, CI runs notebooks on 3.14 and Pyodide is 3.14.2, so every lazy cell is guarded by
hasattr(sys, "lazy_modules")and prints one honest line on the else branch instead of going silent. The cells compile their sample source at run time and execute it with a single namespace dict, which is whatis_lazy_import_module_levelchecks for, so a module level statement can run inside a notebook cell at all.This also carries a fix to R03 that has nothing to do with lazy imports. Re-recording the browser probe turned up
r03-17failing withModuleNotFoundError: No module named 'three', on a lesson this branch does not otherwise touch. The Emscripten filesystem keeps whole second timestamps, so the directory mtime does not always move when the cell writes a new file into it, andFileFinderkeeps serving its stale listing. The cell now falls back to dropping the finder out ofsys.path_importer_cacheand says so in the prose. It is an intermittent failure that would have gone red in CI at random, and one probe recording covers both changes, so splitting it out would have meant two ten minute probe runs for no gain.Also here: six diagrams, three glossary terms (lazy import, import placeholder, global import lock), rows in both READMEs, and the refreshed citation lock, claims list and probe recording.
Checked locally with the full
just check, which is green, plusjust versionswith no stale notes and a full probe re-record where all 76 lessons run end to end on Pyodide.