1

Consider the code below:

class Color:
    RED: "Color"
    GREEN: "Color"
    BLUE: "Color"
    WHITE: "Color"
    BLACK: "Color"

    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b


Color.RED = Color(255, 0, 0)
Color.GREEN = Color(0, 255, 0)
Color.BLUE = Color(0, 0, 255)
Color.WHITE = Color(255, 255, 255)
Color.BLACK = Color(0, 0, 0)

Here, I am creating a few color definitions, which can be accessed from the Color class, as well as creating custom color instances. However, it feels a little repetitive needing to declare then instantiate the values in two different places in my file. Optimally, I would do the following, but because it is self-referencing, I get a NameError.

class Color:
    RED = Color(255, 0, 0)
    GREEN = Color(0, 255, 0)
    BLUE = Color(0, 0, 255)
    WHITE = Color(255, 255, 255)
    BLACK = Color(0, 0, 0)

    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b

Is there a way for me to cleanly define my preset colors in one place whilst maintaining type safety and readability, or is the first example already as good as it's going to get?

Miguel Guthridge
  • 1,444
  • 10
  • 27
  • Type hinting is laying out explicitly what the expected types are, and in the first place you define a slot, the second place you set the actual value, so it isn't repeated exactly, though that's what it looks like in Python. – metatoaster Jan 04 '23 at 04:35
  • For any other kind of value, I would be able to declare and define it in the same place. The only issue is that if it's the same type, it self-references and I get an error. I'm looking for a way I can declare and define it in the same place without getting an error. – Miguel Guthridge Jan 04 '23 at 04:38
  • Nope, entirely not possible because `Color` does not exist while you are defining it, so you will not be able to construct instances of itself while inside its class scope – metatoaster Jan 04 '23 at 04:40
  • Are you sure there isn't some kind of magic I can do with `property` or anything? – Miguel Guthridge Jan 04 '23 at 04:40
  • There is no magic, by definition you cannot use a thing before a thing ever existed. See [thread](https://stackoverflow.com/questions/40244413/python-static-class-attribute-of-the-class-itself). – metatoaster Jan 04 '23 at 04:42
  • The idea of `property`s in Python is that you can create functions that behave like attributes. You wouldn't need to create the thing before it exists - you'd just need to instantiate it when someone used the property. – Miguel Guthridge Jan 04 '23 at 04:49
  • Yes but that is an entirely different concept, you wouldn't be trying to assign instances of `Color` to `Color` before `Color` existed (which is what you tried to do in the question). Moreover `@property` only works for instances of a class, so by definition the class _must exist_ before any instances of it may be constructed such that its instance properties may then be used. You might be thinking of something like `classproperty`, but this is a more involved concept - see [thread](https://stackoverflow.com/questions/128573/) but it doesn't really address your issue. – metatoaster Jan 04 '23 at 04:53
  • 2
    "It doesn't really address your issue" simply because you would be writing a lot more code, effectively building a "`classproperty`" method for each of the colors and to make the code formatting PEP-8 compliant you will need the decorator also need two lines for the method and return, plus an empty line, below, for each of these methods. Tell me how this is less work than what you had in the first place. – metatoaster Jan 04 '23 at 04:56
  • e.g. 1 `@classproperty` 2 `def RED(cls) -> "Color":` 3 `return Color(255, 0, 0)` – metatoaster Jan 04 '23 at 05:01
  • "However, it feels a little repetitive needing to declare then instantiate the values in two different places in my file." Then... don't declare them? There is **not actually any such thing** as declaration, anyway. Code like `RED: "Color"` has **no effect** outside of setting some metadata and being processed by third-party type checkers. It puts information into the `Color.__annotations__`, but it **does not** cause `Color.RED` to be defined in any way, shape or form, and **is not** necessary in order to be able to do `Color.RED = Color(...)` later. – Karl Knechtel Jan 04 '23 at 05:18
  • @KarlKnechtel without declaring them, I don't get editor suggestions (a must for the project I'm working on), and my type checking CI fails. – Miguel Guthridge Jan 04 '23 at 05:22
  • Does `Color.RED : Color = Color(255, 0, 0)`, without a corresponding line inside the class, satisfy your tools? – Karl Knechtel Jan 04 '23 at 05:23
  • Sadly it does not – Miguel Guthridge Jan 04 '23 at 05:24

1 Answers1

3

I think there are two approaches you could use for this. You could define a decorator for a property that attaches to the class:

class classproperty(property):
    def __get__(self, _, cls):
        return self.fget(cls)

class Color:
    @classproperty
    def RED(cls) -> "Color":
        return cls(255,0,0)
    @classproperty
    def GREEN(cls) -> "Color":
        return cls(0,255,0)
    @classproperty
    def BLUE(cls) -> "Color":
        return cls(0,0,255)
    @classproperty
    def WHITE(cls) -> "Color":
        return cls(255,255,255)
    @classproperty
    def BLACK(cls) -> "Color":
        return cls(0,0,0)

    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b

or you could use a metaclass that builds the properties in when the 'object' that represents the class type is being formed:

class __Color__metaclass__(type):
    @property
    def RED(cls) -> "Color":
        return cls(255,0,0)
    @property
    def GREEN(cls) -> "Color":
        return cls(0,255,0)
    @property
    def BLUE(cls) -> "Color":
        return cls(0,0,255)
    @property
    def WHITE(cls) -> "Color":
        return cls(255,255,255)
    @property
    def BLACK(cls) -> "Color":
        return cls(0,0,0)

class Color(metaclass=__Color__metaclass__):
    def __init__(self, r: int, g: int, b: int) -> None:
        self.r = r
        self.g = g
        self.b = b

I personally prefer the metaclass since for some reason my IDE can work out the autocompletions from that and yet can't on my decorated "class properties".

ricardkelly
  • 2,003
  • 1
  • 1
  • 18
  • Thanks for this solution! Unfortunately, Mypy and Pylance both `reveal_type(Color.RED)` incorrectly on both solutions: Revealed type is "Any" (mypy); Type of "c" is "classproperty" (Pylance). Similar issues are present for the second solution. I wonder if you could use `property` as a function rather than as a decorator to create a custom property type which does the construction under the hood (which would be even cleaner than the above). – Miguel Guthridge Jan 05 '23 at 05:38
  • Thinking now it has to be the metaclass. Once I added the annotations to the metaclass, `reveal_type(Color.RED)` says `Color`. – ricardkelly Jan 05 '23 at 06:25
  • Which IDE are you using, out of curiosity. I have Pylance with VS Code, and use Mypy in CI, neither of which work correctly. – Miguel Guthridge Jan 05 '23 at 10:55
  • VS Code 1.74.2 ; Pylance v2023.1.10. What do you get when you hover `Color.RED` in your IDE, and what does `reveal_type(Color.RED)` give in mypy? – ricardkelly Jan 05 '23 at 16:21
  • Ah - found something in how I was (incorrectly) testing the classproperty version, and that's not going to work for your use case. mypy can't work out what we are doing with the classproperty, so if we type hint then it thinks we are trying to call something that isn't callable, and if we don't then it decides the type is 'Any'. It seems to understand the metaclass though on my system. Weird we get different results. – ricardkelly Jan 05 '23 at 17:04