Add a setfunc decorator for attaching functions to an existing class or object
#2358
Replies: 1 comment
|
This is unrelated to Python typing and would better be suggested at discuss.python.org. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
The idea
A three-line decorator factory that assigns the decorated function onto an existing
object under the function's own name:
Usage:
The appeal is that the definition reads like a normal
def, with signature,annotations, decorators and docstring all in the usual places, instead of the flatter
setattr(MyClass, "func", func)tacked on afterwards, or aMyClass.func = funcassignment that sits visually far from the body it names.
I'd like to know whether there is appetite for something like this in
functools(where decorators live) or
types(where the object-model helpers live), or whetherthis is firmly PyPI/recipe territory.
Motivation
Cases where the class body isn't available or isn't the right place:
flag is set, or a backend-specific implementation chosen at import time.
Other languages carve out dedicated syntax for the general shape (C# extension
methods, Kotlin extension functions, Swift
extension, Ruby's open classes andrefinements), so the underlying need isn't unusual. Python can already do it; the
question is only whether the spelling is worth standardising.
Prior art / what exists today
setattr(MyClass, "func", func): already one line, already explicit.MyClass.func = func: same thing, fewer strings.functools.singledispatch(...).registeris a stdlib decorator with a closely relatedshape (decorate a function, register it against a type, keep the definition readable),
though it populates a dispatch table rather than mutating the class.
unittest.mock.patch.objectandpytest'smonkeypatch.setattrcover thetemporary/test-scoped version of this.
forbiddenfruitgoes furtherand patches built-in types through the C API.
This has also come up before on this forum under the heading of "extension methods",
and the
def obj.method(): ...syntax that the comment in my original snippet gesturesat is a much larger proposal than the decorator itself. I'm deliberately scoping this to
the library-level helper. (If someone can link the earlier threads, I'll edit them in.)
Known problems
I'd rather list these myself than have them listed for me. Measured on CPython 3.14.7:
Static analysis breaks. This is the big one. Both mypy and pyright reject the
usage example:
There is no way in the type system today to express "this decorator injects a member
into that class."
typing.dataclass_transformis a precedent for teaching checkersabout a decorator's effects, but nothing analogous exists here. Blessing a pattern in
the stdlib that every type checker flags seems like a hard sell, and realistically
this idea needs a typing story before it needs a
functoolsslot.Zero-argument
super()has no__class__cell. A function compiled outside aclass body never gets one, so out of the box:
This one is fixable from the decorator, but only by rebuilding the code object:
append
__class__toco_freevars, prepend theCOPY_FREE_VARSinstruction theprologue is missing, shift
co_exceptiontableandco_linetableby one code unitto match, rewrite any
LOAD_GLOBAL __class__into aLOAD_DEREF, then rebuild thefunction with an extra closure cell. I have that working on 3.14 across generators,
coroutines, pre-existing closures, try/except bodies and multi-level MROs, with
tracebacks and
co_lines()intact.I think this cuts in favour of the proposal rather than against it. Anyone who wants
this pattern today either performs bytecode surgery or tells users to write
super(MyClass, self)by hand. Inside CPython neither is necessary, since the cellcan simply be set. A helper that can only be implemented correctly in userland by
rewriting bytecode is a reasonable candidate for living where the bytecode is
defined.
__qualname__is wrong. It staysfunc, notMyClass.func, sorepr(),tracebacks and
picklesee the unattached name.__set_name__is never called, becausesetattrafter class creation doesn'trun it. Descriptors attached this way behave differently from ones in the class body.
Built-in and extension types are rejected with
TypeError: cannot set '...' attribute of immutable type 'int', which rules out the use case people most oftenwant extension methods for.
Lambdas silently attach as
<lambda>, since the name comes from__name__.Instances don't get a bound method.
setfunc(instance)stores a plain function,so
selfis not passed and the call raisesTypeError: missing 1 required positional argument. Doing it correctly needstypes.MethodType.Returning
Nonerebinds the module-level name toNone. That keeps thenamespace tidy in one sense, but it means decorators can't be stacked above it, and
linters see a name assigned to
Noneand then never used.If it were to go in, what should it look like?
Points 3, 6, 7 and 8 are fixable in the helper:
That is, an explicit
name=override, a__qualname__fixup, binding viatypes.MethodTypewhen the target is an instance, and returning the functionunchanged. Point 2 is reachable only through code object surgery, as described above.
Points 1, 4 and 5 are not fixable at this level at all.
What I'm asking
MyClass.func = funclarge enough to clear the stdlibbar for a three-line helper?
sink it outright?
super()argument move anyone? A stdlib implementation gets the__class__cell for free, which no third-party version can say.if the typing gap is closed first?
All reactions