-
-
Notifications
You must be signed in to change notification settings - Fork 367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Quo vadis re: classes #223
Comments
The problem with tacking methods onto an existing class is we can't add "slotness" to an existing class, right? I'm more interested in the user-facing API than the internals; there's going to be filth under the hood either way. All other things being the same, I'd prefer to keep the dirt on our side, and let users frolic in their ignorance of what we're doing in their name. I'm assuming adding slotness with a metaclass would require users to actually specify that metaclass? Like this:
Personally I find the current approach of
and it's nice and elegant. Also does using a metaclass for slotness means users can't use another metaclass? |
I absolutely agree on the filth being on our side. My fear is just that the filth might just…break eventually? It seems like playing with internals that may also have other side-effects like confusing PyPy's JIT etc. :| I really don’t know. 😖 |
So I tend to go with “cheating the interpreters and return new classes like slots=True” again. Unless someone voices disagreement (@glyph?) we’ll ship the hacks in 17.3.0 and eventually move there in all cases. |
I know this fear very well :) Mentally I counter it by saying to myself "we're a volunteer open source project, we're doing the best we can, if we do something wrong or paint ourselves into a corner with a wrong decision oh well, we'll fix it and try again" :) 👍 for returning new classes. I can put together a fix for #102 when I have some time and we can take it from there. |
You've got any timeline in mind? Seems like your plate keeps getting fuller. :) |
Since this is settled now, we have to figure out a road map on how to make it “always new class”. That’s a breaking change re: at least the hashing thingie, so we’ll have to be careful. |
What is the plan for being careful? |
What I meant is that we won't do the bandaid thing again and try to find a way to do some kind of deprecation cycle. Maybe make it an attr.s Option so we can raise a DeprecationWarning? |
So we’ve just ran into more problems re metaclasses and I’m afraid taking the “always create new class” is just not practical. We’d break wayyy to much stuff on the way. So the question is: what can be done instead?
|
sanely using meta-classes would require to inherit from a class that is a instance of them |
The original problem with metaclasses is that they aren't composable, right? Our current problem is replacing the class won't allow our users to use the metaclass they want. But switching attrs to a metaclass approach again won't allow our users to use the metaclass they want :) |
true - but when using attrs, many things you do with metaclasses are displaced anyway for example if you use a sqlalchemy declarative metaclass, you dont want any attrs behaviour so i dont see enabling metaclass based inheritance for stuff like correct |
Unfortunately, I think the approach of recreating the class is fundamentally incompatible with metaclasses. We might make it work in some cases but not in all cases. So maybe we can brainstorm a better API for slots, and I can replace our black magic with different black magic so we set slots before the class is finalized. Then we would never have to replace the class. Should we open a new issue to talk about different slots APIs? |
I think this issue is fine since we don’t have anything else open. We can open something new once something concrete starts crystallizing. |
well, metaclasses are fundamentally a mechanism to make the need to recreate the class to begin with go away - since the initial instantiation of the class instance would already do the work, where attrs currently has to break the world by moving the actual class away from under its methods |
What if we could make this work:
Option two:
But honestly the dunder slots just looks ugly to me, like an implementation detail leaking out. |
Option one looks better but I could live with both I guess? |
I think you could support
Obviously the code that executes on 'return' here is just an example -- the actual version would need to collect the attribute names to put into |
I'd like to use @oremanj in addition to being interpreter-specific, doing that sort of thing makes PyPy really slow. Maybe it'll only make it slow at import time, but importing my codebase at work already takes a good couple of seconds and I'd rather not stretch it out even more. |
I think maybe a useful question here is, when are limitations that exist only on some interpreters worse than limitations that exist on all interpreters? (This isn't intended to be a straw man -- there's a maintenance cost to having code that's exercised only on some interpreters -- but since Brython doesn't really support Performance is definitely an open question. (At a rough glance on a couple small inputs, the use of the trace hook looked better on CPython than attrs' current approach.) My understanding of PyPy is that most of the performance impact of getframe/settrace/etc comes from disabling the JIT, but I wouldn't expect the JIT to help much with class creation anyway, so I'd be surprised if the effect is as dramatic as you're worried about? Of course there's no way to know for sure without fully implementing it and timing it. |
I'm definitely OK with the answer for Brython being "implement a new backend". I wish I knew how to set up CI for something like attrs that could meaningfully verify it against some JS / frontend runtimes. (Brython just being an example here; maybe VOC is a better one.) The main thing is that I want to avoid jumbling up a gross interpreter hack in the interface to slotsification, i.e. an un-adorned function call at class scope being able to identify the defining class, necessitating stack-walking and other hacks to make it work, as opposed to something abstract in the decorator. |
Thanks, that's useful feedback and makes sense when put that way. I suspect similar magic can be performed from the decorator, since the decorator gets evaluated before the class definition, but I haven't looked in detail yet. |
I brainstormed some more about where we could insert the magic to make this work. I think the most promising options involve taking advantage of the fact that if you pass The easiest and least magical approach would require the user to put something in their class's bases list explicitly. This only uses stuff in the official Python language definition so it should theoretically be portable to all interpreters that implement py3. Saying
which could support an alias
in order to be less daunting for users who don't want to think about the word "metaclass". (The purest version of this latter base class form would only work when none of C's other bases have nontrivial metaclasses, though support could be added for GenericMeta and ABCMeta without much trouble. It might even be possible to support all metaclasses on 3.7+ using More complex example: if you had the non-slotted attrs class
and you wanted to make it slotted with no weakref slot, you would write something like
Other spelling options: More magic: Once the above exists, there's the option of additionally hooking
The trace hook could be used instead of hooking My gut feeling having written all this out is that the "more magic" approaches that could support unmodified |
Oof there's a lot to unpack here. :) So first of all, Brython is not gonna hold us back from having a better experience on the most used Python. We do have a portable solution right now and having a good experience on Py3+ should be worth some extra code. That said, I think the first approach won't work because Ultimately I'm afraid we won't get around metaclasses unless CPython adds programmatic slots support (which they kinda thought about). Having to subclass is obviously my least favorite approach; ideally everything would be done in the decorator. I wonder if something could be done with a custom P.S. we absolutely won't be rewriting bytecode but I appreciate the level of evilness. 😂 |
I think we'll get the necessary knobs from CPython sooner or later so closing this. |
Time for more existential questions.
Currently attrs has involuntarily two ways of attaching its methods. Both come with certain problems and there are (or are suspected) remedies.
We should really discuss this once and for all and transition towards one approach. Here’s what I’ve got:
We tack our methods to the existing class body.
Default behavior.
Problems
__eq__
and__hash__
(cf. ) is a real problem so far.Remedies
We create a new class in
@attr.s
and return it instead of the originalHappens if
slots=True
. This has the upside that we create a brand new class that doesn’t change so no edge cases are to be taken care of.However, the user gets a different class, which comes with its own set of problems.
Problems
super()
doesn’t work. TypeError when using super() and slots=True #102Solutions
At this point, I find the confusingly broken
super()
worse than the default approach so I would prefer if we found a way to cheat__slots__
into existing classes and I’m not afraid of using C or meta programming to get there.If others have more problems/solutions, I’ll happily add them.
The text was updated successfully, but these errors were encountered: