I hope this helps.
Jonathan
APPENDICES
===========
SOME IMPLEMENTATION DETAILS AND COMMENTS
Because fn.__code__ must not return a permanent object, some sort of opaque proxy would be required. Because Python programs rarely inspect fn.__code__, in practice the cost of this additional indirection is likely to be small.
As things are, the time spent changing the refcount of fn.__code__ is probably insignificant. The benefit is that permanent code objects are made immutable, and so can be stored safely in read-only memory (that can be shared across all processes and users). Code objects are special, in that they are only rarely looked at directly. Their main purpose is to be used by the interpreter.
Python allows the user to replace fn.__code__ by a different code object. This is a rarely done dirty trick. The transient / permanent nature of fn.__code__ could be stored as a hidden field on the fn object. This would reduce the cost of the if ... else ... branching, as it amounts to caching the transient / permanent nature of fn.__code__.
FORK AND COPY ON WRITE
On Unix, the fork system call causes a process to make a child of itself. The parent and child share memory. To avoid confusion and errors, when either asks the system to write to shared memory, the system ensures that both parent and child have their own copy (of the page of memory that is being written to). This is an expensive operation.
INTERPRETER SESSION
>>> from sys import getrefcount as grc
# Identical functions with different code objects.
>>> def f1(obj): return grc(obj)
>>> def f2(obj): return grc(obj)
>>> f1.__code__ is f2.__code__
False
# Initial values.
>>> grc(f1.__code__), grc(f2.__code__)
(2, 2)
# Calling f1 increases the refcount of f1.__code__.
>>> f1(f1), f1(f2), f2(f1), f2(f2)
(6, 4, 4, 6)
# If fn is a generator function, then x = fn() will increase the
# refcount of fn.__code__.
>>> def f1(): yield True
>>> grc(f1.__code__)
2
# Let's create and store 10 generators.
>>> iterables = [f1() for i in range(10)]
>>> grc(f1.__code__)
22
# Let's get one item from each.
>>> [next(i) for i in iterables]
[True, True, True, True, True, True, True, True, True, True]
>>> grc(f1.__code__)
22
# Let's exhaust all the iterables. This reduces the refcount.
>>> [next(i, False) for i in iterables]
[False, False, False, False, False, False, False, False, False, False]
>>> grc(f1.__code__)
12
# Nearly done. Now let go of the iterables.
>>> del iterables
>>> grc(f1.__code__)
2