#🔒 Instance-less Class Design

56 messages · Page 1 of 1 (latest)

white mirage
#

Hi there,

I'm experimenting with a type-only/instanceless design for my library and I would love to get your opinions on such design.

The Goal is:
Replace command groups with a pure class-based approach (more data structure friendly), using metaclasses, no instance needed.

Code available at: https://paste.pythondiscord.com/OBSA

It allows the following synthax: (the same than the instance based one)

GroupModes["ENGINE_SPEED"]
Mode01[0x0C]

# Access modes by enum or int
GroupModes[Mode.REQUEST]  # Returns Mode01 class
GroupModes[1]

# Iteration works directly on classes
for cmd in GroupModes:
    print(cmd)

# Length and containment checks
len(Mode01)
"ENGINE_SPEED" in Mode01
runic micaBOT
#

@white mirage

Python help channel opened

Remember to:

  • Ask your Python question, not if you can ask or if there's an expert who can help.
  • Show a code sample as text (rather than a screenshot) and the error message, if you've got one.
  • Explain what you expect to happen and what actually happens.

:warning: Do not pip install anything that isn't related to your question, especially if asked to over DMs.

white mirage
#

For reference, the original idea is to permit different ways for the user to access the same object
Which leads to the following syntax: https://py-obdii.readthedocs.io/en/latest/usage.html#commands

from obdii import commands

# 1. Using a command directly
commands.ENGINE_SPEED

# 2. Using the command name
commands["ENGINE_SPEED"]

# 3. Using the Mode and PID
commands[0x01][0x0C]

# These three lines are equivalent
# and all return the same command object:
# <Command Mode.REQUEST 0C ENGINE_SPEED []>
thin furnace
# white mirage For reference, the original idea is to permit different ways for the user to acc...

I'm experimenting with a type-only/instanceless design for my library and I would love to get your opinions on such design.
Why don't you want to expose a public instance? And looking at your code you would still need to make an instance

And why are you using metaclasses? you can acheive those syntax by simply by overloading __setitem__

Outside of implementation, I don't think this is a good design choice. If anything it only adds unnecessary complexity for very little benefit and ambiguity. I'd recommend you to stick to one way to access the attribute unless you have an actual problem it solves.

Generally speaking, if you're using metaclasses it means you're either doing something horribly wrong or you have an extremely rare edge case where it is inevitable

white mirage
# thin furnace > I'm experimenting with a type-only/instanceless design for my library and I wo...

I understand this design may sounds overengineered

Why don't you want to expose a public instance
It's just a feeling that without instance would be cleaner, it should be also way more efficient, because each Mode0X have a lot of commands instances defined to class variables, quick example here

For the instanceless design, and to reproduce the syntax I'm using with instances, RAW_CLASS[str/int/Mode] I must use metaclass(es)

metaclasses [...] extremely rare edge case where it is inevitable
I'm using a Singleton metaclass for unique instancied objects (such as MISSING), I'm also considering using it for my Mode0X if I keep the instance design

#

In the end what really changes is this, and this, that instanciate my command group and modes

runic micaBOT
#

obdii/modes/basetypes.py lines 29 to 35

MODE_REGISTRY: Dict[int, ModesType] = {
    0x01: Mode01(),
    0x02: Mode02(),
    0x03: Mode03(),
    0x04: Mode04(),
    0x09: Mode09(),
}```
`obdii/modes/__init__.py` lines 13 to 14
```py
at_commands = ModeAT()
commands = GroupModes()```
thin furnace
# white mirage I understand this design may sounds overengineered > Why don't you want to expo...

it should be also way more efficient, because each Mode0X have a lot of commands instances defined to class variables, quick example here
Tbh it wouldn't make any difference, class attributes are evalutated only once, when the class is first defined. Creating any number of instances wouldn't "redefine" those class attributes or anything

For the instanceless design, and to reproduce the syntax I'm using with instances, RAW_CLASS[str/int/Mode] I must use metaclass(es)
Mb, seems like you can't do overloading for it.

Also why not let the end use do the instantiation? It's a fairly common standard atleast

white mirage
#

It's a tough field, you can go quite in depth, the lib is aiming to simplify everything, start slowly without much knowledge, and the further you go, the more complex it can be

The lib provides a big set of predefined Commands, that allows the user to request data and parse them to something they'll use
We group and wrap all Commands objects under a public commands
From there the user can just query their vehicle with commands.VEHICLE_SPEED and they'll get the value they're looking for

#

stick to one way to access the attribute unless you have an actual problem it solves
Regarding this, in specific scenarios, it may be very handy to be able to access our sets of predefined commands by string, so that's why we support commands["VEHICLE_SPEED"]

pseudo lichen
thin furnace
pseudo lichen
#

as a consequence (and because it is already quite metaprogramming-y), your code probably won't play nicely with static type-checkers

white mirage
#

I understand, and even got running in few Typing issues with __contains__ for e.g., I was able to solve it afterwards, but with a more generic type than the one I wanted to use

pseudo lichen
#

that isn't to say the design is inherently bad, it is just something to consider. Users of static type checkers might not appreciate having to suppress lots of warnings

stuck mesa
#

well it's not really "instance-less", is it?
You are making instances of the metaclass.

white mirage
#

I did not thought about it, so maybe not 100% instance-less

pseudo lichen
#

that's sort of a pedantic point though...

stuck mesa
#

what I am saying is that it's nearly the same as just using "normal" instances

white mirage
stuck mesa
#

what is an example of such a problem?

thin furnace
#

youre finding a problem to solve rather than an actual problem to solve lol

white mirage
#

The goal of a lib isn't it to provide a clever, handy and accessible interface for user to use ?

stuck mesa
#

btw in 3.10 you can uses | for unions

white mirage
stuck mesa
white mirage
stuck mesa
stuck mesa
#

3.10 is the lowest non-EOL version at the moment

thin furnace
stuck mesa
white mirage
#

@thin furnace Why would I standardize this kind of approach ? While I can simply implement (and I already did) a simple alternative

stuck mesa
#

your "simple alternative" doesn't need to be on a class tho, it can just as easily be done on an instance of that class (which is not a metaclass)

white mirage
#

Yes, and that's exactly what is on prod rn

stuck mesa
#

so why switch to something esoteric?

white mirage
#

My idea with type only is just an experiment

#

I made this post to get feedback on wether it could actually be a great implementation

#

That'll replace the instantiated one

#

This idea of making everything instance-less came when I found an alternative for my protocols registration (in these changes)
And from that point I've been experimenting to make it real

crisp brook
#

@white mirage have you seen Io's object system? Basically the opposite of your idea where instead of classes you duplicate and mutate template objects

white mirage
crisp brook
#

For example i might copy and mutate a variant of Number to add complex numbers

#

It's quite cool but literally only exists in that one language. Try porting it

white mirage
#

I get it, might it be expensive ? because every time you derive, the parent instance still lives in memory

crisp brook
#

Well, I think it had to do with some other issues of the language that made it difficult to be used practically

#

But uh it shouldn't be too expensive

#

Considering that Python has a whole bunch of classes in namespace already

white mirage
#

It's quite cool but literally only exists in that one language
However, it shouldn't be too complex to create this kind of behavior in Python with a baseclass or a metadata class

white mirage
# crisp brook It's quite cool but literally only exists in that one language. Try porting it
class ImmutablePrototype:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        object.__setattr__(self, '_frozen', True)

    def __setattr__(self, name, value):
        if getattr(self, '_frozen', False):
            raise AttributeError(f"'{self.__class__.__name__}' object is immutable and cannot be modified.")
        object.__setattr__(self, name, value)

    def __delattr__(self, name):
        if getattr(self, '_frozen', False):
            raise AttributeError(f"'{self.__class__.__name__}' object is immutable and its attributes cannot be deleted.")
        object.__delattr__(self, name)

    def clone(self, **new_attributes):
        cloned_attrs = {k: v for k, v in self.__dict__.items() if not k.startswith('_')}

        cloned_attrs.update(new_attributes)

        new_instance = self.__class__(**cloned_attrs)
        return new_instance
# Base object
Object = ImmutablePrototype()

# Derived Number definition
def number_add(self, other):
    return self.value + other

Number = Object.clone(
    value=0, 
    type_name='Number',
    add=number_add
)
crisp brook
#

Awesome!

runic micaBOT
#
Python help channel closed for inactivity

This help channel has been closed. Feel free to create a new post in #1035199133436354600. To maximize your chances of getting a response, check out this guide on asking good questions.