Let me answer some questions regarding the fractions class.
(Ping @oscarbenjamin)
Given f = fractions.Fraction(1, 2) is it expected that f would be freezable?
Yes, but that’s not currently the case because the prototype is not yet finished. I’ll expand on this more later in this post.
If the answer is “yes” then what is the full set of objects that would be frozen by freeze(f) and what are the implications of all of those things being frozen?
This is a little “tricky” to answer as it depends a little on how we address the caching points below, and also since we want to do sharing across subinterpreters as part of PEP795 maybe we want to mark some de-facto immutable types frozen at interpreter start. Let’s for simplicity assume that there is no caching going on, and that we do not consider any of the de-facto immutable classes in Python frozen out-of-the-box. Then a fraction object would indeed be freezable, and freezing a fraction object would lead to the following objects being frozen:
- The fraction object
- The numerator object
- The denominator object
- The Fraction type object along with all functions
- The fractions module object
- The numbers.Rational type object (the base class of Fraction), along with all functions
- The Real type object (the base class of Rational) with functions
- The Complex type object with functions
- The Number type object with functions
- The numbers module object
- The ABCMeta metaclass object with functions
- The object class with functions
- The type object with functions
The implications of these being frozen:
With respect to the types, the type objects Fraction, Rational, Real, Complex, Number, and object are not technically immutable at the implementation level, but are effectively immutable in normal Python usage. After freezing them, they are also “technically immutable” meaning no amount of metaprogramming should be able to change these classes. (But you can subclass them with a mutable class for example.) By becoming technically immutable, they also become safely sharable across subinterpreters.
The module objects also become immutable meaning you are not able to add or change fields in the fractions module object or int the numbers module object. (See escape hatches in a post that’s coming up soon.)
After freezing, the fraction object in f is also “technically immutable” which means that it can be shared with another subinterpreter by reference. Because its entire class hierarchy is also frozen, all subinterpreters will have a consistent view of the fraction object.
Why the prototype isn’t there yet
The reason why the prototype does not yet support fractions is because we have not yet made the caches that are used by the fractions class thread-safe and safe for use with subinterpreters. The reason for that is simply that we have not gotten around to it yet. Once we have addressed this, fractions will be possible to freeze. For clarity: there is no hidden gotcha or problem – just that we haven’t had the time to do it.
Let’s look at one of these caches which is in the ABCMeta class which uses caching that is implemented in C. If it had been implemented using threading.local, then it would (probably) have worked out-of-the box. Note that a per-thread cache is not the same as a global cache. There are pros and cons with each: a per-thread cache does not need to block on contention from many threads, but on the other hand one thread cannot take advantage of cached results from another thread. Subinterpreters come with some limitations that we need to respect.
Python currently does not permit objects to be shared between subinterpreters. This PEP will permit immutable objects to be shared. This will permit sharing an immutable cache across subinterpreters (i.e. warm it up and then freeze it), but does not permit having a shared mutable cache (i.e. that can keep adding cached values). It is of course possible to drop to C and implement a shared mutable cache (indeed, that’s probably what is going to happen in ABCMeta). Note that even if one drops to C to implement a mutable cache, one must take care to not share mutable objects between subinterpreters as this is not something that Python supports. (This will lead at best to crashes and at worst to silent corruption of data and heisenbugs.)
With free-threading support, rather than using subinterpreters, it would be possible to implement a global mutable cache, since the isolation imposed by subinterpreters does not apply. (One way to think about the thread-local escape hatch is as an interpreter-local escape hatch: there will be only one interpreter when using free-theading, in that case.)
See also below for a follow-up to this.