-
-
Notifications
You must be signed in to change notification settings - Fork 366
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
Class variables #220
Comments
Somehow this doesn't sit right with me for reasons difficult to articulate before my brain processes this a little more. A class variable wouldn't be part of |
Well that’s the big question how to actually treat them. You can overwrite class variables in Python too and then they become instance variables: >>> class C:
... x = 42
>>> i = C()
>>> i.x = 23
>>> C.x
42
>>> i.x
23 Maybe all I need is an option that pushes the value on the class and otherwise works as usual? class C:
x = attr.ib(default=42, class_var=True) which would be equivalent to: class C:
x = 42 but otherwise it’d be a regular attrs attribute? I like this one better indeed. 🤔 |
Ah, I see. This technique of using a class variable as a default fallback for an unset instance variable is interesting; I have to admit I only learned of it a few months ago. I use class variable in different ways so I wasn't sure what use case you had in mind, this clears it up. This is a way of conserving memory, right? Instead of a million instance dicts having a key set, the key is only in the class dict, but it can be overridden on a particular instance if needed. The first thing that comes to mind is slot classes can't really support this, and they are generally the more memory-efficient option. Of course if your class has a hundred default fields, doing it this way is going to be more efficient than a slot class with a hundred slots, so it's worth considering. These attributes must have a default, the class must be non-slots, and they would be handled differently in two ways:
All the other generated methods can continue using Since these attributes must have defaults, what about this for syntax:
(if not, your suggestion with |
It seems like overloading For example, if |
Just quickly:
However they're still pretty special so I tend to an own attr.ib_class still. |
This is already deprecated behavior.
It'll work. This is the equivalent of what is being proposed:
Then you get:
with |
OK, y'all in deeper in the black magic that I've normally gone, so my assumptions are off. Uh… thanks for doing that do I don't have to. :-) |
There's a bit you left out in the code you say this is equivalent to, which is what happens when you say: >>> c = C()
>>> c.x = "some value" I'm assuming "magic, duh", but noting it for completeness. |
Oh, I guess for more complete completeness: C.x = "another value" Which I assume is out of the |
Well Hynek kinda explained it in #220 (comment). This is just normal Python, feel free to copy/paste the code, play around with it and discover exactly how it works ;) |
@Tinche deprecation period ends 2017-08-30…can’t wait! Originally, I wanted to get 17.3 out asap but I think I'll wait for this to pass. Guess I’ll be releasing out of South Africa… :) |
If Where can I catch up on the discussion surrounding this proposal? |
Which discussion exactly? The removal of Attribute will happen before the next release FWIW. And yep, we probably could just pass immutable default values thru into class variables? I wonder if that’d have any weird side-effects, because otherwise it'd be a nice speed boost. (P.S. as a general disclaimer: I'll be off the grid Sep 1–17 plus on vacation until Sep 24. Right now I’m having hell at work due to preparations for that so I’m less responsive than I’d like to be and it’ll be worse in the near future. OTOH I hope I’ll get a bunch of FOSS work done afterwards because I’ll be a kind of retreat until Oct 18) |
I was just wondering if there was a publicly-viewable discussion (or code) around the removal of Attribute access from attrs classes, in favor of the new (descriptor-based?) design. It sounds like the plan has been fairly well fleshed out, so I thought I'd do you the favor of getting caught up before jumping in with suggestions that you've already thought of. |
It happened pretty much exactly a year ago by that decision is orthogonal to this ticket. It was just a bad idea to attach more than necessary to classes. There's also no real plans around descriptors. Current plan is to just delete the attr.ibs and leave the class clean (or for slots classes: not attaching anything in the first place.) |
So can someone explain why we should include class variables in the attrs system? |
Quoting Hynek from the other thread:
Yes, but this still doesn't explain why the class vars should be attr()ibuted. They work quite well without it. |
You’re definitely on to something. We could argue whether or not attrs should support it at all, however I think you’re making a good case to make it opt-in. IOW, the API would go more like: @attr.s(auto_attribs)
class C:
x: ClassVar[int] = attr.ib(default=42) because collecting it would be impossible to prevent undesired class vars to be collected without setting it to The important point here is that that would make #220 not a blocker for 17.3 anymore. |
If it’s just a free default, you def want it as part of your reprs and cmps because they can vary wildly. |
I don't think ClassVar and "free" defaults should be mixed. If you want this, why not just have attrs set the default value directly in the class? |
Or at least give the user the option to do so. |
To my understanding (and in my opinion), something annotated with |
That was kind of the original plan of this ticket. :) But yeah, I think we’re in agreement, that this is not a blocker for 17.3 in any way. phew |
Are we in agreement about how to handle ClassVars with |
I think so. :) |
We now have It would be awesome to have this clean API: @attr.s(auto_attribs=True)
class MyClass(object):
class_var: ClassVar[int]
instance_var: str Will result in:
|
Isn't that exactly what happens? You just have to assign it a value: In [7]: @attr.s(auto_attribs=True)
...: class MyClass(object):
...: class_var: ClassVar[int] = 5
...:
...: instance_var: str
...:
In [8]: MyClass.class_var
Out[8]: 5
In [9]: MyClass("hi").class_var
Out[9]: 5 Fields decorated as ClassVar are ignored. |
@hynek Related to this, would it make sense for the following to produce an error?
Currently (in attrs 19.1.0) I think it would make sense for |
There's two things that's speak against that:
|
would a warning when a class variable is not annotated as such be in the game of ensuring people correctly annotate intent |
Yeah, that sounds fair. |
Ah, I see. The suggested warning sounds fine then. :) Maybe it would also be good to clarify the current behavior somehow in the API documentation? The section on
It says that you must annotate every field, which is why I was puzzled to see that it's fine to leave something unannotated, and that it will give you a class variable instead of an instance variable with a default value. I realize that this is how you normally initialize a class variable in a Python class declaration, but it didn't occur to me that that might be the intended behavior here. I confess that I also didn't understand the last line correctly at first. Maybe something like "Attributes annotated as typing.ClassVar are ignored (that is, they will become class variables)" could be considered? I know the typing.ClassVar should be a hint, but I can't be the only dense person out there. ;) |
I have changed the wording to:
I hope that clarifies it. |
@hynek Hi! :-) Can you please summarize the correct way to set class variables? I need a normal python shared class variable which is totally ignored by attrs. Is this right? Will attrs understand that from typing import ClassVar, Optional
import attr
import numpy as np
@attr.s(slots=True)
class ScreenGrabber:
connection: ClassVar[Optional[int]] = None
img = attr.ib(type=np.ndarray)
def getConnection(self) -> int:
if ScreenGrabber.connection is None:
ScreenGrabber.connection = some stuff
return ScreenGrabber.connection |
It should – does it not work? |
@hynek I don't know... How can I check? I'm looking at Also: I tried: Is this conclusive proof that the "connection" variable is totally ignored by attr and isn't stored inside the individual instances? Is it also proof that |
Also, if I tried So then I did the final test: from typing import ClassVar, Optional
import attr
import numpy as np
@attr.s(slots=True)
class ScreenGrabber:
connection: ClassVar[Optional[int]] = None
img = attr.ib(type=np.ndarray)
def getConnection(self) -> int:
if ScreenGrabber.connection is None:
ScreenGrabber.connection = 2
return ScreenGrabber.connection
x = ScreenGrabber(np.zeros([1,1,3], np.uint8))
print(x.connection)
print(x.getConnection())
print(x.connection)
x.connection = 3 Result:
So it seems like it is indeed only stored as a single, shared class variable, with no per-instance value. Can I safely put this worry to rest now? :D |
I just noticed that I did all my tests with just Anyway, I retried the test as follows (with a few extra print-statements included) and got the exact same results as described above... And I tried the below test with from typing import ClassVar, Optional
import attr
import numpy as np
import inspect
@attr.s(slots=True, auto_attribs=True)
class ScreenGrabber:
connection: ClassVar[Optional[int]] = None
img: np.ndarray = attr.ib()
def getConnection(self) -> int:
if ScreenGrabber.connection is None:
ScreenGrabber.connection = 2
return ScreenGrabber.connection
print(inspect.signature(ScreenGrabber.__init__))
x = ScreenGrabber(np.zeros([1,1,3], np.uint8))
print(x)
print(x.__slots__)
print(x.connection)
print(x.getConnection())
print(x.connection)
x.connection = 3 Result:
PS: It's clear that attrs reads the type annotations even when |
The point of auto_attribs=True is to not have to say
So it only looks like it's reading it, but in truth it's ignoring them. |
@euresti Ah yes I see the thing now! Example: from typing import ClassVar
import inspect
import attr
@attr.s(slots=True, auto_attribs=False)
class Example:
clsvar: ClassVar[int] = 15
x: int
y: str
z: bool = True
a = attr.ib(type=int)
print(inspect.signature(Example.__init__)) Result:
And with from typing import ClassVar
import inspect
import attr
@attr.s(slots=True, auto_attribs=True)
class Example:
clsvar: ClassVar[int] = 15
x: int
y: str
z: bool
a: int = attr.ib(kw_only=True)
print(inspect.signature(Example.__init__)) Result:
|
Well, that answers it... the And if |
Okay, here's the actual attrs code: Lines 324 to 342 in 754fae0
Lines 274 to 282 in 754fae0
Line 37 in 754fae0
So yeah, if any annotation starts with those string sequences, the attribute is totally ignored by the attrs parser. Edit: But this "is class var? ignore" is only executed if |
Let me point out that the current behavior works well for me (with no warning for non-annotated class variables). That's exactly what I want. Having to annotate a class-level constant is inelegant - the value is there and it's in all caps, so the intent is very clear to the humans. For instance:
Yeah, it wouldn't be terrible if I had to annotate SUFFIX with ClassVar, but it wouldn't look good or make the code clearer. And if I had lots of constants in there, it would be quite annoying. |
@micklat Point taken about your preference, bur I don't agree that ALL CAPS implies class variable. Nothing about |
@wsanchez It's a bit more than my personal preference. ALL_CAPS_WITH_UNDERSCORES is the idiomatic way to write constants in the python community, as PEP8 attests. Since a reader accustomed to idiomatic code would understand that SUFFIX is a constant, it would imply that it is a class-level constant, since there's no point in replicating a binding for a constant into each of the instances. |
@micklat: Constant != Class Variable. Perhaps it's obvious to you in this case, but I disagree that it's very clear to others. In any case, not all class variables are constants, and not all constants are named in all caps; PEP8 is convention, not law. But while we're being Pythonic: explicit is better than implicit. |
I would like to see this implemented, now I am using this wasteful use:
because I do not have this
or similar and I am not sure next is explicit or regular enough
with attr.ib_class or similar I suppose we could have same validators as attr.ib and more. |
you can have: @attrs.define
class C:
x: ClassVar[int] = 543 which looks terse and idiomatic enough to me. |
Lol seeing this thread in my inbox provided me with an amusing history retrospective. @hynek How about we also make final fields classvars? @define
class C:
x: Final = 50 There's no way otherwise to have a |
if you say it makes sense… but it would be |
Yes, but Mypy and Pyright are smart enough. (The inferred type will actually be We'll need to change the Mypy plugin (I can do that), as this would technically be an incompatible change. If there is any of this in the wild, I'd imagine people just use |
Every now and then I stumble into wanting to have class variables that are part of everything (e.g. repr) except
__init__
.We can do:
but that seems wasteful.
It would be nice to have something like:
that just promotes the value into
x
in class level and adds an Attribute to attrs's bookkeeping.Opinions?
The text was updated successfully, but these errors were encountered: