Flask Creator Says Exposed CPython Internals Block Multicore Upgrades

How Python was Shaped by leaky Internals, Armin Ronacher, Flask framework — Видео с мероприятий {speach!

Видео с мероприятий {speach!

CPython's early exposure of raw C struct slots and static type pointers locked external libraries into legacy mechanics, permanently barring Global Interpreter Lock removal and leaving Python slower and less dynamic than Ruby.

AI-assisted summary of How Python was Shaped by leaky Internals, Armin Ronacher, Flask framework — original source. Check the original for full context.

Source published:

Key ideas

  • CPython's arithmetic dispatch does not simply map to special dunder methods like __add__; instead, it relies on low-level C struct function pointers (slots) and fast-path type checks spanning hundreds of lines of C code. (5:36)
  • Exposing raw interpreter internals—including sys._getframe(), sys.modules, and reference-counting C APIs—has locked Python into backward compatibility constraints that prevent removing the Global Interpreter Lock (GIL) and supporting multi-version module imports. (43:19)
  • CPython's built-in primitive types reside at static global C memory addresses, preventing embedders like Nginx from safely running multiple isolated Python interpreter instances across threads in a single process. (39:53)
  • Python's commitment to O(1) character indexing forced memory-wasteful fixed-width Unicode implementations (UCS-2 and UCS-4), unlike modern languages like Go and Rust that embrace variable-length UTF-8 without random character indexing. (30:03)

Detailed notes

## Background This talk was delivered by Armin Ronacher, author of the Flask framework, and published on YouTube by the channel 'Видео с мероприятий {speach!' on July 13, 2016. No description or show notes were provided with the original upload.

## TL;DR Armin Ronacher examines how CPython's low-level C virtual machine implementation has inadvertently dictated Python's language semantics and restricted its evolution. He deconstructs the common misconception that basic operations like addition simply resolve to dunder method lookups like `__add__`, revealing instead an intricate maze of internal C slots, fast-path type checks, and sequence concatenation fallbacks. Ronacher highlights how leaky internals—ranging from fixed-width Unicode indexing and static global type structs to exposed stack frames via `sys._getframe()` and mutable global caches in `sys.modules`—have empowered debugging tools while permanently locking the ecosystem into decisions that obstruct multithreading, versioned imports, and removing the Global Interpreter Lock (GIL). Ultimately, he argues that Python sacrificed architectural purity and cache-friendly performance for ad-hoc optimizations, offering lessons for future runtime designers on keeping internal APIs minimal.

## Atmosphere & style The presentation is candid, self-deprecating, and unapologetically technical, balancing deep low-level mechanics with dry humor about Python's historical idiosyncrasies. Ronacher speaks not as an ideological evangelist, but as a battle-hardened open-source maintainer sharing a mix of pragmatic admiration and lingering frustration over the interpreter's architectural traps. Watching the talk feels like peering under the hood of a beloved, familiar engine alongside a master mechanic who openly confesses where the bolts are stripped, creating an engaging, conversational atmosphere that invites active curiosity from the audience.

## Bulletin - [05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) CPython's arithmetic dispatch does not simply map to special dunder methods like __add__; instead, it relies on low-level C struct function pointers (slots) and fast-path type checks spanning hundreds of lines of C code. - [43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) Exposing raw interpreter internals—including sys._getframe(), sys.modules, and reference-counting C APIs—has locked Python into backward compatibility constraints that prevent removing the Global Interpreter Lock (GIL) and supporting multi-version module imports. - [39:53](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2393s) CPython's built-in primitive types reside at static global C memory addresses, preventing embedders like Nginx from safely running multiple isolated Python interpreter instances across threads in a single process. - [30:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1803s) Python's commitment to O(1) character indexing forced memory-wasteful fixed-width Unicode implementations (UCS-2 and UCS-4), unlike modern languages like Go and Rust that embrace variable-length UTF-8 without random character indexing.

## Key takeaways - [05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) Teaching that binary addition translates directly to calling a.__add__(b) is inaccurate; CPython executes BINARY_ADD via fast-path checks and C-level type slots rather than attribute lookups. - [11:54](https://www.youtube.com/watch?v=qCGofLIzX6g&t=714s) CPython operates two parallel systems—C struct function pointers (slots) and Python class dictionaries—and expends significant runtime machinery attempting to synchronize them. - [10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) Binary operator evaluation remains largely unspecified and emergent, leading alternative implementations like PyPy to occasionally disagree with CPython's exact behavior in edge cases. - [17:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1072s) Internal artifacts like slot_wrapper and method_descriptor leak into Python userland, producing observable behavioral discrepancies between native types and user-defined classes that production code now relies upon. - [21:45](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1305s) In older Python versions, built-in C types like compiled regex objects bypassed standard dunder protocols entirely, preventing pure Python implementations of built-ins like repr(). - [23:51](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1431s) The evolution from old-style to new-style classes broke dynamic transparent proxying via __getattr__ because special method lookup was moved directly to class-level slot inspection. - [25:27](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1527s) Early Unicode adoption led Python to guarantee O(1) random character indexing via fixed-width UCS-2 and UCS-4 arrays, causing massive memory bloat compared to UTF-8 representations. - [32:46](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1966s) While considered bad language design, exposing sys._getframe() became the foundation for critical ecosystem tooling, including Sentry, Flask's interactive debugger, and Twisted Manhole. - [37:35](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2255s) Relying on sys.modules as a globally exposed flat dictionary makes side-by-side loading of multiple library versions impossible without breaking backward compatibility for tools like pickle. - [39:53](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2393s) Built-in types like int use static global memory pointers for rapid pointer equality checks, which prevents embedding multiple isolated interpreters across threads within a single operating system process. - [43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) The Global Interpreter Lock cannot be easily removed because foundational interpreter structures, shared static types, and third-party C extension reference counting depend on existing thread-unsafe guarantees. - [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) Keeping the public interpreter API minimal—as JavaScript engines do—prevents ecosystem lock-in and allows runtimes to evolve and optimize aggressively. - [47:25](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2845s) CPython's complex special-casing and bloated instruction footprint cause CPU instruction cache misses and branch mispredictions, leaving Python slower per opcode than fully dynamic languages like Ruby.

## Detailed walkthrough ### [00:06](https://www.youtube.com/watch?v=qCGofLIzX6g&t=6s) Introduction and Armin Ronacher's Background

Armin Ronacher opens the talk by explaining that the subject is Python inside the virtual machine and how internal implementation choices end up shaping Python as a language. Because internal virtual machine mechanics can easily become dry and tedious, he encourages the audience to interrupt and ask questions throughout to keep things lively. He points out a curious tension: while developers broadly enjoy writing Python, the interpreter itself is remarkably complex and restrictive under the hood, simultaneously enabling certain workflows while severely limiting others.

```mermaid flowchart TD accTitle: Armin Ronacher Open Source Ecosystem accDescr: Diagram illustrating Armin Ronacher's major projects and ecosystem contributions. subgraph SentryEcosystem["Sentry Stack"] S["Sentry (Error Reporting)"] D["Django Framework Base"] S --> D end subgraph WebAndTooling["Web Frameworks & Libraries"] F["Flask Framework"] L["Lector (Static CMS)"] W["Werkzeug"] J["Jinja2"] M["MarkupSafe"] end ```

Ronacher sketches his own history with the runtime. His first contact with Python was version 2.0, though he actively started developing on Python 2.2. Driven by curiosity about how the language and its interpreter evolved, he recently compiled Python 1.3 on a modern computer—a surprisingly smooth process that yielded a functional binary and revealed how much legacy code has persisted untouched across decades. Despite holding commit access to CPython for "inexplicable reasons," he clarifies that he has rarely contributed directly to the core runtime; his perspective comes instead from building large systems and popular libraries, including his day job on Sentry (a Django-based error reporting tool), authoring the Flask web framework, creating the static CMS Lektor, and building core libraries like Werkzeug, Jinja2, and MarkupSafe.

- First encountered Python at version 2.0; began writing active code around version 2.2. - Successfully built and ran Python 1.3 on modern machines to inspect original interpreter behavior. - Core author/maintainer of Flask, Lektor, Werkzeug, Jinja, and MarkupSafe. - Primary day job: engineering at Sentry, a Python and Django-based error-tracking platform.

---

[02:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=172s) Dynamic Languages, Optimization, and the Constraints of CPython

Surveying the landscape of programming languages on an axis ranging from strictly static to highly dynamic, Ronacher contrasts Python with Ruby. The Ruby community often highlights its extreme dynamism, such as monkey-patching core primitives—for instance, redefining arithmetic on the `Float` class to turn addition into subtraction. Python nominally forbids patching core built-in types such as overriding `float.__add__`. Yet Ronacher argues that Python is actually far more dynamic than Ruby in ways that make it significantly more difficult to optimize.

Because tutorials and documentation describe an idealized model of how Python executes, developers are often blind to what the virtual machine is really doing. When internal realities conflict with expectations, changing the runtime becomes almost impossible because production codebases inadvertently depend on quirky implementation details. These historical guarantees constrain major architectural redesigns:

> “Certain things that people want to have for a long time is getting rid of the global interpreter lock and that turns out to be a hard and also very much relevant to how the interpreter was designed initially and what guarantees it gives…”

These structural restrictions prevent the runtime from: - Removing the Global Interpreter Lock (GIL) to achieve true multicore parallelism. - Loading multiple distinct versions of the same shared library into the same process simultaneously. - Running multiple isolated sub-interpreters side-by-side within a single executable process without shared state leaks. - Modernizing core type system internals without breaking backwards compatibility.

Ronacher adds that Python 3 compounded this friction by layering substantial architectural complexity on top of an already intricate core, sometimes with insufficient design peer review.

---

[05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) The "Big Lie" of `__add__` and the Anatomy of Addition

Ronacher displays a simple Python snippet to illustrate the gap between developer perception and interpreter reality:

```python magic = 42

def add_magic(a): return a + magic ```

Standard tutorials routinely teach that executing `a + magic` evaluates conceptually to invoking `a.__add__(magic)`. Ronacher flatly rejects this: “A lot of tutorials on the internet will tell you that this is actually equivalent to this, which it's not really… in fact it is totally not that.”

Instead, the source code compiles down to CPython bytecode containing the `BINARY_ADD` opcode. Execution shifts inside `ceval.c`, an enormous file of several thousand lines structured around a massive evaluation loop and switch statement handling every single opcode.

```mermaid flowchart TD accTitle: CPython BINARY_ADD Execution Flow accDescr: Step by step internal path for evaluating BINARY_ADD down to C slots. A["Python code: a + magic"] --> B["Bytecode: BINARY_ADD"] B --> C["ceval.c: main opcode evaluation loop"] C --> D{"Exact type checks on stack args"} D -->|"Exact int + int"| E["Fast-path integer addition"] D -->|"Exact str + str"| F["Fast-path string concatenation"] D -->|"Fallback / Generic"| G["PyNumber_Add(v, w)"] G --> H["Inspect C struct slot: tp_as_number->nb_add"] H --> I["Sequence concatenation check or binary operation"] ```

When `BINARY_ADD` executes, it pops two operand arguments (`v` and `w`) from the value stack and walks through an extensive sequence of conditionals: - It checks whether both arguments are strictly, exactly `int` (excluding subclasses). - It checks whether both arguments are strictly, exactly `str`. - If neither fast-path matches, control drops into `PyNumber_Add(v, w)`.

Despite its name, `PyNumber_Add` is misleadingly labeled; Ronacher points out that it functions as a general `PyObject_Add` because it also encapsulates sequence concatenation logic.

---

[08:32](https://www.youtube.com/watch?v=qCGofLIzX6g&t=512s) C Slots vs. Dunder Methods in CPython

Ronacher clarifies a fundamental ambiguity in Python terminology regarding "slots." Most developers associate the word purely with the class-level `__slots__` attribute, which eliminates an instance's underlying `__dict__` to conserve memory. However, the runtime relies on a second, distinct kind of slot: internal C-level struct function pointers embedded directly within `PyTypeObject`.

Every Python object contains an `ob_type` pointer referencing its type descriptor. Within this type structure lies `tp_as_number`, a pointer to a `PyNumberMethods` struct containing function pointers such as `nb_add`. Instead of looking up the string `"__add__"` inside an attribute dictionary, the C execution engine inspects the `nb_add` function pointer slot of both operands:

- The runtime checks `v->ob_type->tp_as_number->nb_add`. - It evaluates the relationship between the types of `v` and `w` to determine method precedence and handle reverse operations. - The C implementation required to coordinate and dispatch this single addition operation amounts to roughly 400 lines of low-level C code.

Ronacher concludes that teaching developers that arithmetic translates directly to `__add__` lookup is "the big lie." The runtime bypasses attribute dictionary lookups in favor of these fixed C function pointer slots, creating an execution reality radically detached from standard object model explanations.

[10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) The Mystery of Adding Two Objects

Reasoning about fundamental operations inside the Python interpreter turns out to be surprisingly frustrating. In an ideal world, taking object `A` and object `B` and applying addition, subtraction, or multiplication would trigger a simple, deterministic sequence of steps. JavaScript, for instance, introduced clear standards specifying exactly how two objects are evaluated during addition. In Python, however, determining what actually happens is remarkably difficult. Especially in Python 2, the outcome depends on internal object flags and intricate, specific type relationships between the two operands. As Armin Ronacher puts it: “I would argue that there is probably not a single programmer in in the python Community who could tell you exactly what happens if you add two objects together.”

This lack of certainty is evidenced by projects like PyPy, which attempts to re-implement Python in Python itself. Even though PyPy's core developers aim for bug-for-bug compatibility with CPython and should theoretically get it right, PyPy still occasionally disagrees with CPython’s exact behavior in edge cases. The underlying mechanics are simply too fraught with historical quirks to produce an obvious specification.

- JavaScript explicitly specifies the behavior of binary operators on objects in its language standard. - Python’s binary operations depend on low-level object flags and complex runtime type hierarchies. - PyPy periodically diverges from CPython regarding binary operations because the true behavior is an emergent property of CPython's C implementation rather than a formal specification.

---

[11:54](https://www.youtube.com/watch?v=qCGofLIzX6g&t=714s) Two Parallel Worlds: C Slots and Python Classes

To understand why operator dispatch is so complex, one must look at the concept of a type "slot." In the CPython interpreter, a slot is essentially a function pointer on a C `struct` maintained for every single type. If you inspect an integer from the Python side, you see a class dictionary containing a special method called `__add__` pointing to a callable. However, the C interpreter was designed in the early 1990s, long before these special dunder methods existed. Back around 1995, Python maintained two completely disjoint systems: Python classes and native C types. Built-in types implemented in C had no concept of Python class dictionaries; adding two built-in integers directly executed a C struct function pointer in the number methods suite known as `nb_add`.

Later, support was added for user-defined Python classes, allowing them to participate in operator overloading via callback methods like `__add__`. This produced two completely separate evaluation models. Python eventually attempted to unify them by building a synchronization layer. When a C type defines an `nb_add` slot, CPython uses runtime machinery to expose that slot to Python code as `__add__`. Conversely, when a pure Python class declares an `__add__` method, CPython extracts that function and populates the underlying C type's slot table (`tp_as_number->nb_add`) with a C function pointer that delegates back to the Python method. In theory, `Foo.__add__` and `FooType->tp_as_number->nb_add` are kept in sync, but legacy decisions make clean synchronization impossible.

```mermaid flowchart TD accTitle: C Slots vs Python Special Methods Synchronization accDescr: Diagram illustrating how C-level type slots and Python-level dunder methods attempt to synchronize with one another.

subgraph C_Side["C-Level Type Struct"] CSlot["C Slot: tp_as_number->nb_add"] end

subgraph Python_Side["Python-Level Class Dict"] PyMethod["Special Method: __add__"] end

CSlot -->|"Slot wrapper exposes C func"| PyMethod PyMethod -->|"Internal magic installs wrapper into slot"| CSlot ```

- In early Python (circa 1995), native C types and user-defined Python classes lived in separate worlds with zero shared inheritance. - C types executed arithmetic via specialized C struct slots such as `nb_add`. - User-defined classes handled arithmetic through dunder methods like `__add__`. - Modern CPython continuously tries to keep C struct function pointers (`tp_as_number->nb_add`) and Python dictionary entries (`__add__`) synchronized.

---

[14:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=843s) Number Addition vs. Sequence Concatenation

The early design of Python introduced another historical rift: the distinction between numbers and sequences. Very early on, Guido van Rossum provided built-in numbers (integers and floats) alongside sequences (strings, lists, and tuples). He opted to create two separate C-level concepts for addition: one for numeric arithmetic and another for sequence concatenation. As a consequence, C structs hold distinct slot suites for numbers (`tp_as_number`) and sequences (`tp_as_sequence`).

Whenever the `+` operator executes, the interpreter must inspect both slots, introducing another vector for desynchronization. While both conceptually map to `__add__` at the Python layer, explaining how they resolve in practice is nearly impossible. Most tutorials take the easy way out and simply claim that `a + b` translates directly to `a.__add__(b)`. More conscientious tutorials might suggest it evaluates as `type(a).__add__(a, b)`. Yet both explanations are fundamentally inaccurate representations of what the virtual machine actually does.

```mermaid flowchart TD accTitle: Real Execution Flow of a + b in CPython accDescr: Flowchart showing the actual series of checks and fallbacks executed by CPython when evaluating a plus b.

Start["Evaluate: a + b"] --> CheckInt{"Are both a and b integers?"} CheckInt -- Yes --> FastInt["Execute specialized fast integer addition"] CheckInt -- No --> CheckStr{"Are both a and b strings?"}

CheckStr -- Yes --> FastStr["Execute specialized string concatenation"] CheckStr -- No --> CheckNumSlot{"Does type(a) implement number slot nb_add?"}

CheckNumSlot -- Yes --> ResolveNum["Resolve number slots based on complex type relationships"] CheckNumSlot -- No --> CheckSeqSlot{"Does type(a) implement sequence slot sq_concat?"}

ResolveNum --> InvokeNum["Invoke nb_add slot"] CheckSeqSlot -- Yes --> InvokeSeq["Invoke sq_concat slot via sequence path"] CheckSeqSlot -- No --> RaiseErr["Raise TypeError"] ```

- Early Python split the `+` operator into numeric addition (`nb_add`) and sequence concatenation (`sq_concat`). - Standard educational material oversimplifies the operation as `a.__add__(b)` or `type(a).__add__(a, b)`. - The real interpreter execution order includes: 1. A fast-path check to see if both operands are pure integers. 2. A fast-path check to see if both operands are strings (for optimized concatenation). 3. Querying whether `a` implements numeric slots, checking `b`'s slots, and applying inheritance precedence rules. 4. Falling back to the sequence concatenation path if numeric addition is not implemented.

---

[16:10](https://www.youtube.com/watch?v=qCGofLIzX6g&t=970s) The Cost of Internal Optimization and Frozen APIs

The contrast between what the interpreter does and what Python syntax suggests is striking. Evaluating `a.__add__(b)` involves looking up the attribute name on the type and calling it directly. In Armin's words: “The scary thing is this thing is easier than this thing.” The internal fast paths and slot resolution systems built to speed up arithmetic are vastly more complicated than a basic method lookup. Because this dictionary-based path is unoptimized and rarely used internally, there are even peculiar situations where manually writing out `a.__add__(b)` runs faster than letting the interpreter evaluate `a + b`.

Over decades of development, CPython accrued layers of optimizations to accelerate operations within this complex architecture. A reasonable engineer might ask why the community does not simply discard the convoluted C dispatch slots, optimize the clean Python method path, and standardize `a + b` as `type(a).__add__(a, b)`. The barrier is that CPython has exposed its low-level implementation details to the external ecosystem for so long that the core team can no longer change it. Armin explains the trap: “And the reason for this is that unfortunately we expose so much of the language to the ecosystem around it that we can't change The Interpreter anymore.”

- Manually executing an explicit dunder method lookup can, in specific scenarios, bypass slot resolution overhead and run faster than using operator syntax. - CPython cannot replace its internal dual-slot mechanism with unified method lookups without breaking the global Python package ecosystem.

---

[17:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1072s) Leaky Internals: Slot Wrappers and Method Descriptors

Anyone who has compiled a C extension is familiar with the low-level nature of Python’s C API, where custom types must explicitly populate raw C struct slot tables. However, these internals leak directly into high-level userland Python code. Even beginners who have never touched C routinely encounter cryptic exceptions referencing internal types, such as errors stating that a `slot_wrapper` has no attribute.

This leakage creates profound semantic discrepancies between native and user-defined types. Defining a class in Python and inspecting its `__add__` method yields an `unbound method` (or a regular function in modern Python). Inspecting the exact same attribute on a built-in C type like `int` yields a `slot_wrapper`. A slot wrapper is an internal CPython construct that wraps a raw C function pointer so Python can invoke it, yet it surfaces as a distinct Python object with unique behaviors.

By comparison, JavaScript avoids this fragmentation: a function is simply a function across the entire language, whether authored in JavaScript or implemented inside the browser engine in C++. Python, however, exposes an enormous menagerie of callable artifacts: - Standard functions (`function`) - Unbound methods / Bound methods (`instancemethod` / `method`) - Slot wrappers (`slot_wrapper`) - Method descriptors (`method_descriptor`) - Built-in functions (`builtin_function_or_method`) - Arbitrary callable objects implementing `__call__`

These types behave differently in subtle, critical ways. For example, if you pull an unbound method off a custom class and assign it as an attribute on another class, it acts as a descriptor, automatically binding `self` when accessed from an instance. If you do the exact same thing with a C-level `slot_wrapper`, it does not bind `self`. Python developers have come to rely on these incidental edge cases in production code. A common idiom is setting a class-level conversion hook using a built-in type:

```python class Record(object): conversion_function = int ```

This pattern only works because `int` is a C type whose call mechanism does not implement method descriptor binding; assigning a normal user-defined function there would cause it to receive `self` as its first parameter and fail. Because developers rely on these subtle differences, CPython can neither clean up its internal type architecture nor eliminate these discrepancies. Every alternative implementation, including PyPy, is forced to painstakingly replicate every single leaky internal detail.

[21:45](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1305s) Leaky C Extensions: The Curious Case of the `re` Regex Object

The internal design of Python is fraught with complexity, and because nearly everyone depends on these behaviors, optimizing the runtime has become virtually impossible. These low-level details do not just affect performance when emulating Python in other languages—they actively shape how developers write Python code. A striking, historical example of this leakage existed in the standard `re` regular expression module prior to Python 2.7. When compiling a regular expression, the runtime returned an internal pattern object (such as `SRE_Pattern`). If a developer attempted to introspect that compiled pattern by accessing `__class__`, Python raised an `AttributeError`. Similarly, accessing `__repr__` directly failed, even though calling `repr()` worked and the object otherwise behaved like a normal Python object.

Armin Ronacher highlights how deeply bizarre this situation was: “The C interpreter sees a different version of this object than Python code would do.” Because the object was written in C, its author bypassed standard Python attribute resolution and customized its behavior purely within the C API. At the C layer, class and representation slots existed, but they were never exposed as Python-level dunder attributes. Consequently, it was impossible to write a pure Python implementation of the built-in `repr()` function that reliably invoked `__repr__` across all standard types, because objects like the compiled regex pattern explicitly disagreed with Python-level introspection. This discrepancy demonstrated how the C API allowed internals to remain completely invisible and inconsistent from the perspective of high-level Python code.

- Compiled regular expression objects in Python versions prior to 2.7 raised `AttributeError` when accessing `__class__` or `__repr__`. - The discrepancy stemmed from C-level object definitions overriding internal struct slots without exposing the corresponding dunder attributes to the Python namespace. - Pure Python emulations of core built-in functions (like `repr()`) could not be fully implemented in Python because certain built-in C objects broke the dunder contract.

---

[23:51](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1431s) Old-Style Class Proxying and the Evolution of `__getattr__`

Another vivid instance of internal implementation leaking into language semantics lies in the difference between old-style and new-style classes, particularly around Python 2.4. In older versions of the language, developers could construct a proxy object using an old-style class that defined `__getattr__` to forward attribute lookups to a wrapped object. If the proxy forwarded calls to an integer like `42`, attempting to evaluate the proxy in the interpreter would successfully call `__repr__` by dispatching through `__getattr__`, finding the wrapped integer's representation function, and returning `"42"`. It operated as a flawless 100% transparent proxy.

Modern Python, however, no longer permits this pattern because special method lookups bypass `__getattr__` and inspect the class dictionary directly. Old-style classes relied on an internal `instance` type that forwarded all special method calls dynamically, matching the original mechanics of early Python interpreters. When Python introduced new-style classes, it preserved old-style behavior through an increasingly complex emulation layer rather than keeping a single, unified dispatch model. Ronacher reflects on this shift: “So when you actually consider how Python as a language was taught to people, at one point this `__getattr__` was actually correct; it just became more complex over the years and now has this massive sort of disagreement in how it works.”

```mermaid flowchart TD accTitle: Evolution of Dunder Method Dispatch in Proxies accDescr: Comparison of old-style proxy dispatch via getattr versus modern class-level slot lookup.

subgraph OldStyle["Old-Style Class Dispatch (Python 2.4)"] A["Proxy Instance"] -->|"Lookup __repr__"| B["__getattr__('__repr__')"] B -->|"Dynamic forward"| C["Wrapped Target (e.g. 42)"] C -->|"Returns target __repr__"| D["Transparent 100% Proxy"] end

subgraph NewStyle["Modern / New-Style Dispatch"] E["Proxy Instance"] -->|"Lookup __repr__"| F["Proxy Type Object Slot"] F -.->|"Bypasses __getattr__"| G["AttributeError / Default Object Repr"] end ```

---

[25:27](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1527s) The History and Flaws of Fixed-Width Unicode Storage

The historical handling of strings in Python provides another clear illustration of how implementation constraints shaped the language. Originally, Python had only byte strings, which were straightforward byte arrays where each character held an integer value from 0 to 255. This structure established a strong expectation among developers: string indexing is $O(1)$, meaning one can immediately access the first, second, or fourth character by offset. When Unicode support was introduced, Python's designers sought to preserve this direct indexing guarantee. Instead of adopting a variable-length byte representation, they modeled Unicode as arrays of fixed-width numbers, initially spanning from 0 to 65,535 (UCS-2), and later allowing builds configured with UCS-4 for larger code points.

This approach was heavily influenced by early standardization efforts. Around 25 years prior, two separate bodies attempted to expand beyond ASCII. The Unicode Consortium initially believed that 65,535 characters would suffice for all the world's writing systems. Simultaneously, an ISO committee pursued a 32-bit standard ($2^{32}$ characters), which stalled because they could not devise an efficient encoding scheme. Eventually, it became apparent that Unicode exceeded 65,535 characters—emojis, in particular, require code points well beyond the 16-bit plane. While UTF-8 emerged as a variable-length solution that efficiently encoded common scripts (ASCII in 1 byte, Cyrillic in 2 bytes), Python 2 stuck with fixed-width build configurations. Under a UCS-2 build, accessing an emoji split the character into two surrogate halves. To process emojis properly, Python had to be compiled with UCS-4, which allocated 4 bytes per character. Ronacher underscores the inefficiency: “If you want to deal with emojis in Python 2 you would have to compile it with UCS-4 which takes then four bytes per character, which is an enormous waste; in particular, it's an enormous waste because the largest character which can ever be in Unicode is only 21 bits I think, yeah 21 bits, which is just slightly above half of it, so you will have the vast majority of your space unused.”

- The Unicode Consortium originally operated on the faulty assumption that 65,535 code points (UCS-2) would cover every character. - Unicode code points max out at 21 bits, meaning UCS-4 builds (32 bits per character) leave massive amounts of allocated memory permanently zeroed out. - Under UCS-2 builds of Python 2, supplementary characters like emojis cannot fit into a single slot and break into surrogate pairs when indexed.

---

[30:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1803s) The $O(1)$ Indexing Guarantee vs. Modern Variable-Length UTF-8

The central reason Python endured such massive memory bloat instead of using UTF-8 was its rigid commitment to $O(1)$ random character indexing: “Because we guaranteed that if you access the first character you will get the first character and if you access the second character you'll get the second character, and the only way to do this in UTF-8 would be to scan character by character by character to figure out which one you want to access.” With UCS-2 or UCS-4, the interpreter simply offsets memory by multiplying the index by the fixed width (2 or 4 bytes). Over the preceding decade, however, the industry coalesced around UTF-8 as the de facto standard. Modern systems languages like Go and Rust embraced UTF-8 natively and intentionally omit arbitrary index-based character access, while platforms like Apple's Swift avoid teaching developers that random character indexing is an efficient operation.

Python 3 addressed this problem by implementing a flexible string representation (PEP 393), where each string's backing array adjusts to 1, 2, or 4 bytes depending on the widest character it contains. Even with this optimization, Python stubbornly retains the original guarantee of constant-time character indexing. Ronacher argues that this guarantee was never the outcome of a deliberate architectural plan, observing that “this guarantee is only given to you because the interpreter decided at one point that that is a reasonable thing to do; it was not, I don't think there was a conscious decision in doing this, I think it just happened.” This reflects a recurring theme: the language definition is essentially identical to the CPython interpreter implementation. While this tight coupling hampers optimization and occasionally causes desynchronization, it also grants Python developers a visceral, hands-on intimacy with the underlying machine, contrasting sharply with the sandboxed, high-level abstraction experienced by JavaScript developers.

- In pure UTF-8, retrieving a character at index $N$ requires an $O(N)$ linear scan from the beginning of the string to decode preceding multibyte sequences. - Newer languages such as Go and Rust do not provide constant-time random character access on UTF-8 strings by design. - Python 3 mitigates storage waste via a tiered internal representation (Latin-1, UCS-2, or UCS-4 per string), preserving the historic $O(1)$ indexing contract. - CPython's language boundaries are defined by its concrete implementation, creating a stark contrast with sandbox-oriented runtimes like JavaScript in web browsers.

[32:46](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1966s) The Leaky Power of `sys._getframe()`

Armin Ronacher explains that because Python's interpreter has always shone through so directly, a unique ecosystem developed around its internals. While exposing the interpreter state makes it notoriously hard to eliminate the Global Interpreter Lock (GIL) or implement certain compiler optimizations, it also gives developers immense power. When polling the audience on who uses or has heard of `sys._getframe()`, only a shockingly small number of hands go up. Armin characterizes `sys._getframe()` as the “secret sauce” that makes a lot of Python code spicy—if not necessarily clean. At any point during execution, code can ask the interpreter for the internal frame object of the current call site, exposing nearly the entire interpreter state. From the perspective of language design, this is generally considered completely unacceptable: “If you would go into designing a new high-level language, everybody would argue that never give this thing—this thing is slow, it's ridiculous, just don't expose it, it shouldn't exist.” However, because Python exposed it early on and libraries immediately leaned into it, core developers were forced to keep frame inspection relatively performant, unlike in languages like Ruby where similar mechanisms exist but crawl because nobody relies on them.

A frame object allows runtime inspection and sometimes mutation of local variables, retrieval of the current filename, examination of active exceptions, and walking the call stack. This power has invited both clever utility and sheer abuse across the ecosystem. For example, `zope.interface` used a method called `implements` that walked up one frame from its call site to dynamically inject attributes directly into the enclosing class definition—a technique that makes little sense from a conventional calling perspective. Similarly, the standard library `warnings` module uses stack frames to anchor deprecation warnings two frames above the call site rather than pointing at the warning machinery itself, and the `logging` module skips internal logging frames so log lines report where the user actually called the log function rather than where the logging helper was implemented.

Beyond standard library tricks, this deep frame access underpins critical debugging and observability infrastructure. Armin brings up Twisted Manhole, an obscure tool allowing developers to attach a remote interactive debugger to a long-running, misbehaving Python server process—such as a game world simulation—and inspect state without shutting it down. He notes that this same capability powers Sentry, where he works: when an application crashes, Sentry extracts every local variable, frame duration, and stack layer directly from the live runtime state. Python web frameworks depend heavily on this leakiness; when a Django or Flask application crashes, it displays detailed tracebacks, and Flask even allows developers to execute arbitrary code interactively inside an HTML-rendered traceback frame to inspect the environment.

- `sys._getframe()` returns a frame object exposing runtime local variables, active exceptions, and current execution coordinates. - Core Python developers were forced to optimize frame access because real-world code became dependent on it, unlike in Ruby where equivalent mechanisms remain unoptimized. - `zope.interface` used frame inspection to alter class definitions from within body-level function calls. - The standard `warnings` and `logging` modules inspect callers to skip internal frames and attribute warnings or logs to caller sites. - Twisted Manhole, Sentry, Django debug pages, and Flask's interactive web debugger all fundamentally rely on exposed frame internals.

[37:35](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2255s) `sys.modules` and the Impossibility of Multi-Version Imports

Armin shifts to another deeply exposed piece of interpreter state: `sys.modules`. Whenever Python imports a module, the module object is cached in this dictionary. This ensures that when disparate parts of an application import the same module (such as `cgi`), both call sites receive the exact same instance. While convenient, exposing this dictionary directly caused the broader ecosystem to treat it as an invariant public API. Developers regularly look up classes by dynamically importing a module name and querying `sys.modules`, the standard `copy` module inspects it, and `pickle` relies on it entirely to unpickle and reconstruct serialized objects.

Because the entire ecosystem relies on `sys.modules` being a flat global dictionary mapping simple module names to module instances, modern architectural improvements cannot be introduced without breaking backward compatibility. There have been numerous attempts to allow multiple versions of the same library to coexist within a single Python runtime, but this cannot be implemented without adding version or scope identifiers into `sys.modules`. Serialized data formats lock Python into this legacy behavior forever: “So until pickle dies, we will never have versioned modules—this is disappointing.” Armin points out that Zope built an entire database (ZODB) around pickling objects, meaning any breaking architectural change to `sys.modules` or `pickle` would invalidate decades of persisted database records.

```mermaid flowchart TD accTitle: How sys.modules restricts versioning accDescr: Import requests check sys.modules, caching a single global instance, which prevents loading multiple versions.

subgraph ImportCache["Runtime Import Resolution"] A["import foo"] --> B{"foo in sys.modules?"} B -- Yes --> C["Return cached module instance"] B -- No --> D["Load file & store into sys.modules['foo']"] end

subgraph EcosystemLock["Ecosystem Dependencies"] E["pickle.loads()"] -->|"queries exact module name"| C F["copy.copy() / inspect"] -->|"expects single global instance"| C G["Multiple library versions\n(e.g., foo v1 & foo v2)"] -.->|"BLOCKED: cannot key by version without breaking pickle"| B end ```

[39:53](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2393s) Builtin Types, Static Memory Locations, and Embedding Woes

Armin asks the audience whether they have ever wondered why `int` is reported as a type while an empty subclass of `int` is reported as a class, noting that while Python 3 reconciles the terminology somewhat, the underlying runtime mechanics remain identical. The fundamental distinction is that built-in types live at fixed, static global memory addresses within the CPython binary, whereas user-defined classes sit dynamically allocated on the heap. When CPython performs primitive arithmetic operations—such as adding two numbers together—it needs to check whether both operands are exact integers as quickly as possible. Rather than performing dynamic lookups, the C code simply compares the operand’s type pointer against `&PyInt_Type`, a global variable in C.

Because `PyInt_Type` is a single static global C variable with internal reference counting and mutable state, it cannot be safely shared across isolated interpreter instances running on separate operating system threads within the same process. Armin compares this to how modern web browsers embed JavaScript engines: in a browser, every tab can run completely isolated JavaScript runtimes (like V8) across separate threads without sharing state. In Python, an embedder like Nginx cannot spin up five worker threads running five isolated Python interpreters inside the same process because all instances collide on static C types: “This is, I think, the biggest reason why Python was never a language that was embedded in things, because the interpreter design internally never supported this idea of having multiple interpreters running.” Armin shares that he personally attempted to fix this limitation by refactoring built-in types through a level of indirection so separate threads could host fully isolated interpreters, but the deep-rooted C design made it virtually impossible to pull off.

- Built-in types like `int`, `float`, and `str` are represented by static C structs (such as `PyInt_Type`) located at fixed memory addresses. - CPython performs identity checks (`obj->ob_type == &PyInt_Type`) via raw pointer comparisons for maximum speed during hot operations like integer addition. - Subclasses of primitive types are allocated on the heap, preventing the fast-path static pointer equality check from applying to them. - Because static type structs hold mutable internal state and reference counts, running multiple isolated interpreter runtimes across threads in a single process leads to memory collisions. - This structural limitation prevents Python from being embedded in multi-threaded architectures (like Nginx worker threads or multi-tab web browsers) in the way JavaScript runtimes are embedded.

[43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) The Deep Consequences of Leaky Internals and the Unremovable GIL

Armin Ronacher reflects on the cascading rabbit hole that emerges when developers and the wider ecosystem become dependent on low-level interpreter implementation details. Once these details are published and relied upon, they become impossible to change without breaking backward compatibility. The single most prominent casualty of this architectural legacy is the Global Interpreter Lock (GIL). Removing the GIL is not merely an engineering challenge; it is blocked because foundational interpreter assumptions—such as having a single static integer type shared across everything—prevent running isolated threads with independent interpreter states side by side. Furthermore, Python directly exposes internal reference counting mechanisms to the C API, weaving them into virtually every third-party C extension in existence.

This deep exposure extends upward into the highest layers of the language. For example, implementing cleanly isolated, multi-version libraries remains exceptionally difficult because `sys.modules` is exposed directly as an accessible interpreter detail rather than hidden behind an abstraction. Even the major backward-incompatible migration to Python 3 was unable to undo these foundational patterns. While this design has not completely stopped people from getting productive work done, it imposes a hard ceiling on interpreter speed, parallelism, and concurrency that the community must live with. Reflecting on this systemic trap, Armin notes: “It's something that I understand at this point that Python as a language is just this is how it works and we have to live sort of with this history of how the interpreter works and Python 3 also couldn't really change that, but it's something that maybe as you go in and then discover other languages is something that you wouldn't want to repeat.”

```mermaid flowchart TD accTitle: Cascading Consequences of Leaky Interpreter Internals accDescr: Diagram illustrating how exposing low-level interpreter details locks in the GIL and prevents architectural evolution.

subgraph Exposed["Exposed Internals"] RC["Exposed reference counts in C API"] ST["Shared static types (e.g., integer)"] SM["Publicly mutable sys.modules"] end

subgraph Ecosystem["Ecosystem Lock-in"] EXT["Ubiquitous C extensions rely on refcounts"] MOD["Single global module namespace"] end

subgraph Limitations["Architectural Bottlenecks"] GIL["Inability to remove GIL"] PAR["Impossible multi-interpreter threading"] MV["Difficulty supporting multi-version libraries"] end

RC --> EXT ST --> PAR EXT --> GIL SM --> MOD MOD --> MV ```

[45:20](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2720s) Dynamic Flexibility vs. Speed Ceiling and Closing the Talk

Because the interpreter is bound by these historical constraints, it struggles to parallelize and cannot easily become more dynamic without suffering severe performance penalties. The CPython interpreter is forced to "cheat" with specialized fast paths for operations like integer addition to stay performant despite its internal complexity. In contrast, a language like Ruby retains full dynamic flexibility—theoretically allowing a developer to monkey-patch integer addition into subtraction because it avoids hardcoded fast paths for primitives. In chasing raw execution speed under an overly complex model, Python ended up sacrificing both architectural purity and dynamic capability.

Armin halts his slide progression with self-deprecating humor, admitting that explaining the nuanced mechanics and widespread ramifications of leaky interpreter internals is surprisingly difficult to communicate in a standard lecture format. Acknowledging that the talk drifted into dense technical weeds, he invites the audience to ask questions and take the discussion in any direction they wish, even if it diverges wildly from the prepared presentation slides. As Armin puts it: “I'm rambling through the slides a little bit because I'm realizing that it's actually very hard to convey this overall message that how the interpreter works internally in a sense that is sort of easy to understand and what the ramifications are of it.”

[46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) Q&A: Small Public APIs and Modern Cache-Conscious Architecture

An audience member opens the Q&A by asking whether the fundamental lesson of the talk is that language designers must expose as tiny a public API surface as possible to avoid ecosystem lock-in. Armin enthusiastically agrees, pointing to JavaScript as an exemplary counter-model: engines like V8 keep their internal interpreter abstractions largely inaccessible to runtime code, preventing developers from building workflows dependent on low-level engine quirks. As Armin states: “The correct thing to do for a language is sort of what JavaScript did in many ways, where you keep the public exposed interpreter API very, very small to nonexistent, because then people will never get in this sense where they can start abusing it.”

The attendee follows up with a probing question about performance: if Python abandoned its special-case shortcuts—such as the fast paths in dictionary lookups and arithmetic—wouldn't the language become intolerably slow? Armin argues the opposite. While he understands why historical maintainers added special-casing to boost performance, he contends it was done the wrong way when evaluated against modern hardware architecture. Contemporary CPUs achieve high performance through cache locality, branch prediction, and compact instruction footprints that do not thrash CPU caches. In Python, an overwhelming amount of execution time is spent inside dictionary lookups. Armin argues that if Python drastically simplified the object model and stripped out complex special cases, the interpreter loop would execute significantly less code:

- **Cache utilization and branch prediction**: A minimal codebase fits comfortably in CPU instruction caches, drastically reducing cache misses and branch mispredictions. - **Dynamic flexibility**: Simplifying operations rather than maintaining sprawling manual C optimizations would allow Python to be simultaneously faster and more dynamic. - **The Ruby paradox**: Armin points out the bitter irony of CPython's current design: “I think the irony in this is that we are less dynamic than Ruby, but we are probably slower per opcode than Ruby is as a result of this, because it was obviously slow so people started doing these optimizations for the common cases, but we could have made the uncommon case significantly faster by simplifying it.”

The session closes with appreciative applause from the audience as the host thanks Armin for the insightful deep dive into Python's internals.

## Notable quotes > [10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) I would argue that there is probably not a single programmer in in the python Community who could tell you exactly what happens if you add two objects together. — Armin Ronacher

> [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) The correct thing to do for a language is sort of what JavaScript did in many ways, where you keep the public exposed interpreter API very, very small to nonexistent, because then people will never get in this sense where they can start abusing it. — Armin Ronacher

> [16:10](https://www.youtube.com/watch?v=qCGofLIzX6g&t=970s) unfortunately we expose so much of the language to the ecosystem around it that we can't change The Interpreter anymore. — Armin Ronacher

> [37:35](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2255s) until pickle dies, we will never have versioned modules—this is disappointing. — Armin Ronacher

## Mentioned Armin Ronacher, Python, CPython, Flask, Sentry, Django, Lektor, Werkzeug, Jinja2, MarkupSafe, Ruby, JavaScript, PyPy, Guido van Rossum, Go, Rust, Apple, Swift, Zope, ZODB, Twisted Manhole, Nginx, V8

[Source](https://www.youtube.com/watch?v=qCGofLIzX6g)

中文简报

Flask创作者称CPython暴露底层实现阻碍了多核升级 CPython早期暴露原始C结构体槽位和静态类型指针,使外部库受困于遗留机制,永久阻碍了全局解释器锁(GIL)的移除,并导致Python比Ruby更慢且动态性更低。 Armin Ronacher探讨了CPython的底层C虚拟机实现如何无意中决定了Python的语言语义并限制了其演进。他驳斥了诸如加法等基本操作仅简单解析为`__add__`等双下划线方法查找的常见误解,揭示了其背后由内部C槽位、快速路径类型检查以及序列拼接后备方案构成的复杂网络。Ronacher强调,从定宽Unicode索引、静态全局类型结构体,到通过`sys._getframe()`暴露的调用栈帧以及`sys.modules`中的可变全局缓存,这些泄露的底层内部细节虽然赋能了调试工具,却使整个生态永远受制于既有决策,阻碍了多线程、版本化导入以及全局解释器锁(GIL)的移除。他最终认为,Python为了临时优化牺牲了架构纯洁性和缓存友好性能,这为未来的运行时设计者在保持内部API极简方面提供了教训。 CPython的算术分发并不仅是简单映射到诸如__add__等特殊的双下划线方法,而是依赖于底层C结构体函数指针(槽位)以及跨越数百行C代码的快速路径类型检查。 (5:36) 暴露解释器原始内部细节(包括sys._getframe()、sys.modules以及引用计数C API)使Python受困于向后兼容性限制,从而阻碍了移除全局解释器锁(GIL)以及支持多版本模块导入。 (43:19) CPython的内置基础类型驻留在静态全局C内存地址中,导致像Nginx这样的嵌入方无法在单进程的多线程间安全运行多个隔离的Python解释器实例。 (39:53) Python对O(1)字符索引的执念迫使其采用了浪费内存的定宽Unicode实现(UCS-2和UCS-4),这不同于Go和Rust等采用变长UTF-8且不提供随机字符索引的现代语言。 (30:03)

Reading summary

Armin Ronacher argues that CPython's historical exposure of low-level implementation details directly to developers and C extensions has locked the runtime into legacy behaviors, hampering modern performance optimizations, concurrency, and clean architectural evolution. The Gap Between Dunder Abstractions and C Slots (5:36) Standard Python tutorials frequently teach that operators like addition evaluate to invocations of special dunder methods like a.__add__(b). Ronacher characterizes this as inaccurate, noting that the CPython execution engine instead compiles code down to opcodes like BINARY_ADD and relies on internal C function pointer slots within PyTypeObject, such as tp_as_number->nb_add, running roughly 400 lines of low-level C logic per dispatch. Because Python built-in types originated in the 1990s as disjoint C structs before user-defined classes supported operator overloading, CPython maintains an intricate synchronization layer attempting to bridge C struct slots and Python dictionary methods. Performance Traps in Fast Paths and Memory Inefficiencies (14:03) In attempting to accelerate operations within an overly complex type system, CPython introduced dedicated fast paths that bypass generalized dispatch. For instance, evaluating a + b executes hardcoded C-level checks to detect exact integer or string pairs before querying numeric slots or falling back to sequence concatenation. Ronacher notes that these convoluted fast paths can produce situations where manually invoking a.__add__(b) runs faster than using operator syntax. Similarly, historical guarantees like O(1) random character indexing led Python to use fixed-width UCS-2 and UCS-4 allocations—wasting substantial memory on modern Unicode code points like emojis—until Python 3 implemented PEP 393's tiered layout. Leaky Introspection Powers Ecosystem Tooling and Fragility (32:46) Python's willingness to expose raw interpreter internals has simultaneously enabled critical developer tooling and cemented architectural fragility. Mechanism like sys._getframe() grant direct access to local variables, execution coordinates, and the active call stack. While traditional language design discourages exposing frame states due to performance and safety costs, third-party libraries quickly adopted it: zope.interface manipulated class scopes dynamically, the standard logging and warnings modules skip internal helper frames, and observability platforms like Sentry, Twisted Manhole, and Flask interactive debuggers extract real-time execution state directly from crashing processes. Architectural Gridlock Around Modules, Types, and the GIL (37:35) Because internal data structures were exposed as de facto public APIs, modernizing core execution models has become virtually impossible without breaking external packages. sys.modules operates as a flat global dictionary, locking Python into a single version per library because widespread serialization tools like pickle directly depend on unmodified module paths. Furthermore, primitive types like int exist as static global C structs (PyInt_Type) with mutable internal reference counts, causing memory collisions that prevent running isolated interpreters across multiple OS threads in an embedded process. These exposed reference counting details and shared static structures represent foundational barriers preventing the removal of the Global Interpreter Lock (GIL). Cache Locality and the Small Surface Alternative (46:36) Contrasting Python with JavaScript engines like V8, Ronacher points out that keeping exposed interpreter surface areas minimal prevents developers from depending on engine-specific quirks. In response to whether stripping out Python's specialized shortcuts would degrade execution speed, Ronacher argues that modern CPUs favor cache locality, branch prediction, and compact instruction footprints over sprawled manual C dispatch paths. By simplifying the object model and pruning specialized internal fast paths, an interpreter can occupy less CPU instruction cache and reduce branch mispredictions, potentially delivering faster opcode execution and genuine dynamic flexibility. Ronacher concludes that Python developers must live with the performance and concurrency ceilings imposed by decades of leaky interpreter design, serving as an architectural cautionary tale for newer programming languages to keep their internal virtual machine APIs strictly encapsulated.

精炼阅读摘要

Armin Ronacher 指出,Python 语言的语义与生态深受其 CPython 虚拟机内部实现细节泄漏的塑造;这种设计不仅为开发者赋予了强大的底层内省能力,也带来了不可逆的生态锁定,导致架构重构与消除全局解释器锁(GIL)变得极其困难。 运算符分发的表象与 C 槽位内幕 (5:36) 公开教程通常宣称二元加法 `a + magic` 等同于调用双下划线方法 `a.__add__(magic)` 或 `type(a).__add__(a, b)`,但 Ronacher 指出这是一种脱离虚拟机的简化假设。在 CPython 字节码中,该操作先由 `ceval.c` 中的 `BINARY_ADD` 指令处理,若未命中针对纯整型或纯字符串的快速路径,则转入重达数百行低级 C 代码的 `PyNumber_Add` 流程。底层逻辑依赖于 `PyTypeObject` 内部的结构体函数指针槽位(如 `tp_as_number->nb_add`),并需在数值运算槽位与序列拼接槽位(`tp_as_sequence->sq_concat`)之间判定类型优先级与回退规则。 这种复杂的双轨制源自早期 Python 的历史包袱。在 1995 年左右,C 原生类型与 Python 用户定义类完全割裂,前者直接依赖 C 槽位,后者使用双下划线方法。虽然现代 CPython 构建了同步包装层,但为了维护历史兼容性,这套机制已无法重构为完全基于统一字典查找的纯净对象模型。Ronacher 提及,在某些特殊情况下,直接显式调用底层方法查找反而能绕过槽位解析开销,比运算符语法运行得更快。 内部实现外泄对语言语义的塑形 (17:52) CPython 的底层实现细节直接渗透到了上层 Python 代码中,演变为开发者依赖的语义特性。例如,在 Python 原生类中提取方法会得到普通函数或未绑定方法,但在 C 级内建类型(如 `int`)上提取 `__add__` 则会暴露底层的 `slot_wrapper`。与 JavaScript 中函数概念高度统一不同,Python 暴露了普通函数、方法描述符、内建方法与槽位包装器等多种调用形态,且槽位包装器在类赋值时不遵循描述符的 `self` 自动绑定。许多生产级代码依赖 `conversion_function = int` 这一特性来避免绑定首个实例参数,使替代实现(如 PyPy)不得不全盘复制这些偶然形成的底层怪癖。 历史上的内省缺陷与字符串存储设计进一步印证了这一现象。在 Python 2.7 之前,标准库 `re` 编译出的模式对象在 Python 层访问 `__class__` 或 `__repr__` 会抛出 `AttributeError`,因为其 C 槽位未向 Python 命名空间开放映射,导致无法用纯 Python 正确重写内建的 `repr()`。在字符串方面,Python 为了保持类似字节数组的 $O(1)$ 常数时间随机字符索引保证,在 Python 2 中引入 UCS-2 与 UCS-4 固定宽度存储,导致处理超出 16 位的 Emoji 时面临拆分成代理对或 UCS-4 下大量高位字节填零浪费的困境;即使 Python 3 引入了 PEP 393 灵活字符串,依然背负着这一未经验证即成为事实标准的索引承诺。 高权内省机制的利弊与生态锁定 (32:46) 直接暴露虚拟机状态赋予了 Python 生态独特的工程能力。模块 `sys._getframe()` 允许代码获取执行上下文帧对象,实时检查局部变量、异常状态和调用栈。尽管从纯粹的现代高阶语言设计角度来看这通常被视作应予禁止的后门,但由于 Python 早期即将其开放,生态深度依赖促使 CPython 核心开发者必须对其性能进行持续维护,使其未像 Ruby 的同类机制那样严重拖慢系统。该能力构成了 Sentry 错误追踪、Django 与 Flask 调试页面(如交互式 HTML 回溯执行)以及 Twisted Manhole 运行时附加调试的基石,同时也支撑了标准库 `logging` 和 `warnings` 的跳帧定位功能。 然而,这种底层状态的全面开放导致了严重的生态僵局。全局字典 `sys.modules` 缓存已导入模块,使全系统导入统一实例,但由于 `copy`、`inspect` 乃至深度依赖固定模块映射的 `pickle` 序列化协议将其作为既成事实的公共 API,Python 至今无法在同一进程内实现并存多版本同名模块。Ronacher 强调,诸如 Zope 等持久化对象数据库(ZODB)积累了数十年的序列化记录,直接断绝了通过重构 `sys.modules` 来引入多版本隔离架构的可能性。 静态内存类型对并发与嵌入能力的压制 (39:53) CPython 内部的高性能设计选择严重削弱了其并发与宿主嵌入能力。内建类型(如 `int`、`float`、`str`)在二进制文件中以静态 C 结构体(如 `PyInt_Type`)存在于固定的全局内存地址中,以便虚拟机通过直接比对指针 `obj->ob_type == &PyInt_Type` 来实现加法等操作的极速类型校验,而用户自定义子类则动态分配于堆上。这种依赖全局静态变量且内含可变引用计数的结构,使宿主程序(如 Nginx 工作线程或类似浏览器的多标签架构)无法在单个操作系统进程内的多个线程上并行运行互相隔离的解释器实例。 这也是难以彻底消除全局解释器锁(GIL)的根本障碍之一。GIL 的留存不仅是工程复杂度问题,更因为 C API 全面公开了原始引用计数机制并被无数第三方 C 扩展采用,且类型系统本身无法支持跨线程独立实例化。Python 为应对对象模型过于复杂而手工堆砌了大量针对特定内建类型的特判分支;在现代现代多核与 CPU 缓存架构下,这种臃肿的分支逻辑不仅增加了指令缓存颠簸和分支预测失败的概率,还使 Python 在丧失如 Ruby 般彻底的动态灵活性(如无法对基元类型进行猴子补丁)的同时,在每字节码周期的理论执行效率上也承担着沉重的惩罚。 Armin Ronacher 总结认为,Python 解释器与语言定义的高度绑定既塑造了其独特的实用生产力,也构成了性能与并行演进的长期天花板;对于新一代编程语言设计者而言,应当借鉴 JavaScript 等运行时的封装策略,尽可能将公开暴露的解释器底层 API 压缩至最小,以避免生态对偶然实现细节产生过度依赖。

Detailed notes — zh

## Background 该视频由 YouTube 频道“Видео с мероприятий {speach!”于 2016 年 7 月 13 日发布,内容为著名开源开发者、Flask 框架作者 Armin Ronacher 的技术演讲。原页面未提供额外的文字简介或详细说明。 ## TL;DR Armin Ronacher 在演讲中深入剖析了 CPython 虚拟机繁复的底层实现细节,指出初学者教程中所谓“a + b 纯粹等价于 a.__add__(b)”的说法本质上是一个美好的谎言,解释器内部充斥着 C 槽位(slots)与复杂的硬编码快速路径。他展示了 sys._getframe、sys.modules 等解释器内部机制如何在毫无保留暴露给用户空间的同时,反向塑造了整个语言生态,并直接导致全局解释器锁(GIL)难以移除、多版本库无法并行隔离加载以及进程内多解释器实例嵌入受阻等沉重历史包袱。最后,他反思了过度特化常见路径对现代 CPU 缓存的不利影响,并对比 JavaScript 等语言,强调了极简公开 API 对语言长期演进的重要性。 ## Atmosphere & style 整场演讲呈现出一种既坦诚锐利又带有几分幽默调侃的技术解剖风格。Armin 顶着著名框架作者和拥有 CPython 提交权限的资深开发者光环,却选择以一种好奇“旁观者”的视角来无情戳破语言表层抽象的华丽外衣,言谈间充满了对历史架构包袱的无奈与反思。现场氛围专注且略带震撼,听众随着他一步步剥开解释器深处如迷宫般的 C 源码,直面 Python 光鲜外表下脆弱复杂的底层现实,并在最后的问答交锋中引发了深层次的工程共鸣。 ## Bulletin - [17:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1072s) Armin Ronacher 指出 CPython 将大量底层实现细节无保留地暴露给生态,导致解释器被深度锁死,无法轻易移除 GIL 或支持多版本库共存。 - [05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) 教程中宣称的运算符重载等价于双下划线方法其实是“善意谎言”,CPython 在字节码与 C API 层依靠硬编码槽位(slots)和复杂的特化分支执行,而非动态查找 __add__。 - [32:46](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1966s) sys._getframe 和 sys.modules 的公开赋予了 Sentry 与 Django 极强的实时调试与反射能力,但也导致难以在单进程多线程中嵌入隔离的解释器实例,阻碍了底层现代化。 - [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) 针对性能优化,Armin 认为盲目堆砌 C 特化分支会破坏 CPU 缓存局部性,解释器应通过精简对象模型与代码体积来提升现代硬件上的执行效率。 ## Key takeaways - [02:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=172s) Python 表面上有清晰的概念模型,但底层高度依赖暴露的解释器机制,导致其比 Ruby 更动态、更难进行全局深度优化。 - [02:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=172s) 底层实现的泄漏直接阻碍了 Python 的现代化进程,包括 GIL 的移除、进程内多版本库共存以及多实例隔离运行。 - [05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) 基础运算 a + b 并没有在字典中查找 __add__,而是直接分发至底层 C 结构体的槽位(slots)及专门的整数、字符串快速路径。 - [08:34](https://www.youtube.com/watch?v=qCGofLIzX6g&t=514s) Python 存在两套完全不同的 Slot 概念:语言层用于省内存的 __slots__ 与 CPython 解释器层用于函数指针偏移的 Type Slots。 - [10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) 由于历史上 C 类型与 Python 类的分裂以及数值槽与序列槽的双重存在,几乎没有开发者能完全预判 a + b 的所有底层分支。 - [17:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1072s) Python 将 slot_wrapper 等底层内部对象暴露给用户层,破坏了如 JavaScript 那般“万物皆为统一函数”的一致性模型。 - [23:51](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1431s) 新式类将双下划线特殊方法的解析强制绑定在 Type 上,终结了 Python 2.4 旧式类通过 __getattr__ 实现 100% 透明代理的机制。 - [25:27](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1527s) 为维持对单个字符 O(1) 随机访问的直觉假设,Python 早期坚持采用固定宽度的 UCS 存储而非变长 UTF-8,造成了严重的内存开销与架构束缚。 - [32:46](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1966s) sys._getframe 暴露了调用栈帧的完整内部状态,虽然牺牲了解释器重构自由度,却构成了 Django 调试页、Flask 及 Sentry 异常监控的核心底气。 - [37:35](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2255s) sys.modules 作为扁平全局单例字典暴露给外界,加上 pickle 序列化对此机制的永久固化,使得同一进程内并行加载多版本库的方案彻底破产。 - [39:57](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2397s) 内置类型(如 PyInt_Type)作为 C 全局静态变量存在,使得 CPython 无法在单进程的多线程宿主中原生嵌入互不干扰的独立解释器实例。 - [43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) 引用计数与 C API 的强耦合让现代多线程隔离模型无法落地,同时也逼迫解释器必须硬编码快速路径,反而在部分元编程场景下牺牲了动态性。 - [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) 相比 Python 的深度暴露,现代语言设计(如 JavaScript)应当尽量保持极小的公共暴露 API,避免生态过度耦合底层实现。 - [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) 频繁堆砌特化分支会增加指令体积并引发 CPU 缓存失效,精简整体架构与对象模型反而能在现代硬件体系下获得更优的通用执行效率。 ## Detailed walkthrough ### [00:06](https://www.youtube.com/watch?v=qCGofLIzX6g&t=6s) 演讲主题与 Armin Ronacher 的 Python 背景 演讲者 Armin Ronacher 明确了本次演讲的主题:深入探讨 Python 虚拟机(VM)的内部实现机制,以及这些底层设计如何深刻影响并塑造了 Python 这门编程语言本身。由于解释器底层的很多技术细节可能枯燥繁杂,他开场便鼓励台下听众随时提问互动。谈及对这门语言的感受时,他指出大家都喜欢用 Python,但很少有人意识到解释器内部是多么繁琐细致(nitty-gritty);这种机制既限制了我们能做什么,又在某些方面赋予了我们极大的灵活性。 Armin 随后分享了自己的 Python 经历。他最早接触 Python 是从 2.0 版本开始,真正开始深度写代码则是在 2.2 版本。出于对语言演变与解释器进化的好奇,他曾尝试回溯编译非常古老的版本,甚至在现代计算机上成功编译了 Python 1.3 并获得了可执行文件。这让他发现,Python 解释器中有大量核心机制在过去数十年间几乎从未改变。 作为活跃的开源开发者,Armin 介绍了自己在生态中的角色。他的日常主职工作是在 Sentry(一款主要基于 Django 框架构建的 Python 开源错误收集与监控系统)进行开发;在此之外,他主导开发了 Flask 框架、静态站点内容管理系统 Lektor,以及 Jinja、MarkupSafe、Werkzeug 等大量基础库。尽管他在 Python 核心仓库拥有代码提交权限(Commit access),但他打趣说这“出于某些无法解释的原因”,自己极少向解释器本身贡献代码,而更偏向于以好奇的旁观者视角去研究解释器的各种机制。 - 接触时间线:初识 Python 2.0,真正深度开发始于 Python 2.2,曾成功在现代机器上编译测试 Python 1.3。 - 开源履历:Sentry 核心开发、Flask 框架作者、Lektor 创始人,以及 Jinja、Werkzeug、MarkupSafe 等核心库作者。 - 角色定位:拥有 CPython 提交权限,但主要以应用层框架开发者与探索者的视角审视 CPython 源码。 --- ### [02:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=172s) 动态语言光谱:Python 与 Ruby 的对比及演化困局 编程语言通常分布在从“极度动态”到“极度静态”的光谱两端。Armin 借用 Ruby 来与 Python 进行对比:Ruby 社区常以高度动态自豪,开发者甚至可以直接修改内置 `Float` 类的加法方法,将其行为强行篡改成减法。虽然 Python 不支持直接篡改内置 `float` 的 `__add__`,但 Armin 提出了一个更为激进的观点: > “I actually think that python as a language is way more Dynamic and as a result way harder to optimize than it is to optimize ruy”——Armin Ronacher 在 Armin 看来,Python 实际上比 Ruby 更加动态,因而更难进行深度优化。网络上的通用教程往往给开发者灌输了一套美好的“概念模型”,但一旦深入底层,就会发现语言的真实运转机制根本不是那回事。更棘手的是,大量用户在实际代码中以非常古怪(bizarre)的方式深度依赖了这些解释器内部细节,导致核心团队难以对底层实现做出大刀阔斧的重构。 这些底层历史包袱直接导致了 Python 诸多长期悬而未决的痛点: 1. **全局解释器锁(GIL)难以剥离**:GIL 的移除牵涉到解释器最初的设计保证,导致并发模型难以演化。 2. **多版本隔离加载受限**:无法在同一个进程内方便地同时加载同一个第三方库的不同版本。 3. **解释器进程内隔离困难**:很难在同一个可执行程序中并排运行多个互不干扰、不共享状态的解释器实例。 4. **底层基础类型体系僵化**:无法轻易更改类型运作的基本原则。 5. **Python 3 的历史堆叠**:随着时间推移,越来越多复杂的特性被堆砌在已非常臃肿的底层之上,Armin 认为其中不少新设计甚至缺乏足够的同行评审(peer review)。 --- ### [05:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=336s) 语法抽象与底层现实:加法操作的真相 为了揭示底层细节与教程抽象之间的巨大鸿沟,Armin 举了一个极其简单的示例:定义全局变量 `magic = 42`,随后定义函数 `add_magic(a)` 返回 `a + magic`。许多网络教程和书籍都会宣称,这里的加法在语义上完全等价于调用对象的双下划线方法(即 `a.__add__(magic)`)。但 Armin 指出,这完全是一个巨大的谎言。 在解释器内部,Python 会先将代码编译为字节码,加法对应的操作码指令是 `BINARY_ADD`。解释器的主循环位于庞大的 `ceval.c` 文件中(该文件有数千行之多,本质是一个巨大无比的 `switch-case` 分发循环)。遇到 `BINARY_ADD` 指令时,解释器会从求值栈中弹出两个操作数,紧接着执行一长串充满特殊判断的分支逻辑: ```mermaid flowchart TD accTitle: CPython 加法操作码的分发逻辑 accDescr: 展示 BINARY_ADD 在 CPython 内部如何分流到具体快路径或通用求值函数 subgraph EvalLoop["ceval.c: BINARY_ADD"] A["从栈中弹出操作数 V 与 W"] --> B{"V 和 W 是否均为纯正 int?"} B -- "是" --> C["执行专门的快速整数加法分支"] B -- "否" --> D{"V 和 W 是否均为纯正 str?"} D -- "是" --> E["执行专门的字符串拼接分支"] D -- "否" --> F["调用 PyNumber_Add(V, W)"] end subgraph GenericAdd["PyNumber_Add 逻辑"] F --> G{"检查 nb_add 槽位"} G -- "存在槽位" --> H["调用 binary_op1 执行运算"] G -- "非数值类型" --> I["进入序列拼接特殊处理分支"] end ``` 如果两个操作数不是纯正的原生类型(不包括子类),解释器便会调用 C API 中的 `PyNumber_Add`。Armin 吐槽该函数虽然名字叫“Number”,但实际上完全是个坏名字(bad name),因为它内部还包含了序列(Sequence)拼接的特殊处理,实质上相当于 `PyObject_Add`。最关键的是,在这一长串 C 源码中,根本找不到对属性名 `"__add__"` 的字典查找过程,而是直接调用了 C 结构体上的槽位函数(slot)。 - 专用快速路径:CPython 会在字节码层面对纯整数(exact integer)和纯字符串(exact string)进行类型短路优化,避开通用分发。 - 源码膨胀:仅仅是把两个数字加在一起这一项操作,穿透整个解释器链路所触及的 C 源码就长达约 400 行。 --- ### [08:34](https://www.youtube.com/watch?v=qCGofLIzX6g&t=514s) 揭秘“槽位(Slots)”与被掩盖的真实机制 在讨论到加法调用的具体实现时,Armin 向现场观众提问究竟什么是 Python 中的“Slot”。他指出,在 Python 语境下其实存在两套完全不同的“Slot”概念: | 槽位类型 | 所在层面 | 作用机制 | 核心目的 | |:--- |:--- |:--- |:--- | | **`__slots__`** | Python 语言层 | 在类定义中声明属性列表,阻止为每个实例生成 `__dict__` | 节省内存空间,加快属性访问 | | **C 结构体槽位(Type Slots)** | CPython 解释器层 | 定义在 `PyTypeObject` 结构体内部的特定函数指针成员(如 `tp_as_number->nb_add`) | 避免运行时的字典查找,直接通过指针偏移调用 C 级操作 | 在 C 语言层面,每个 Python 对象都有一个 `ob_type` 指针指向其类型对象(Type Object)。类型对象内部维护着庞大的结构体,其中包含各种协议簇指针(例如处理数值操作的 `tp_as_number`)。当执行加法时,解释器检查的是结构体偏移中的 `nb_add` 函数指针,而不是从对象或类型的字典里查找字符串 `"__add__"`。 解释器会根据参与运算的两个对象的类型关系,调用对应的槽位函数。整个过程涉及约 400 行 C 语言逻辑,但没有任何地方涉及到属性字符串查找。Armin 直言: > “Where is `__add__`? It doesn't appear anywhere, sort of that's the big lie, the big lie is that we we invoking this special things, but we're not; instead we're looking at this stupid slots things.”——Armin Ronacher 这种通过 C 结构体硬编码槽位来加速操作的底层实现,虽然兼顾了性能,却彻底打破了“所有运算符重载都等价于在字典中查找对应双下划线方法”的浅层抽象模型,构成了 Python 内部实现向外渗透(leaky internals)的经典例证。 ### [10:39](https://www.youtube.com/watch?v=qCGofLIzX6g&t=639s) a + b 背后不可捉摸的语义与历史分裂 尝试从 Python 解释器的角度推导运行时行为是一件令人惊讶且沮丧的事情。按理说,对于对象 $a$ 和对象 $b$,当对其执行加法、减法或乘法时,理应有一套极其明确且标准化的规则——就像 JavaScript 近年来通过明确规范做到的那样,标准巨细靡遗地规定了两个对象相加的具体结果。然而在 Python(尤其是 Python 2)中,想要准确预知 `a + b` 会发生什么几乎是不可能的,它不仅取决于对象上的底层标志位(flags),还取决于两者之间错综复杂的类型层级关系。Armin Ronacher 对此直言不讳:“I would argue that there is probably not a single programmer in in the python Community who could tell you exactly what happens if you add two objects together um and I I kind of assume this from the fact that there's the pipie project and the pipie project tries to implement the Python language in Python um and it's kind of interesting sometimes when pipy disagrees with the implementation that cpython has and you would think that they must get it right but then they still disagree in in certain ways.” 这一切复杂性的根源在于 CPython 历史上的“槽”(slots)机制。所谓 slot,本质上是解释器为每个类型维护的 C 结构体(`struct`)中的一个函数指针槽位。早在 1995 年左右,Python 世界割裂为两套互不相干的体系:一套是基于 C 语言实现的内置类型(C types),另一套则是后来引入的 Python 类(Python classes)。如果将两个用 C 编写的内置整数相加,解释器会调用专门的 C 槽方法 `nb_add`;而当语言加入了 Python 类并希望支持自定义加法时,设计者引入了特殊的回调方法 `__add__`。 为了弥合两者的鸿沟,解释器引入了一套同步机制:当 C 类定义了 slot 时,C 底层逻辑会通过某些机制将该 slot 包装并作为特殊方法暴露给 Python 层;反过来,当 Python 类定义了 `__add__` 时,解释器会获取该函数并将其塞入底层 C 类型的相应 slot 中。这种试图让 `foo.__add__` 与底层 C 类型的 `tp_as_number->nb_add` 始终保持同步的设想,由于早期架构决策而变得异常脆弱。早在 Python 诞生初期,Guido van Rossum 将相加操作分化为两大独立概念:数值相加(numbers)与序列拼接(sequences)。因此解释器内部同时存在数值槽(number slots)和序列槽(sequence slots),加法操作符 `+` 必须同时兼顾这两者,导致底层状态极易出现不同步。 ```mermaid flowchart TD accTitle: Python 中 a + b 的解释器求值决策流 accDescr: 展示 Python 解释器在执行 a + b 时经历的多重分支与槽位查找逻辑 Start["执行表达式 a + b"] --> FastInt{"a 与 b 是否均为整数?"} subgraph FastPath ["快速路径"] FastInt -- "是" --> DoIntAdd["执行底层整数快速加法"] FastInt -- "否" --> FastStr{"a 与 b 是否均为字符串?"} FastStr -- "是" --> DoStrConcat["执行底层字符串快速拼接"] end subgraph NumberSlots ["数值槽路径 (nb_add)"] FastStr -- "否" --> CheckNum{"a 或 b 是否实现数值槽?"} CheckNum -- "是" --> ResolveTypeRel["根据复杂类型关系解析优先级"] ResolveTypeRel --> InvokeNumSlot["调用数值槽 nb_add 解析结果"] end subgraph SequenceSlots ["序列槽路径 (sq_concat)"] CheckNum -- "否" --> CheckSeq{"a 是否实现序列槽?"} InvokeNumSlot -- "未实现 / 失败" --> CheckSeq CheckSeq -- "是" --> InvokeSeqConcat["调用序列槽 sq_concat 拼接"] CheckSeq -- "否" --> TypeError["抛出 TypeError 异常"] end ``` ### [15:00](https://www.youtube.com/watch?v=qCGofLIzX6g&t=900s) 教程谎言与解释器内复杂的快速路径 市面上绝大多数 Python 教程对运算符重载的解释完全是一种“善意的谎言”。入门教程通常写道:执行 `a + b` 等价于调用 `a.__add__(b)`;稍微严谨一点的教程会纠正为调用 `type(a).__add__(a, b)`。然而这两种说法在 CPython 实现面前都是错误的。当在解释器中写下 `a + b` 时,底层经历了一套极度复杂的快速路径判定逻辑: - 首先检查 $a$ 和 $b$ 是否都是整数,若是,则直接进入解释器内部专设的整数快速加法通道; - 接着检查两者是否都是字符串,若是,则走专用的字符串快速拼接通道; - 随后尝试走数值加法:检查 $a$ 是否实现了数值槽并解析其 slot,再检查 $b$ 是否实现数值槽;根据两者之间极其复杂的继承关系决定调用顺序,通常最终解析为由 $a$ 的槽函数接收 $a$ 和 $b$; - 若数值槽尝试失败,解释器才会退回到另一套独立的执行路径,尝试进行序列拼接(sequence concatenation)。 相比之下,直接在 Python 中写 `a.__add__(b)` 的查找逻辑反而极其简单直接:仅仅是在类型对象上查找名为 `__add__` 的属性并将其作为普通方法调用。令人啼笑皆非的是,解释器为了追求极限性能而在底层堆砌的 slot 快速路径,其判定与调度逻辑远比暴露给用户的上层模型复杂得多。在某些边界情况下,手动编写 `a.__add__(b)` 的执行速度甚至反而比操作符 `a + b` 更快。 面对如此庞杂的包袱,人们很自然会产生一个疑问:为什么不直接重构解释器,把 `a + b` 在底层彻底规范化为直接调用 `a.__add__(b)` 并对其进行统一优化?Armin 强调,之所以无法推翻重来,根本原因在于 CPython 历史上一贯的做法是将底层内部实现毫无保留地泄露给了整个周边生态系统:“And the reason for this is that unfortunately we expose so much of the language to the ecosystem around it that we can't change The Interpreter anymore.” ### [17:52](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1072s) C API 的泄露与万物皆非统一函数的类型泥潭 任何编写过 Python C 扩展的人,都深知 Python C API 的抽象层级有多么底层。每一个自定义的 C 类型都必须直接了解并填充底层结构体的 slots。这种内部细节不仅暴露在 C 层,甚至被原封不动地暴露给了 Python 运行时的普通用户。哪怕是从未接触过底层原理的新手,也经常在日常报错中撞见诸如 `'slot_wrapper' object has no attribute '...'` 这样充斥着内部黑话的异常信息。 这种底层泄漏导致了高层语义的严重不一致。若在 Python 中定义一个继承自 `object` 的普通类并赋予其 `__add__` 方法,访问该类的 `__add__` 得到的是一个普通的未绑定方法(Unbound method,底层为函数或 lambda);但如果访问 C 内置类型的 `int.__add__`,拿到的却是一个名为 `slot_wrapper` 的底层特殊对象。`slot_wrapper` 本质上是在 C 解释器层面包裹 C 函数的代理,却赤裸裸地暴露在用户层。这与 JavaScript 形成了鲜明的对比——在 JavaScript 中“万物皆为函数”(everything is a function),无论是用 JS 写的还是 C++ 底层实现的,在外部视角下完全一致。而 Python 内部却分裂出一大批拥有微妙行为差异的调用物类型: - 普通函数(functions) - 内置函数(built-in functions) - 槽包装器(slot wrappers) - 方法描述符(method descriptors) - 各类可调用对象(callable objects) 这些细微的实现差异最终演化成了生态中被广泛依赖的隐式契约。例如,如果把一个普通类的未绑定方法 `__add__` 提取出来赋值给另一个类作为属性,它会扮演描述符(descriptor)的角色,在调用时自动绑定实例并将 `self` 作为第一个参数传入;但如果将一个 C 类型的 `slot_wrapper` 做同样的操作,它却不会表现出这种自动绑定行为。许多开发者甚至写过形如 `conversion_function = int` 这样的类属性代码,其运行完全依赖于 `int` 作为 C 类型不会被错误当作方法描述符去绑定 `self`。这种隐形依赖形成了一道死锁:解释器核心开发者永远无法重构和清理这些怪异行为,因为现有的成千上万行代码正依赖着这些泄漏出来的缝隙存活;而像 PyPy 这样的替代编译器项目,为了实现 100% 的兼容性,也不得不原样复制 CPython 这套充满历史包袱的底层怪癖。 ### [21:45](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1305s) C 扩展与解释器内部行为的穿透泄漏 Armin Ronacher 强调,开发者必须深入理解这些底层细节,原因在于“它非常复杂,而且每个人都在使用它,这使得性能优化几乎变得不可能”(“it's very complex and everybody uses it and it makes optimizations almost impossible”)。当将 Python 代码移植到其他语言运行时,必须对这些行为进行模拟;更严重的是,这些实现细节直接塑造了 Python 自身代码的行为模式。他举了一个虽然在新版中已被修复、但极具代表性的历史怪异问题:标准库中的 `re`(正则表达式)模块。在旧版 Python 中编译正则表达式后,返回的对象类型形如 `SRE_Pattern`。令人震惊的是,如果尝试访问该对象的 `__class__` 属性,Python 会直接抛出 `AttributeError`;同时,它也没有 `__repr__` 属性。在常规认知中,Python 里的任何对象都应该拥有类属性,这种完全不响应特殊属性的行为显得极为不可思议。 出现这种现象的根源在于,该对象完全是由 C 语言编写的底层扩展。编写该 C 对象的开发者定制了它暴露给 Python 层的全部外观行为,导致 C 解释器眼中的对象结构与 Python 代码所能看到的内容完全脱节。例如,如果尝试用纯 Python 编写内置的 `repr()` 函数逻辑去调用目标对象的 `__repr__`,对该对象将永远失效,因为其类型信息和表示方法只能在 C 语言层面上读取,Python 脚本层根本无法触及。这生动地证明了 Python 的 C-API 机制允许底层实现一套对上层解释层完全不可见或严重失真的定制行为。 - 在 Python 2.7 之前的旧版本中,`re.compile()` 返回的内部正则对象缺少 `__class__` 和 `__repr__` 等基本特殊属性。 - 解释器 C 层面与 Python 脚本层面的对象模型存在割裂,导致部分高级反射机制在 C 扩展对象上彻底失灵。 --- ### [23:51](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1431s) 旧式类、代理转发与对象模型的历史包袱 Armin 接着展示了 Python 2.4 时代的旧式类(Old-style Classes)行为。在那个年代,如果定义一个不继承自 `object` 的类,并在其中实现 `__getattr__` 来代理转发另一个底层对象,所有的特殊双下划线方法(Dunder Methods)都会被平滑透传。比如实例化类 `A` 并让其代理整数 `42`,当解释器尝试寻找该实例的 `__repr__` 时,会直接触发 `__getattr__` 并在内部的整数 `42` 上解析该方法,从而让实例完美呈现为 `42` 的形态,实现 100% 的透明代理。 然而,现代 Python 采用的新式类(New-style Classes)打破了这种设计,不再通过实例字典或 `__getattr__` 查找特殊双下划线方法,而是强制直接从对象的类(Type)上查找。这意味着旧式类为了保持对古老版本的兼容,必须依赖专门的 `instance` 内部类型来进行繁琐复杂的属性与调用转发。Armin 指出,过去在教学中向初学者传授的“所有属性访问都会走到 `__getattr__`”这一逻辑,在早期确实是真实的底层实现,但随着类型系统的演进,这套机制演变成了充斥着各种妥协与不一致的沉重代理负担。 ```mermaid flowchart TD accTitle: Python 属性查找机制历史演变 accDescr: 对比 Python 旧式类与新式类在调用双下划线特殊方法时查找路径的区别。 subgraph OldStyle["Python 2.4 旧式类 (透明代理)"] A1["调用 repr(obj)"] --> B1["查询 obj.__getattr__('__repr__')"] B1 --> C1["转发至底层对象 (如 int 42)"] C1 --> D1["成功执行代理对象的 __repr__"] end subgraph NewStyle["现代 Python 新式类"] A2["调用 repr(obj)"] --> B2["直接查找 type(obj).__repr__"] B2 --> C2{"类上是否存在实现?"} C2 -- "是" --> D2["直接执行"] C2 -- "否" --> E2["抛出 AttributeError (绕过 __getattr__)"] end ``` --- ### [25:27](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1527s) 字符串与 Unicode 的演变:固定宽度引申的架构陷阱 话题转入更加直观却同样充满内幕的 Unicode 实现。Armin 解释道,理解这一机制必须回顾 Python 诞生之初的背景:早期的 Python 根本没有 Unicode 概念,字符串就是纯粹的字节序列,每个字符的数值范围落在 0 到 256 之间(即单字节)。这种物理布局催生了 Python 程序员极度依赖的一种直觉假设:访问字符串的第 1、第 2、第 3 个字符是绝对廉价且瞬时的 $O(1)$ 随机索引操作。 当 Unicode 逐渐普及并被引入 Python 时,开发团队为了维持这种“可以直接按下标取字符”的心智模型,做出了一个深远且代价高昂的决定:简单地把固定宽度数组从 0-256 扩大到 0-65,535(两字节的 UCS-2 编码)。随着人类语言文字的不断扩充,两字节迅速饱和,解释器后来又加入了支持更大数字的编译选项。尽管这种设计在直觉上很便利,但从工程角度来看却埋下了巨大的遗憾。 - 早期 Python 字符串本质为单字节数组,随机下标访问具备先天的硬件级性能优势。 - 引入 Unicode 后,为了保持对单个字符的常数级下标访问,Python 选择了固定宽度的内部表示,而不是变长流式布局。 --- ### [26:43](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1603s) UCS-2、UCS-4 与 UTF-8 的标准之争及内存膨胀 Armin 回顾了大约 25 年前字符编码标准演进的戏剧性历史。当时有两个独立的组织在尝试解决 ASCII 的全球化扩充问题:Unicode 联盟最初天真地认为“65,000 个字符对全世界所有人都足够了”(“65,000 characters is enough for everyone... they were wrong”);而 ISO 委员会则主张搞一套 $2^{32}$ 容量的超大标准,但因其过于庞大且缺乏高效的物理编码方式而未被广泛采纳。随着 Emoji 表情符号的爆炸式普及,字符序号迅速突破 65,535,至少需要 3 个字节以上才能表示。 与此相对,业界逐渐意识到 UTF-8 这种变长编码的优越性:绝大多数文本(ASCII 或西里尔字母等低码位文本)只需要 1 到 2 个字节,能够极大地压缩体积并避免内存中充满大量的填充零。而如果 Python 2 解释器在编译时选用了基于固定 2 字节的 UCS-2,当文本中包含 Emoji 时,一个单独的 Emoji 字符会被硬生生切碎成两半(作为代理对存在),必须通过访问两个连续字符才能拼出一个完整表情;开发者若要解决这个问题,就必须在编译解释器时开启 UCS-4 选项,使得字符串中的**每一个字符**都强行占用 4 个字节。Armin 直言这构成了极其惊人的内存浪费,因为即便是 Unicode 的终极上限也只需要 21 位(刚刚超过 2 个字节半),分配 4 个字节意味着绝大多数内存位都在白白闲置。 | 编码方案 | 单字符内存占用 | 访问特定字符的时间复杂度 | 对 Emoji 的处理缺陷 | 空间利用率 | |:--- |:--- |:--- |:--- |:--- | | **UTF-8** | 1 至 4 字节(变长) | $O(N)$(必须线性扫描前置字节) | 原生单码点支持,无截断 | 极高(无冗余零字节) | | **UCS-2** | 固定 2 字节(16 位) | $O(1)$(直接按偏移量跳转) | 字符被拆成两半(代理对溢出) | 仅在 BMP 范围内较高 | | **UCS-4** | 固定 4 字节(32 位) | $O(1)$(直接按偏移量跳转) | 支持完整码位,无拆分 | 极低(Unicode 上限仅 21 位,大量浪费) | --- ### [30:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1803s) 抽象泄漏对现代语言设计的启示 面对为何 Python 宁愿承受巨大的内存浪费也不直接将内部存储切换为 UTF-8 的质问,Armin 给出了关键答案:Python 历史性地向开发者做出了“访问索引 1 就能精确瞬时拿到第 1 个字符”的语义承诺。如果内部采用 UTF-8,要定位第 $N$ 个字符就必须从头进行全量字节扫描;而采用固定宽度的 UCS-2 或 UCS-4,解释器仅需简单的乘法偏移即可跳至目标位置。虽然 UTF-8 在近十年已无可争议地成为全行业的事实标准(例如 Go 语言全盘拥抱 UTF-8 且原生不提供直接的字符随机索引,Rust 与 Swift 亦在此类设计上保持克制,避免误导开发者以为随机索引字符是无成本的),但 Python 却被早期的内部假设紧紧绑架。 尽管 Python 3 通过动态自适应内部表示(根据字符串中最大字符动态选用 1、2 或 4 字节)在一定程度上缓解了内存问题,但它依然死守着“固定索引”的抽象承诺。Armin 评价道:“我们所使用的语言,几乎完完全全就是那个现存的解释器本身”(“the language that we use is pretty much exactly The Interpreter as it exists”)。这种解释器底层逻辑向上层语法的直接穿透,一方面极大地阻碍了全局性能优化,在底层与上层脱节时会表现得极其诡异;但从另一个角度来看,这也塑造了 Python 程序员独特的思维方式。相比于 JavaScript 开发者习惯被局限在高度沙盒化、高度抽象且只能被动接受限制的浏览器运行时环境中,Python 开发者能够透过语法直接触及运行时的裸机机制,形成了完全不同的技术世界观。 ### [032:46] `sys._getframe`:暴露执行状态的双刃剑 在 Python 中,解释器的内部实现一直毫不遮掩地暴露给用户空间,这种深度穿透塑造了许多在其他编程语言中极为罕见的特性。Armin Ronacher 直言,尽管完全暴露的解释器让社区几乎无法彻底摆脱 GIL(全局解释器锁),也阻碍了大量激进的编译器优化,使 Python 始终受困于特定运行机制,但这也赋予了开发者极具威力的控制权。他在现场调查有多少人使用过或听说过 `sys._getframe()` 时,举手的人寥寥无几。Armin 表示,这个函数是让许多 Python 代码变得“充满刺激(spicy)但未必算得上优秀”的秘密原料。 任何时刻调用 `sys._getframe()`,解释器都会奉上当前调用栈的帧对象(Frame Object),直接暴露当时近乎完整的解释器内部状态。从纯粹的高级语言设计与性能考量出发,这种设计堪称疯狂:“如果去重新设计一门高级语言,所有人都会主张‘千万别提供这玩意,它太慢了、太荒谬了,根本不应该存在’。”然而 Python 在极早期就暴露了栈帧,并且大量现有代码深度依赖它,迫使 CPython 核心开发团队必须设法保证其运行性能。相比之下,Ruby 等语言虽然也有类似机制,但因极少有人使用而慢得无法直视。 通过帧对象,开发者可以随心所欲地读取局部变量、在某些场景下篡改局部变量、检查当前执行的文件名,以及读取当前帧正在处理的异常。这种机制催生了大量既精巧又带有“滥用”色彩的设计: - **Zope Interface**:其 `implements` 方法在被调用时,会动态向上探测一层调用栈,直接要求上一层的类对象注入新属性,在纯粹的函数调用语义层面完全不合常理。 - **warnings 模块**:能够根据传入的参数,将警告信息锚定到往上数若干层的调用栈位置,而不是硬编码在函数自身内部。 - **inspect 模块**:深度依赖该特性来反射和内省运行时代码结构。 - **logging 模块**:在记录日志时主动跳过日志库自身的内部调用帧,保证最终记录的文件名和行号精准指向业务调用的位置。 ```mermaid flowchart TD accTitle: sys._getframe Call Stack Inspection accDescr: Diagram showing how sys._getframe traverses and exposes caller frame internals. subgraph UserSpace["用户调用层"] A["业务代码 / 调用点<br/>(Local Vars, Exception Context)"] B["中间库 / 框架层<br/>(warnings / logging / zope.interface)"] end subgraph FrameState["解释器运行时"] F1["Frame 0: logging.info()"] F2["Frame 1: Caller Frame<br/>(文件名、行号、局部作用域)"] F3["Frame 2: Root Caller"] end A -->|"调用"| B B -->|"sys._getframe(1)"| F2 F2 -.->|"读取并剥离内部帧"| B ``` ### [035:54] 实时调试生态与崩溃追踪系统的底气 这种直通解释器底层的机制,孕育了极具特色的调试与排错生态。Armin 以近乎被遗忘的 `twisted.manhole` 为例:在工业界和许多公司内部,常有常驻的游戏服务器或后台进程在模拟世界中运行,当出现诡异行为却又不能停机重启时,工程师可以直接挂载一个调试器连入正在运行的 Python 进程,在 live 解释器中直接执行任意 Python 代码,检视所有运行状态。 作为 Sentry 的核心成员,Armin 指出这种灵活性是 Sentry 等崩溃监控平台的核心支柱。在常规语言中,发生崩溃时往往只能拿到浅层的错误堆栈;而在 Python 运行时中,Sentry 不仅能提取每一层调用栈的文件与行号,还能原汁原味地提取出崩溃发生瞬间每一帧内的所有局部变量数值,乃至精准的时间开销。对于普通开发者而言,Django 报错时呈现的华丽调试页,或者 Flask 崩溃时直接在浏览器 HTML 页面中内嵌交互式终端供人执行代码排查,其底层全都仰仗于 Python 对解释器运行状态的无保留暴露。 > “这种灵活性在绝大多数其他语言中根本不存在。作为 Python 程序员,我们对此习以为常,因为即便你自己不写这类代码,当你写一个 Django 应用而它崩溃时,Django 调试页弹出来,并打印出当时存在的所有状态……如果没有暴露这些底层,这根本不可能实现。” —— Armin Ronacher ### [037:35] `sys.modules` 的全局单例锁定与多版本共存之困 解释器暴露的另一大核心状态是 `sys.modules` 字典。在 Python 中,模块一旦被加载就会被缓存在 `sys.modules` 中,这确保了应用中无论哪一部分再次 `import`(比如多个地方导入 `cgi` 模块),拿到的始终是完全相同的模块单例。然而,这个原本用于缓存的内部字典由于完全公开,成为了所有 Python 开发者默认依赖的事实标准: - 许多代码会先执行 `__import__(module_name)`,随后直接从 `sys.modules[module_name]` 中取出所需对象。 - 类反射、对象复制(如 `copy` 机制)以及动态加载广泛依赖它来反查类归属。 - **pickle 序列化**:深度依赖 `sys.modules` 来定位类并反序列化恢复对象。 这种全透明暴露带来的沉重代价是**多版本库共存的彻底破产**。长久以来社区多次尝试在同一个 Python 解释器进程中并行加载同一类库的不同版本,但由于整个生态都认定 `sys.modules` 是以扁平的模块名作为键,如果要支持版本隔离,就必须为字典引入版本标识符、命名空间作用域或多层间接寻址,这会直接击碎所有现存代码。 Armin 强调,这个设计包袱因 pickle 而被永久锁死:“因为每个人都依赖这个,尤其是 pickle,而且你必须明白,当人们把对象 pickle 序列化存盘时,是打算永远存下去的;Zope 的整个数据库完全建立在 pickle 对象之上。如果现在修改 pickle,所有历史上生成的 pickle 数据都会彻底损坏。所以在 pickle 彻底消亡之前,我们永远别想拥有带版本控制的 `sys.modules`,这令人沮丧。” ### [039:57] 类型与类的底层分歧:为何 Python 难以原生嵌入? 在分析为什么移除 GIL 极其困难时,Armin 抛出了一个深层疑问:在 Python 的底层机制中,为什么内置的 `int` 是一个“类型(type)”,而用户自定义的 `int` 子类却是一个“类(class)”?(虽然在 Python 3 中表面上的打印输出有所统一,但底层的内存与查找模型依旧沿用原机制)。核心差异在于内存布局: 1. **内置类型(Type)**:拥有严格固定的内存地址,通常以 C 语言全局结构体变量(例如 `PyInt_Type`)的形式静态常驻在内存中。 2. **用户类(Class)**:是在运行时动态分配并保存在堆内存(Heap)上的普通对象。 当解释器执行两个数字相加等极高频操作时,为了避免昂贵的通用方法分发,底层必须以极快速度判定操作数是否为纯粹的整数。CPython 的做法极其直接:检查对象的类型指针是否精确等于静态全局变量 `&PyInt_Type` 的内存地址(单纯的指针比较)。 然而,这一优化引发了灾难性的连锁反应,使得 Python 几乎无法像其他现代引擎那样干净地嵌入到多线程宿主中: | 运行时特性 | JavaScript 引擎 (如浏览器 V8) | CPython 解释器 | |:--- |:--- |:--- | | **隔离模型** | 每个 Tab 独立启动完全隔离的引擎实例 | 全局类型(如 `PyInt_Type`)以 C 全局变量形式存在 | | **多线程嵌入** | 宿主(如 Nginx、Browser)可分配多线程独立并发运行 | 无法在单进程内运行多个互不干扰的解释器 | | **对象修改风险** | 内部类型彼此状态隔离,无全局污染 | 静态类型的引用计数、属性补丁跨实例全局共享 | 如果像 Nginx 这样的高性能服务器希望将 Python 作为脚本语言,在四五个独立线程中分别启动一个完全独立的解释器实例,这在 CPython 中会直接崩溃或相互污染。因为所有解释器实例都会强行共享同一份带引用计数、可能被 monkey-patch 修改的全局 `PyTypeObject`。Armin 感慨道,这就是 Python 很难被真正嵌入到大型多线程系统中的首要病灶;他曾亲自尝试修改所有内置类型、引入一层间接寻址以实现线程级隔离的独立解释器,但整个解释器内部千丝万缕的紧密耦合让这项工程步履维艰。 ### [43:19](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2599s) 内部实现泄露的深远代价:GIL、多版本支持与性能天花板 Armin Ronacher 深入剖析了当解释器内部细节被暴露给外界后所引发的一连串无法挽回的后果。一旦开发者开始依赖这些底层机制,整个生态就掉入了一个“什么都无法再修改”的深不见底的兔子洞。最显著的代价就是无法移除全局解释器锁(GIL):在 CPython 中,全局仅有一套基础整型等静态类型定义,并在所有地方共享;这使得我们无法简单地在单个进程中让一个线程运行一个独立的解释器实例、另一个线程运行另一个互不干扰的解释器。更致命的是,解释器底层的引用计数(reference counting)等内存管理细节被直接暴露在每一个 C 扩展模块中,这种紧密耦合已经彻底烙印进语言生态,不破坏向后兼容就根本无法将其剥离。 与此同时,这种泄露还直接扼杀了更现代的语言特性演进。正如之前提到的,由于 `sys.modules` 作为一个完全暴露的解释器实现细节被广泛滥用,导致 Python 至今都极难实现多版本库共存(multi-version libraries)。整个解释器的复杂设计一路向上渗透,直接照射并影响到了 Python 语言的最顶层抽象。Armin 坦言,他现在完全理解 Python 现状的成因——这就是历史包袱,开发者不得不与这一段演进历史共存,哪怕是伤筋动骨的 Python 3 迁移也未能力挽狂澜。然而,对于正在设计或学习其他语言的人来说,绝不应该再重蹈覆辙。尽管这些设计对解释器执行速度和高并发能力构成了沉重的压制,形成了难以突破的技术天花板,但在当前绝大多数日常开发工作中,它尚不至于完全阻碍人们用 Python 把活干完。 在并行化和动态特性方面,Python 同样陷入了尴尬的夹缝境地。由于基础类型在全局共享,无法在多实例间隔离,使得并发改造寸步难行;而为了在如此繁复的内部机制下维持基本的运行速度,解释器又不得不在许多路径上“抄近道”,例如牺牲掉部分极度动态的元编程能力。相比之下,Ruby 可以自由地在运行时将加法操作动态重定义为减法,因为 Ruby 没有为整型加法硬编码特化的快速路径(fast path),而 Python 处于性能考量却不得不硬编码这些优化,从而在动态性上也失去了一定纯粹度。讲到这里,Armin 略带歉意地表示自己可能有些语无伦次,因为他意识到要用通俗易懂的方式向听众讲清解释器底层机制及其对生态的连锁反应确实相当困难,因此他建议听众不必拘泥于演讲主题,可以在接下来的提问环节自由发问。 - **无法摆脱 GIL**:共享的基础类型(如单一整型定义)与各 C 扩展中深度依赖的底层引用计数,导致现代线程隔离模型无法落地。 - **缺失多版本共存能力**:`sys.modules` 的透明暴露使得模块导入隔离和并行版本管理变得极其脆弱。 - **并发与性能天花板**:内部细节向高层渗透,锁死了运行时进行根本性优化的可能性。 ```mermaid flowchart TD accTitle: 暴露内部实现对 Python 生态的连锁约束 accDescr: 展示 CPython 暴露底层细节如何导致引用计数渗透、静态类型共享,并最终形成无法移除 GIL 的锁死状态。 subgraph Leakage["泄露的底层细节"] L1["sys.modules 暴露"] L2["共享底层类型系统 (如单个全局整型)"] L3["引用计数与 C 扩展 API 深度绑定"] end subgraph Consequences["架构与生态后果"] C1["难以支持多版本库共存"] C2["多线程解释器实例隔离失败"] C3["无法移除全局解释器锁 (GIL)"] C4["为维持性能硬编码快速路径,牺牲纯粹动态性"] end L1 --> C1 L2 --> C2 L3 --> C3 L2 --> C3 C2 --> C3 L2 --> C4 ``` --- ### [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) 问答交流:极小化公开 API 与字典特化优化的迷思 演讲结束后,一位现场听众提问并归纳核心观点:演讲的终极主旨是否在于“语言应当暴露出尽可能小的公共 API”?因为一旦把内部实现暴露在外且被外部大量依赖,后续就再也无法收回。Armin Ronacher 对此深表赞同,并以 JavaScript 为例进行了横向对比:JavaScript 在许多层面上做得非常出色,其暴露给宿主环境和开发者的底层解释器 API 极小甚至几乎不存在,因此开发者根本没有机会去依赖或滥用解释器的内部机制;而 Python 恰恰走了一条相反的路,暴露了庞大的底层细节,如今积重难返。Armin Ronacher 强调说:“因此,在很多方面,语言设计的正确做法就像 JavaScript 那样,保持公开暴露的解释器 API 非常小,甚至完全不存在,因为这样人们就绝不会陷入开始滥用它的境地。” 随后,该听众进一步追问特化分支(special cases)的问题:既然像整型加法特化或字典(dict)内部那样存在大量的特殊处理分支,若不采用这些特化逻辑,运行速度就会变得非常慢,那么到底能有什么替代方案?对此,Armin 提出了颠覆直觉的架构思考。他指出,CPython 为了性能不断塞入特殊分支的处理方式在现代硬件体系下走入了误区。现代计算机之所以能高效运行代码,核心依赖于 CPU 缓存友好性(cache locality)、极小的指令内存占用(footprint)以及对相同内存区域的循环复用,从而避免缓存失效(cache invalidation)。 在 Python 运行时中,解释器在字典查找和操作上耗费的时间往往超过了解释器主循环的其他所有部分。如果解释器能够大幅精简单个 Python 对象的数据结构与复杂度,不堆叠大量杂乱的分支逻辑,就能显著压缩高频热点代码(hot code paths)的体积。更少且更纯粹的代码更容易被 CPU 完整缓存在 L1/L2 Cache 中,分支预测成功的几率也大大提升。Armin 尖锐地指出了当下的讽刺现状:“我认为这里的讽刺之处在于,我们比 Ruby 缺乏动态性,但由于这一切,我们在每个操作码(op code)上的执行速度可能比 Ruby 还要慢。” 他认为,过去大家看到运行缓慢就盲目为常见情况(common cases)追加硬编码特化,但如果当初能坚持精简整体架构,反而能让不常见的通用情况获得大幅提速,同时兼顾更高的动态灵活性与硬件亲和力。提问者由衷感谢这一极具洞察力的解答,全场随之响起热烈掌声,演讲圆满结束。 - **API 暴露原则**:现代语言(如 JavaScript)通过隐藏解释器内部细节,避免了生态对底层实现的过度耦合,从而获得了极大的演进自由度。 - **缓存与现代硬件认知**:快速执行的核心不在于堆砌 C 语言层面的特殊条件分支,而在于降低指令占用、提升缓存局部性与分支预测成功率。 - **性能架构反思**:过度特化常见路径导致代码膨胀和缓存穿透,反而让整体操作码执行速度落后于设计更精简、更动态的解释器系统。 ## Notable quotes > [46:36](https://www.youtube.com/watch?v=qCGofLIzX6g&t=2796s) 在很多方面,语言设计的正确做法就像 JavaScript 那样,保持公开暴露的解释器 API 非常小,甚至完全不存在,因为这样人们就绝不会陷入开始滥用它的境地。 — Armin Ronacher > [30:03](https://www.youtube.com/watch?v=qCGofLIzX6g&t=1803s) 我们所使用的语言,几乎完完全全就是那个现存的解释器本身 — Armin Ronacher ## Mentioned Armin Ronacher, Python, CPython, PyPy, Ruby, JavaScript, Go, Rust, Swift, Flask, Django, Sentry, Lektor, Jinja, Werkzeug, MarkupSafe, Zope, twisted.manhole, Guido van Rossum, Unicode 联盟, ISO [Source](https://www.youtube.com/watch?v=qCGofLIzX6g)

Source quotes

[46:36] Armin Ronacher: 在很多方面,语言设计的正确做法就像 JavaScript 那样,保持公开暴露的解释器 API 非常小,甚至完全不存在,因为这样人们就绝不会陷入开始滥用它的境地。 [30:03] Armin Ronacher: 我们所使用的语言,几乎完完全全就是那个现存的解释器本身

Source quotes

[10:39] Armin Ronacher: I would argue that there is probably not a single programmer in in the python Community who could tell you exactly what happens if you add two objects together. [46:36] Armin Ronacher: The correct thing to do for a language is sort of what JavaScript did in many ways, where you keep the public exposed interpreter API very, very small to nonexistent, because then people will never get in this sense where they can start abusing it. [16:10] Armin Ronacher: unfortunately we expose so much of the language to the ecosystem around it that we can't change The Interpreter anymore. [37:35] Armin Ronacher: until pickle dies, we will never have versioned modules—this is disappointing.

Original transcript

[0:06] um so this talk is about [0:09] python inside the virtual machine and [0:11] how that actually affects python is a [0:13] language and because there's a lot [0:15] of potentially boring stuff in there I [0:18] would really welcome if people would [0:19] just ask questions or anything of that [0:22] sort along the way to spice it up a [0:24] little bit um what's interesting about I [0:27] think python as a language is that we [0:28] all use it and we are sort of we like it [0:33] I think for the most part at least [0:34] people don't seem to mind it too much um [0:37] but it's actually surprising how [0:39] nitty-gritty uh The Interpreter [0:41] internally is and how much it sort of [0:43] restricts Us in in what we can do but [0:45] also how much it enables Us in other [0:47] ways um so this is basically um so I [0:50] should give a little prease for this um [0:52] I've been doing python since so the [0:55] first version of python I used was 2.0 [0:57] but it wasn't like I wasn't programming [1:00] when 2.0 came out um I think the first [1:02] one I actually used was 2.2 but I had an [1:04] interest in trying to figure out how it [1:06] goes back to to see how um how the [1:10] language evolved and how The Interpreter [1:11] evolved um and then just recently I [1:13] think last year I already gave a talk [1:15] similar to this one and for that one I [1:17] decided to see how it is to compile [1:19] python 1.3 I think so it's uh it's it's [1:24] interesting how far you can go back with [1:26] The pyth Interpreter um and and actually [1:28] still get a running binary out of it uh [1:32] you can actually still compile the old [1:33] versions on Modern computers and and and [1:35] see how the language evolved um and so [1:37] there's a lot of stuff in The [1:39] Interpreter which didn't really change [1:40] over the years [1:42] um and I have just an interest for it [1:44] because I I like python a language I [1:46] spent a lot of time working in it um I [1:49] have made many open source projects in [1:51] it um and as such I was always curious [1:54] to see how far you can take the language [1:56] and how you can evolve The Interpreter [1:58] um or in this case how you can't really [2:00] change all that much because of some [2:02] reasons which we'll go into um on uh my [2:06] date shop at the moment is uh I work for [2:08] the most part on Sentry which is an open [2:11] source project written in Python uh most [2:14] mostly based on Chango framework and [2:16] it's a it's an error reporting tool um [2:19] and then other than that I did the flask [2:22] framework uh I had I started a new uh [2:25] sort of static file content management [2:27] system recently called Lector and I lots [2:30] of python libraries like V Ginger uh [2:33] markup safe and many more so I have done [2:36] a lot of python and but never really as [2:39] a as a core developer on the language I [2:40] do have Commit access for some [2:42] inexplicable reasons but I I barely ever [2:44] contributed anything to The Interpreter [2:46] but I have a lot of fun looking in in [2:48] and see how The Interpreter is um so I [2:52] would say that in in regards to all of [2:54] the languages which exist in the world [2:56] for programming um they kind of are on a [2:59] on on a line that goes from very Dynamic [3:01] to very static and very Dynamic can be [3:05] seen in many different ways so for [3:07] instance how many here use Ruby or have [3:10] used Ruby a little [3:12] bit okay not that many but I I should [3:15] probably explain a little bit about Ruby [3:16] because it's kind of interesting in a [3:17] comparison to python which is that uh in [3:21] Ruby they Pride themselves as the Ruby [3:24] community that it's a very very highly [3:26] Dynamic language in the sense that you [3:28] can change a method [3:30] on the I don't know on the float class [3:33] and then you can change addition to [3:34] subtraction uh not that you would do [3:36] that but you can do it um so it's very [3:40] dynamic in the sense that you can change [3:42] Behavior which you think shouldn't [3:44] necessarily be something that you want [3:45] to change um and python doesn't do that [3:49] you can't change the under under add [3:51] method on the float class I mean [3:53] technically I guess you could but it was [3:56] never designed to support that um but [4:01] I actually think that python as a [4:02] language is way more Dynamic and as a [4:04] result way harder to optimize than it is [4:07] to optimize ruy uh and have some um some [4:10] examples of this um and I find it [4:13] interesting because we all are being [4:16] told that python Works in a certain way [4:18] and then as you dig deeper and deeper [4:20] into it you realize that that's actually [4:22] totally not how the language Works um [4:25] and you would think that or maybe we [4:27] change the language so that it works how [4:28] we are told that the language work works [4:30] and then you realize you can't do that [4:31] because people actually depend in very [4:33] bizarre ways on some of the internals in [4:35] the language and as a result it makes it [4:38] very hard to evolve the language so [4:40] certain things that people want to have [4:41] for a long time is getting rid of the [4:42] global interpreter lock and that turns [4:45] out to be a hard and also very much um [4:49] relevant to how The Interpreter was [4:51] designed initially and what guarantees [4:52] it gives um is very hard to um not just [4:56] bring concurrency into the language but [4:58] also to bring the ability to have [5:00] multiple versions of the same Library [5:02] loaded at the same time um it makes it [5:05] very tricky to have multiple [5:06] interpreters running in the same [5:07] executable side by side without sharing [5:10] anything in it and it makes it even [5:12] harder to just change some of the basic [5:15] principles in in how the the types in [5:18] Python work um and over the years more [5:21] and more stuff has been piled on top of [5:23] this already very complex language in [5:25] particular um Python 3 I think goes very [5:28] far in in doing all kinds of stuff on [5:31] top of all of this [5:33] um which also lacks a little bit of peer [5:35] review I would [5:36] say okay so this is the language we are [5:40] all told uh exists you have a global [5:42] variable called Magic it's 42 and [5:44] there's a function called add magic and [5:46] it adds a plus magic I think that makes [5:49] a lot of sense it's not a very useful [5:50] function um but it is a function that [5:53] everybody understands um and a lot of [5:56] tutorials on the internet will tell you [5:58] that this is actually equivalent to [6:02] this [6:03] which it's not [6:06] really [6:07] um it's not really for many different [6:10] reasons but in fact it is totally not [6:12] that um what is actually happening [6:14] behind the scenes is that python [6:16] compiles all of this into bite code and [6:18] the instruction for adding things [6:20] together is called binary ad um and from [6:24] that moment on we go down a rabbit hole [6:26] which has nothing to do with under under [6:28] at under under [6:30] um there's a there's a file in Python [6:32] called C eval which is uh I don't know [6:35] how large it is at this point but it's [6:37] very huge I think it's about a couple [6:39] thousand lines and it's basically a huge [6:41] switch statement and it's a a big loop [6:44] and it checks every single uh op code so [6:47] for instance there's a there a a branch [6:49] in there for binary [6:51] Edition um and I I I cut this [6:55] tremendously in fact there a dot dot dot [6:57] dot dot dot all of those I just deleted [6:59] and from the slides because they're not [7:00] super relevant but if you actually [7:02] consider how much codes The Interpreter [7:04] has to walk through to add two numbers [7:07] together it's a lot um and it became [7:10] bigger and bigger over the years um it [7:12] might have been easier at one point but [7:13] at the moment it takes two arguments [7:16] from the stack so it pops these two [7:18] arguments and then it does all kinds of [7:20] stuff for instance it checks if both of [7:22] those V if the first variable and the [7:26] second variable or the first argument [7:27] second argument if they're both exactly [7:29] integer and not just sub classes or [7:30] anything of that sort then it does a [7:32] special thing um if both of them are [7:35] exactly strings then it does a special [7:37] thing um otherwise it goes to p number [7:40] at um I will look into what p number at [7:43] is um but what do you think p number at [7:46] should be with that name [7:50] um it implies something with numbers and [7:53] addition um if you actually look at what [7:55] it does there's a thing in there where [7:58] um the first part has something to do [8:00] with nbor at that's number Edition slot [8:04] we'll go into this but then also it has [8:06] a special handling for sequences in [8:08] there so Pi number at is not actually [8:10] anything to do with numbers it's [8:12] actually Pi object at it just has a bad [8:14] name [8:15] um but one thing you will notice here [8:18] there is no under under add anywhere in [8:19] this function like it doesn't look for [8:21] an attribute called under under add uh [8:24] what it does it invokes this binary [8:26] operation one on the variable V and W [8:28] that's the first two arguments to the [8:30] number thing and then it looks at the [8:32] slot and B add so how many here know [8:35] what the slot is in [8:37] Python okay so I would just go by random [8:41] uh one person over there who just said [8:43] slot [8:45] um actually let's do thing slots in [8:48] Python are two things and a lot of [8:49] people think that slots are under under [8:51] slots under under to give an object [8:54] what's called a slot and to remove the [8:57] dictionary that's one of of the two [8:59] slots that python has the other slot [9:01] that python has is these special things [9:04] um which are added on top of the type on [9:09] the C interpreter to not have to look [9:12] into a dictionary attribute um what this [9:15] means in practice is that up here on the [9:18] on the V and the W object which we have [9:20] on The Interpreter level there are [9:22] special um containers it's a huge struct [9:26] and on different positions in the struct [9:29] uh there are different methods stored so [9:31] for instance if you add two numbers [9:33] together [9:35] um it's it's ridiculously hard to read [9:37] we're just going to skip past this as [9:39] much as we can but there's a each [9:41] variable each python object has an OB [9:44] type which is sort of a pointer to the [9:46] type object it has and on OB type we [9:49] have for instance TPS number which means [9:51] if the type is seen as number there is a [9:53] huge list of methods it could have um [9:56] and then in this case we might invoke [9:58] that thing and then otherwise if uh uh [10:02] what do we have so we we look at the [10:04] slots of both two different types and [10:06] then depending on the relationship of [10:08] them together with add numbers and this [10:11] actually if you if you wouldn't look at [10:13] my slides um is in total something like [10:15] 400 lines of code for just adding [10:16] numbers together [10:20] um and where is underscore underscore [10:23] atcore uncore it doesn't appear anywhere [10:26] um sort of that's the big lie uh the big [10:29] lie is that we we invoking this special [10:32] things um but we're not uh inste we're [10:34] looking at this stupid slots things um [10:37] and they're [10:39] surprisingly uh frustrating I would say [10:42] um if you look at the interpreter and [10:45] try to reason about it um it would be [10:47] very nice to say like you have an object [10:48] a and object B and to do something an [10:52] addition with them or subtraction or [10:53] multiplication there's a very very clear [10:57] thing that happens um [11:00] JavaScript learned this recently they [11:01] have a standard uh and it defines very [11:03] clearly what happens if you add two [11:05] objects together in Python it's it's [11:08] it's impossible to tell um it depends [11:11] especially if I have python 2 it depends [11:12] on flags on the object um it depends on [11:16] the specific relationship the types have [11:18] together with each other um and I would [11:20] argue that there is probably not a [11:22] single programmer in in the python [11:24] Community who could tell you exactly [11:27] what happens if you add two objects [11:29] together um and I I kind of assume this [11:33] from the fact that there's the pipie [11:34] project and the pipie project tries to [11:37] implement the Python language in Python [11:39] um and it's kind of interesting [11:41] sometimes when pipy disagrees with the [11:43] implementation that cpython has and you [11:45] would think that they must get it right [11:48] but then they still disagree in in [11:49] certain ways [11:51] um and it is very [11:54] tricky so to make this a little bit more [11:56] concrete what is a slot a slot is is um [12:00] is basically a it's a slot on a struct [12:04] which the python interpreter keeps for [12:06] every single type so yeah you can [12:08] imagine this way you have an integer and [12:11] if you look at the class from the python [12:13] site there's a dictionary on it on in [12:14] this dictionary there special underscore [12:16] underscore atcore underscore and there's [12:18] a function to it so there's a function [12:20] that is invoked with that name um but [12:24] the C interpreter was written long [12:26] before I guess this special method [12:28] existed so when you go back to I don't [12:31] know 1995 there were two things there [12:34] were python classes and there were C [12:36] types and they had no relationship with [12:37] each other so if you added two integers [12:40] together that were written in in C that [12:43] were like built-in types into the [12:45] language then it always invoked this uh [12:49] special slot method called NB at that's [12:51] what it did um and later on they added [12:54] support for classes and then I said okay [12:56] so it would be nice if classes could be [12:57] also added together so they added [12:59] independently of these C types they [13:01] added python classes and in the python [13:04] python class the logic was underscore [13:05] uncore atcore uncore is the special [13:08] callback method um to to add numbers [13:12] together and so they had these two [13:15] separate systems and they try to unify [13:17] them I guess or maybe that even happen [13:19] side by side I can't really tell it's [13:20] hard to say but nowadays what happens is [13:23] if you have a cclass and it has a slot [13:27] then there is some magic in the C code [13:29] code which exposes these slots as the [13:31] special methods to python code and same [13:34] way if you make a special python class [13:36] with underscore uncore atore uncore it [13:39] takes this function which is there and [13:41] stuffs it into the C type and stores it [13:43] there as a c function so they try to [13:45] synchronize up with each other um so you [13:49] could think that fuore uncore atcore [13:53] uncore can be this food type TPS now NB [13:57] which we have on the c c interpret [13:59] that's sort of the expectation that they [14:00] are synchronized with each other [14:03] um but history makes this really hard so [14:06] for instance um you have to imagine that [14:09] very early on python had integers and [14:11] floats but it also had strings and lists [14:16] and [14:17] tupal and so whoever that's what [14:20] probably uh guo wrote this himself he [14:23] decided that there are two different um [14:26] concepts for adding things together one [14:28] for numbers one for sequences and so [14:30] they have the two different slots for [14:32] adding things together um and plus looks [14:36] at both of those slots so you can [14:39] already see that there might be [14:40] desynchronization that can happen um but [14:44] theoretically they're kind of supposed [14:46] to map to underscore underscore um [14:50] but it just becomes very hard to explain [14:53] as a uh to a user and and that's also [14:57] why I guess nobody really explains that [14:59] because it seems Irrelevant for the most [15:00] part um if you read a tutorial it says a [15:04] plus b is a under under at under under B [15:07] slightly more tuto slightly more correct [15:10] tutorials have discovered that what [15:13] seems to be happening is actually the [15:15] type of a invokes under under at with I [15:18] should actually say a comma B um but [15:21] both of them are wrong because here's [15:24] what happens if you write a plus b in [15:26] The Interpreter um it checks if a and B [15:29] are integers in case they're integers it [15:31] tries to do a fast addition on the like [15:33] in a very special place in interpreter [15:35] um if they both strings and it tries to [15:38] make a fast [15:39] concatenation then it will try to do a [15:42] number addition is a implementing number [15:44] slots then it resolves the slot is B [15:46] implementing number slots and resolves [15:47] that slot based on the type relationship [15:49] between them which is very complex I'm [15:51] not going to go into this it invokes [15:53] them slightly differently but usually it [15:56] ends up with something like the slot of [15:59] a invokes with a and b um but then if [16:03] that also doesn't doesn't work then it [16:05] tries to do the sequence concatenation [16:06] which goes for a different interpret [16:08] path in [16:10] comparison um this is what happens if [16:12] you do a DOT under under at under under [16:15] B it looks for an attribute called first [16:18] it looks for an attribute called under [16:20] under at on type then it looks this up [16:24] and invokes it um the scary thing is [16:28] this thing is easy easier than this [16:31] thing so [16:34] um the fast path that we have in The [16:37] Interpreter through the slots and [16:39] everything we have at this point is [16:41] significantly more complex than what we [16:44] are actually told as Python program is [16:46] what happens um this path is not [16:48] optimized um this path is often not [16:51] directly invoked but you can find places [16:54] where manually writing this is actually [16:57] faster um than this not always but in [17:01] some [17:02] cases um and the reason why I want to [17:04] mention this is because the interpretor [17:07] has grown over the years with [17:09] optimizations and other things to make [17:12] it operate faster in this very complex [17:15] World it has set up [17:19] and you would think that well why don't [17:22] we just take this thing and optimize it [17:25] and then say when you do a plus b it [17:27] does a under under add on under B like [17:30] why don't we just do that that seems to [17:31] be like the reasonable thing to do [17:32] that's easy that's what every tutorial [17:34] says why do we do this complex [17:37] thing um and the reason for this is that [17:40] unfortunately we expose so much of the [17:43] language to the ecosystem around it that [17:47] we can't change The Interpreter anymore [17:51] um [17:52] so everyone who has ever written a c [17:55] extension in Python is uh quite aware of [17:58] how how low level the C API is so every [18:02] custom C type you add knows about the [18:05] slots [18:07] um [18:08] every even even if you ignore the cap [18:11] API entirely slot wrappers as a concept [18:14] are exposed to python as a language um [18:17] every once in a while even newbies that [18:20] have never used a language encounter an [18:22] exception message which says like slot [18:24] rapper has no attribute under under Fu [18:27] or something because these all of those [18:29] internals need to be exposed um to the [18:34] runtime [18:36] and we have this uh I would say very [18:40] very complex uh just language buildup [18:45] which is hard to explain to people and [18:47] it's sort of for some inexplicable [18:50] reasons used by everyone with all of [18:52] those little needy details um so for [18:56] instance um [19:00] I I have a better example later on um [19:02] where we have some code for this but [19:04] point is that they are not [19:06] equal and they try to be hidden for you [19:09] as a user as much as possible so for [19:11] instance if you have a class X which is [19:13] a subass of object I think most people [19:15] have written this at one point to add [19:17] the special at method and then when you [19:19] access this you can see there's an [19:20] Unbound method X which happens to be a [19:22] Lambda um what happens if you look at [19:25] what uh a c type looks like well if you [19:28] do it under under at on the C type then [19:30] you get a slot wrapper what's a slot [19:32] wrapper well what this actually is this [19:34] is a special thing which wraps a c [19:39] function on the C interpreter level [19:42] but it's exposed to a python user like [19:46] you can see that these are clearly not [19:47] equivalent even though they should [19:49] be and so we have exposed all of this [19:52] like very nitty-gritty detail of The [19:55] Interpreter all the way up to the Python [19:57] language so if someone ever changes this [20:01] behavior all the python code would have [20:03] to deal with a different object there um [20:05] and this is something that unfortunately [20:06] python didn't do just as well as [20:08] JavaScript points because in JavaScript [20:10] everything is a function like there is [20:12] no difference we have one function in [20:13] JavaScript and if you write one yourself [20:16] or if you do it on the C Level they're [20:18] just the same there's no real difference [20:20] in looking at them other than that one [20:22] of them might not expose the source code [20:23] that created it um where in Python we [20:26] have we have slot wrappers we have [20:28] method descript scriptors we have uh [20:30] built-in function we have function we [20:32] have callable objects we have a huge [20:33] range of all of those and they all [20:35] exposed the slightly small details in [20:37] how they behave differently to give you [20:39] an example if you have an Unbound method [20:42] under under at under under and you take [20:44] a reference to it and you put it onto [20:46] another class then it acts as a [20:49] descriptor so it receives self as the [20:51] first argument if you try to do the same [20:53] thing with a slot wrapper it doesn't do [20:55] it um and people start you depending on [20:59] this so for instance one thing I have [21:00] seen a couple of times is that someone [21:02] writes a class and then as a class [21:05] attributes there is written conversion [21:08] function equals [21:10] int um and that depends on int that type [21:13] not having a descriptor so not binding [21:16] self as the argument um so you could [21:19] never change that behavior you could [21:21] never clean it up because we depend on [21:23] these little differences in it [21:26] and and as you go into like uh piie is a [21:29] language they all have to replicate [21:31] every single bits and piece of these [21:33] like very intricate [21:34] parts and python tries to sync them up [21:37] as much as possible to hide this [21:39] complexity as much as possible from you [21:40] but it unfortunately doesn't get it [21:42] right all the time um and the reason why [21:45] we should care about this is because [21:46] it's very complex and everybody uses it [21:50] and it makes [21:52] optimizations almost [21:54] impossible um we have to emulate it as [21:58] we go to languages but it also shapes [22:01] how our own code works and this is a [22:04] this is an example which thankfully has [22:05] been fixed but this is I think a very [22:06] interesting one uh there's a module [22:09] called re so if you run this on Python [22:11] 2.7 or newer it behaves differently but [22:14] I think it shows very well in how weird [22:16] it gets so there's a module called re [22:19] for regular expressions in Python and if [22:21] you compile an expression you get an [22:22] object back and that object is an a [22:24] special object called Sr rule or Sr e RX [22:29] or something and if you access under [22:33] under class under under it gives you an [22:34] attribute error that seems weird like [22:38] why does it do that how does it do that [22:39] what how does it make any sense for an [22:42] object in Python not to have a class [22:43] attribute and in fact it also doesn't [22:45] have under under rapper under under so [22:47] it has a whole bunch of behavior of a [22:50] normal python object but then also it [22:53] never responded to those special [22:55] attributes so how how can this be and [22:57] the reason for this is that this object [22:59] was written in C and whoever wrote the C [23:02] object customized the entire behavior of [23:05] sort of how it looks like in the python [23:07] land so the C interpreter sees a [23:10] different version of this object than [23:12] python code would do so for instance you [23:15] if you would want to write the rapper [23:17] function in Python to invoke the special [23:19] under under rapper under under function [23:21] you could never implement this because [23:23] at least this object would always [23:25] disagree with it because you can only [23:27] access the class information and the [23:28] repper information from the C language [23:31] you can't do it from the Python language [23:33] um and it it's it gets really weird [23:36] because there's so many cases of this um [23:39] and this is uh this is an example of [23:41] where the cap API allows you to [23:43] implement a certain behavior that is [23:45] completely invisible from the python [23:49] level [23:51] um so this obviously is not Python 3 [23:53] code this is python 2.4 code let's [23:57] say and in all the versions of python it [24:01] was possible to create a class that um [24:04] is not a new style class so to say where [24:07] you implement under underg Ator and then [24:11] it returns an attribute from another [24:13] object and would pass through all of the [24:16] special methods so you could for [24:18] instance instantiate a and it looks like [24:20] 42 because when The Interpreter looks [24:23] for the under under wrapper under under [24:25] function it goes and looks through get [24:29] attribute and it will find the under [24:30] under function on the integer 42 so it [24:33] is a 100% proxy so to say um and modern [24:38] python doesn't work like this anymore [24:40] and the reason for this is [24:43] that this special instant type which [24:46] exists in all the python versions and in [24:48] old style classes in new python versions [24:50] forwards all of the [24:52] calls like it did in very old versions [24:55] of python so it emulates what the python [24:57] interpreter used to be [24:58] at one point um just that at this point [25:03] it has to do it as this very complex [25:05] proxy [25:07] object so when when you actually [25:09] consider how python is a language was [25:11] taught to people at one point this under [25:14] under at under under was actually [25:16] correct it just became more complex over [25:18] the years and and now has this massive [25:20] sort of disagreement in how it [25:24] works um the other thing that sort of [25:27] leaks through which is a little bit [25:29] easier to understand is uh is Unicode [25:32] and I think for Unicode it is kind of [25:34] important to understand where the [25:35] language comes [25:36] from when python was created originally [25:39] it didn't have any Unicode support at [25:40] all it had strings and strings were [25:42] implemented as straightforward as you [25:44] can imagine they [25:46] hold uh the a sequence of characters [25:49] each character has a numeric value from [25:51] 1 to [25:52] 256 [25:54] and because of that it gives you certain [25:57] ideas of how is string is supposed to [25:59] work and python exposed this so for [26:00] instance if you have a string of SK [26:03] characters you can always get the first [26:05] character you can always get the second [26:06] character the third character fourth [26:08] it's very easy to access these [26:10] individual characters on it when they [26:13] started adding uni code supports to it [26:15] they decided that the best way to do it [26:18] would be to mimic this Behavior but [26:20] instead of going from zero to 256 they [26:23] would go from Z to 65,000 and then later [26:26] on when they realize that Unicode is [26:28] going to grow they decided that we can [26:30] also compile python in a version where [26:32] will go to much larger [26:34] numbers and this is nice in one hand on [26:39] one hand but it's also very [26:40] disappointing on the other um so does [26:43] anyone here know what UCS [26:46] means encoding it's an encoding uh UCS [26:50] uh I don't actually know what it stands [26:51] for I think might be uni code something [26:54] system it was [26:56] basically uh about 25 years ago I guess [26:59] someone decided that we need to [27:02] evolve uh s key for all the languages in [27:05] the world and uh there were two [27:08] independent consortiums which decided to [27:10] do that one of them was the Unicode [27:11] Consortium and they decided 65,000 [27:13] characters is enough for everyone um [27:16] they were wrong they had to make it [27:17] larger afterwards but independently [27:20] there was a group of people on the iso [27:21] committee which decided that they're [27:23] going to make um two to the power of 32 [27:27] different characters and um they made a [27:31] much more complex standard which didn't [27:33] really go anywhere because they couldn't [27:35] figure out how to encode it efficiently [27:37] and then along the way where these two [27:39] standards were evolving um two things [27:42] happened the first one was that people [27:43] realized unic code will have to get more [27:46] characters than uh 65,000 um so in [27:50] particular if you use emojis emojies are [27:53] way past 65,000 they are uh they need [27:55] more than two bytes um [27:58] but [27:59] also eventually they realize that it's [28:02] actually very valuable to encode um the [28:06] the uni code um data into more compact [28:11] uh encodings so if you if you imagine [28:13] that um you have most of your text fit [28:17] into asky or cilic or something that's [28:20] very low in numbers then it doesn't [28:22] really make any sense to store all the [28:24] numbers with a lot of zeros in between [28:26] because it's just a waste of space so if [28:28] you would store all the text in uh in [28:31] two bytes then first of all you could [28:33] only store 65,000 individual characters [28:35] per position but also it would be very [28:39] wasteful um so the utf8 is the most [28:43] common encoding where you can sort of [28:46] make the individual character larger or [28:48] bigger depending on how much um how [28:49] large the number in it is so um s key [28:52] for instance fits into 8 bytes sorry 8 [28:55] Bits um klic I think fits into two if [28:58] I'm not mistaken [29:01] um and it it grows up like this uh ucs2 [29:05] on the other hand is basically the [29:06] original uni code encoding it's each [29:08] character going from 0 to [29:10] 65,000 and there is no support [29:13] whatsoever to encode characters in it [29:15] which are larger than 65,000 so if you [29:18] if you take a standard python 2 [29:19] interpreter and um and it's compiled [29:22] with ucs2 then accessing the first [29:24] character second fourth and so forth [29:26] will um will give you exactly the [29:29] character but then if you ENC encounter [29:31] an emoji the Emoji will split in half so [29:34] if you would have to exess the first [29:35] part in second part of the Emoji [29:37] independently um and so if you want to [29:39] deal with emojis in Python 2 you would [29:41] have to compile it with UCS 4 which [29:44] takes than four bytes per character [29:46] which is enormous waste in particular [29:48] it's an enormous waste because the [29:50] largest character which can ever be in [29:52] unic code is only 21 bits I think yeah [29:55] 21 bits which is just like LLY above [29:58] half of it so you will have the vast [30:00] majority of your space unused um and so [30:03] why why do we waste so much memory in [30:05] Python why why do we not just use udf8 [30:07] or something like this um because we [30:10] guaranteed that if you access the first [30:12] character you will get the first [30:13] character and if you access the second [30:15] character you'll get the second [30:16] character and the only way to do this in [30:19] um in utf8 would be to scan character by [30:22] character by character to figure out [30:24] which one you want to access whereas if [30:26] we store it in UCS 2 or UCS four then we [30:29] can skip the number of bites until by [30:32] the [30:33] characters and there are many arguments [30:35] to be made about a system like this [30:38] versus other systems but I would say in [30:40] the last 10 years it has um it has [30:44] emerged that uh utf8 is the way to go um [30:49] and a lot of new languages actually [30:51] embrace this a little bit uh so for [30:53] instance go is UTF everywhere and it [30:55] doesn't give you character access same [30:57] thing with r [30:58] um I think Swift might be doing [31:00] something similar but at the very least [31:02] um Apple never really gave you [31:05] this this direct access to character so [31:08] the language didn't give you this idea [31:11] that you could do this efficiently [31:14] um and it's kind of disappointing that [31:17] in Python [31:18] we the nice thing is we we fix this in [31:21] Python 3 a little bit where depending on [31:23] the largest character in the string it [31:24] gets bigger or smaller um but it still [31:28] gives you this guarantee and this [31:30] guarantee is only given to you because [31:32] The Interpreter decided at one point [31:34] that that is a reasonable thing to do it [31:37] it was not I don't think there was a [31:39] conscious decision in doing this I think [31:41] it just happened [31:43] um and this is sort of a a general story [31:47] in Python where the the language that we [31:49] use is pretty much exactly The [31:51] Interpreter as it exists and this on the [31:55] one hand makes it very hard to do [31:57] optimization [31:58] um and it can get really weird if it [32:01] desynchronizes but it doesn't happen [32:03] very often but on the other hand it gave [32:05] us as python [32:07] programmers uh I would say a very [32:09] different way to look at a language [32:11] compared to many other programmers [32:13] um if you're a JavaScript programmer or [32:16] if you are a r well maybe not R but if [32:19] you're a JavaScript programmer at the [32:20] very least you you take the language and [32:23] you use it and you live with the [32:25] limitations of it and [32:28] maybe over the years you realize that [32:31] there is so many limitations in it and [32:33] you you you go to browser windows and [32:34] ask them to give you a little bit more [32:36] access to the language uh but it [32:39] stays um it stays very high level it [32:43] stays very um very sandboxed and [32:46] contained whereas in Python because The [32:48] Interpreter has always shined through so [32:50] directly a whole bunch of stuff has [32:52] happened which I think is quite unique [32:55] um the first one is and I think that's [32:57] for for me the biggest reason why I like [32:59] python so much like even though I know [33:01] that all of this exposed interpreter [33:03] makes it hard for us to ever get rid of [33:05] the Gill makes it hard for us to ever do [33:07] certain optimizations where we're always [33:08] kind of locked into this sort of uh [33:11] language that works in this way but on [33:13] the other hand we have so much power in [33:15] our hand because we have access to The [33:16] Interpreter um that this is like who [33:20] uses this get frame or has heard of [33:23] it a shocking small amount of people um [33:27] this get frame [33:28] is is sort of the secret source of what [33:30] makes a lot of python [33:33] code uh spicy I wouldn't say good but um [33:39] basically what you can do at any point [33:40] in time you can ask The Interpreter give [33:42] you the internal state of the current [33:44] call site um and this returns a frame [33:47] object and this Frame object exposes [33:50] almost the entire interpreter State at [33:52] this point in time which from a [33:53] performance point of view is nuts you [33:55] shouldn't do that like if if would go [33:58] into like designing a new high level [34:01] language everybody would argue that this [34:03] never give this thing this thing is slow [34:06] it's ridiculous like just don't expose [34:08] it it shouldn't exist um python exposed [34:11] it uh very very early on and code [34:13] started using it a lot uh and because [34:16] code started using it the the core [34:18] language developers were forced to keep [34:20] it at least somewhat [34:22] performant um and something like SS get [34:26] frame also exists in other languages [34:28] like Ruby and in Ruby it's really really [34:30] slow because nobody uses it um so to [34:32] understand what sus get frame is it [34:34] gives you a frame object and with the [34:36] frame object you can access everything [34:39] that's there so for instance you can [34:40] access local variables you can under [34:42] some circumstances change local [34:44] variables you can see the fun the file [34:47] name where you're currently executing um [34:50] you can uh see what exception has been [34:53] on this Frame and so forth and this has [34:57] been abused but also heavily used so for [34:59] instance um there's a project called [35:02] soap interface where you call a method [35:04] called implements and it goes into it it [35:08] looks at the current call site and then [35:09] goes one up and then it asks the class [35:13] to inject a new attribute into it this [35:17] is something that doesn't make any sense [35:19] from a from a calling point of view but [35:21] they just decided to do that um the [35:23] warnings module depending on where you [35:25] call it um will anchor the warning to a [35:28] different code location so for instance [35:30] you can say I want to warn that this [35:33] method is going to be deprecated but I [35:35] want the warning to be relevant two [35:37] frames up and it does this sort of stuff [35:40] at uh the inspect module is full of this [35:41] stuff uh logging for instance um will [35:44] skip frames in the logging module so [35:47] that it logs based on the location where [35:49] you invoke the method yourself and not [35:50] where the method was defined um and then [35:54] like so much debug support so for [35:56] instance does anyone know twisted [35:58] manhole probably not it's a one person [36:02] it's a it's a very uncommon thing but [36:04] there are so many versions of this that [36:06] people use inside companies even where [36:09] you can take a running python [36:10] interpreter and it just runs there and [36:11] it does its thing and then something [36:14] weird happens in it let's say you have [36:16] um a computer game and it just has a [36:18] server process it runs there it emulates [36:20] a game world and you don't want to stop [36:22] it but it misbehaves slightly you can [36:25] actually attach a debugger to the python [36:27] interpreter and execute python code [36:28] running in there and figure out what it [36:31] does at any point in time it gives you [36:32] all this state of The Interpreter and [36:35] you can do really interesting things [36:36] with it um so for instance I work for [36:38] Sentry and Sentry is is a system where [36:42] you can report crashes to and we get all [36:45] of this information out of a crash at [36:47] runtime from The Interpreter so we can [36:49] we cannot just report where a crash [36:51] happened and what the other frames were [36:52] we can report every single local [36:54] variable that was there we can report [36:56] how much time was spent um that we can [36:58] get from The Interpreter State um this [37:00] sort of [37:01] flexibility is not available in most [37:04] other languages and as a python [37:05] programmer we are kind of used to this [37:07] because even if we don't use it [37:08] ourselves if you write a Chango um [37:11] application and it crashes the Chango [37:13] debug page comes up and it prints you [37:15] out all of the states that was there [37:17] like the the the framework Chango [37:19] depends on having access to this uh same [37:22] thing with flask if there's an exception [37:23] it will print out the St phace in in [37:25] flask you can even execute your own code [37:27] in an HTML page and figure out more [37:29] stuff that's there and that wouldn't be [37:31] possible um if this wasn't [37:35] exposed the the other thing um that sort [37:38] of is exposed is Su modules um which is [37:43] nice on the one hand but it's not so [37:45] nice for others SS modules is U whenever [37:48] you import a module it's cached in this [37:50] special attribute called modules and you [37:53] can it guarantees that whenever you [37:55] import this module every single other [37:57] thing will see the same version of it so [37:59] if you import the CGI module in this [38:01] part of the application and you import [38:03] the CGI module in the other part of the [38:04] application then they both will see the [38:06] same one and the reason they see the [38:08] same one is because the the the module [38:10] is cached in the sus modules [38:13] attribute and the the downside of this [38:17] is that everybody expects it to be there [38:21] so at any point in time people started [38:24] uh saying that oh I have a class called [38:27] X it's from module Fu and I can just go [38:30] s modules Fu and I find my class in [38:32] there pickle uses this to reimported [38:35] stuff um copyright uses it to make [38:38] copies of things um people write import [38:41] functions like this import modu module [38:43] under under input under under module and [38:45] then they look for it in SS modules um [38:48] this is exposed interpreter State you [38:50] don't have that in many other languages [38:52] and the downset of this is obviously [38:54] that we will never be able to uh change [38:58] this now because everybody expects that [39:00] this is the case so there have been many [39:02] attempts for instance to allow multiple [39:04] versions of the same python library to [39:06] load in the same interpreter and it [39:09] seems almost impossible to do now [39:11] because we would have to make S modules [39:13] also have a version identifier or a [39:15] scope identifier or something else in [39:16] there not just a module name so the [39:19] upside is we can discover every single [39:21] module the downside is we can only ever [39:23] have one of them at the at the same time [39:26] um and because [39:28] everybody depends on this um and [39:31] especially pickle and we you have to [39:34] imagine that when people pickle out the [39:36] objects they pickled out for eternity uh [39:38] there soap has a database which is [39:40] entirely based on pickling objects if [39:42] you change pickle now you're going to [39:44] break all the pickles which were ever [39:45] created um so until pickle dies we will [39:50] never have version subm modules this is [39:53] disappointing um this is a this is a [39:57] which [39:58] um is part of the reason why getting rid [40:01] of the uh the global interpreter lock is [40:04] hard have you ever wondered probably not [40:07] but I wondered for a long time why int [40:11] is a type and a subclause of in is a [40:15] class did anyone to wonder why that is [40:18] the [40:19] case you can show [40:21] hands no so I wondered why why is one [40:25] thing a type one is the other thing is a [40:27] class like how does it make sense like [40:28] theoretically making an empty subass of [40:31] an integer why does it make a different [40:33] thing um and the answer for this turns [40:36] out to be [40:38] that uh this by the way in Python 3 [40:40] looks a little bit different I think [40:41] they both say type or class now I'm not [40:43] sure but they still work the same the [40:46] difference is that a [40:49] type is guaranteed to live always [40:52] exactly at the same memory [40:54] location and a class sits somewhere on [40:57] the keep so you have to imagine this [40:59] like this um the python interpreter has [41:03] a lot of internal types like integers [41:06] floats strings Unicode object list tups [41:09] and so forth and one of the things that [41:12] The Interpreter internally has to do it [41:13] has to compare them like for instance uh [41:16] you had this example before where you [41:17] add two numbers together if you add two [41:20] numbers together The Interpreter would [41:21] like to know are those two exactly [41:23] integers so how does it know if a number [41:26] is exactly an integer the reason for [41:28] this is that there's a a pi in type [41:31] which is the internal representation of [41:33] integers and it's a global variable and [41:36] one of the things with glob variables is [41:38] you can only have one of them um so you [41:40] can't have two p in types um and so what [41:43] The Interpreter does is it checks if the [41:47] type is exactly the memory address of [41:49] that [41:49] integer [41:51] um so what does this mean well it has a [41:55] very it has a very annoying consequence [41:58] which is that imagine this imagine you [42:02] are engine X and you would like to use [42:06] um uh python as a scripting [42:09] language then what you would like to do [42:12] is you would like to have let's say four [42:14] or five threats running and each of them [42:17] has an individual python interpreter [42:19] running that's for instance how [42:20] JavaScript work if you if you use a [42:22] browser um each of your tabs might run [42:25] an independent JavaScript engine and [42:28] these engines they don't share anything [42:31] and python we can't do that because if [42:32] it would run two python interpreters [42:34] they would see the same python type and [42:37] this is not a constant this can change [42:40] there's a reference count on it people [42:42] could patch their own things on it if [42:43] they manag to bypass this interface and [42:46] this is I think the biggest reason why [42:47] python was never a language that was [42:49] embedded in things [42:52] because it's it the interpreted design [42:55] internally never supported this idea of [42:57] having multiple interpreters running [42:59] which is uh which is PR disappointing [43:02] and I try to fix this I I try to go in [43:04] and change all of those types and go [43:06] through a level of indirection to make [43:07] it possible to for instance have one [43:10] fret running with one complete [43:12] independent interpreter and the second [43:13] fret running was a complete independent [43:15] interpreter and it just it's you you [43:19] start going down a rabbit hole of things [43:21] you can't change because everybody [43:22] depends on this um so what are the [43:26] consequences of all of this [43:28] and biggest one is we can't get rid of [43:29] the Jill um because we have for instance [43:33] only one integer type and it's shared [43:37] across everything and we couldn't for [43:39] instance say we would like to have one [43:41] threet running here with one interpreter [43:43] another fret running here with another [43:44] interpreter and they don't share integer [43:46] types um but also we expose on The [43:51] Interpreter level reference counts and [43:52] other things that are in every single C [43:56] extension and it's so ingrained in the [43:58] language that it's just at this point [44:01] something that you can't take away [44:02] without changing the language um we [44:05] can't change the internals because the [44:06] internals have been changed and as [44:10] mentioned before it's really hard to do [44:12] multiversion libraries now because s [44:14] modules is an exposed interpreted detail [44:17] um and it just goes on and on with [44:20] having this like really complex [44:23] interpreter which shines all the way to [44:25] the highest part of the language [44:27] and it's it's something that I [44:31] understand at this point that python as [44:32] a language is just this is how it works [44:35] and we have to live sort of with this [44:38] history of how The Interpreter works and [44:41] Python 3 also couldn't really change [44:42] that but it's something that maybe as [44:44] you go in and then discover other [44:46] languages is something that you wouldn't [44:47] want to repeat sort of um it has [44:51] obviously strong implications on the [44:53] speed of The Interpreter and on the [44:56] concurrent that you can have with it but [45:00] um it's I would say in the in the [45:02] current state of how we how we have the [45:04] language it's not something that really [45:06] stops us from doing [45:08] um from doing a lot of uh of of the work [45:12] that we do with it but we have sort of a [45:13] ceiling of where it's hard for us to get [45:16] over um yeah and that's sort [45:19] of uh what I have here um hard to [45:22] parallel I mentioned we we can't share [45:24] this uh static types well we can't [45:27] separate the static types because [45:28] they're shared [45:30] um and we also can't really go more [45:33] dynamic because The Interpreter already [45:34] has to cheat in certain places like for [45:36] instance Ruby can do making addition [45:40] into a subtraction if it wants because [45:42] there is no fast path for integer [45:44] addition uh but in Python to to stay [45:47] fast despite all of this complexity um [45:50] we we lost a little bit there yeah uh [45:54] I'm sorry I'm rambling through the [45:55] slides a little bit because I'm [45:56] realizing in that it's actually very [45:58] hard to convey this overall message that [46:02] how The Interpreter Works internally in [46:04] a sense that is sort of easy to [46:06] understand and what the ramifications [46:07] are of it um which is why I would [46:10] propose if uh people have questions to [46:12] it feel free to wildly go off what the [46:16] actual topic of this presentation is um [46:19] because I I can see that I think it's [46:22] maybe not as clear what sort of the [46:25] point of of of this Le is and and how it [46:28] affects people's [46:30] code yeah that's [46:34] [Applause] [46:36] it so what you basically just since you [46:41] said that it's maybe hard to get the [46:43] point across so is what's the point [46:46] actually that you should expose as as [46:50] small a public API as possible basically [46:54] because because that's what's happening [46:55] basically when you uh expose all the [46:57] internals and people depend on them and [47:00] yeah you going take it away that's the [47:02] problem yeah so you have to the the [47:05] correct thing to do for a language is [47:06] sort of what JavaScript did in a many [47:08] ways where you keep the public exposed [47:10] interpreted API very very small to [47:13] non-existent because then people will [47:15] never get in this sense where they can [47:17] start abusing it um and and with python [47:21] as mentioned we have this exposed API [47:24] and now it's hard to change it so yeah [47:26] it's this this idea of having a small [47:29] public [47:30] API uh best exemplified I guess in The [47:33] Interpreter and uh then again uh what [47:36] about the special cases like uh you [47:39] Illustrated for the addition I believe [47:41] there are lots of special cases in the [47:43] dict [47:45] implementation uh what's what's the [47:47] alternative I mean if you lose them then [47:49] um then it's slow right so but that's [47:52] the thing I actually AG I I can see why [47:56] python got all of this special handling [47:58] to make it faster but it did it in a [47:59] really wrong way because if you consider [48:01] how a computer nowadays Works [48:06] um a lot of the code is fast because [48:09] it's cached it's something it it has a [48:12] very little footprint it goes over the [48:14] same amount of memory all the time we [48:15] never it doesn't invalidate caches um so [48:19] it would be in the interest of The [48:21] Interpreter to keep the hot Cod paths as [48:24] small as possible um so for if we would [48:27] get more datas for dictionaries all the [48:29] time then we could make the dictionary [48:30] code as fast as possible and we are [48:32] spending more time in a dictionary code [48:34] than in all of the rest of The [48:36] Interpreter Loop um so I think that by [48:40] reducing the overall uh footprint of [48:44] sort of the complexity that goes into [48:46] one python object or the operations it [48:48] exposes then we could actually be both [48:51] more Dynamic and uh more flexible as a [48:55] result and also [48:57] um because it becomes easier to reason [49:00] over it and we have less code that we [49:03] have to execute all the time so it has [49:04] higher chances of being cached um it has [49:07] higher chances of being correctly [49:09] predicted [49:10] um it's I I think the irony in this is [49:14] that we are less Dynamic than Ruby but [49:18] we are probably slower per op code than [49:21] Ruby is um as as a result of this [49:25] because it was obviously slow so people [49:27] started doing this optimizations for the [49:29] common cases um but we could have made [49:32] The Uncommon case significantly Faster [49:34] by simplifying it thanks very much it [49:37] was [49:40] insightful you [49:44] sure that's it then thanks Harmon [49:47] [Applause]