0

Less object oriented snippet (1):

class Trickjump(TypedDict):
    id: int
    name: str
    difficulty: str
    ...

Dataclass snippet (2):

@dataclass
class Trickjump():
    id: IdAttr
    name: NameAttr
    difficulty: DifficultyAttr
    ...

My issue lies in the fact that I can't decide which approach would be better in my case. I need attributes that are dynamically creatable with a name and custom functionality, where the user gives me the attribute's name as a string and the value as a string. I want to then use the attribute's custom functionality with the given value to produce a result.

In the first code snippet, which I find is less object oriented, I could get the custom functionality of an attribute and use it with the value in the following way:

Attributes.get("attr_name").sort_key(value)

While in the second code snippet I would have to dynamically create classes with type(...) to act as attributes and it would also directly hold the value, probably best by using metaclasses.

I think the first way is way less work, but I feel like the second option is something more advanced but possibly also a bit over the top. Which way do you think is the best for my scenario? Or is there another solution?

2
  • If you want dynamically generated types, you need to call type explicitly. If you want instances of Trickjump with attribute values provided by a user, which you choose depends on whether you want an object with attributes or a dict; it's a matter of interface preference, not correctness. Note that subclasses of TypedDict are primarily used as type hints, and a function like def foo(x: Trickjump): ... would accept {'id': 3, 'name': 'bob', 'difficulty': 'low'} just as well as Trickjump(3, 'bob', 'low'). Commented Feb 28 at 16:37
  • @chepner Yeah I knew all that already, I just kept it short to show my main issue being the decision between the two systems. Thanks for the explanation though! Commented Feb 28 at 17:27

1 Answer 1

2

Object orientation won't offer you good support for dynamic (runtime) created attributes.

but you can create an improve a class that will load the attributes it should support as data, and operate upon them.

A "typeddict" also will have much less value past the pre-running stage of type checking - which doesn't apply to user, runtime, generated content at all.

So, having a class that would getthe attributes needed as a dictionary - it can be a plain dictionary, mapping each attrite name to all the information you need for that attribute - from typing, to creation metadata, maybe formulas for computed output attributes, etc... and spills out usable Python objects - which don't need to have a class of their own, but can offer whatever interface you need to work with those attributes: constrain input types, create a corresponding table in a SQL database, etc...

Since attributes are data you may not want to interact with them using python code, as it would require typing in the attribute name in your source files: you will use the attributes by their string name - it is possible to do so with the getattr and setattr built-ins - but it is awkward compared to using the ["attrname"] syntax.

With this reasoning, what seems that will fit you better is a custom "MutableMapping" class - maybe with other ancillary classes, for dealing, for example, with the metadata - and a well written set of __getitem__, and __setitem__ types. It could even render itself into a Python dataclass Python source code, if you'd need to "solidify" one of these instances.

Say, something along:


from collections.abc import MutableMapping, Mapping, Sequence

class UserContent(MutableMapping):
    
    def __init__(self, name, spec, initial=None):
        self.spec = spec
        self.name = name
        self.data = {}
        if initial and isinstance(initial, Sequence):
            for key, value in zip(spec.keys(), initial):
                self[key] = value
        elif isinstance(initial, Mapping):
            self.data.update(initial)
    
    def __iter__(self):
        yield from self.data.keys()
        
    def __setitem__(self, key, value):
        if key not in self.spec or not isinstance(value, self.spec[key]):
            raise KeyError("...")
        self.data[key] = value
        
    def __getitem__(self, key):
        return self.data[key]
    
    def __delitem__(self, key):
        del self.data[key]
        
    def __call__(self, *args):
        """create s a new "instance" of this with the same specs, and new content"""
        if len(args) == 1 and isinstance(args[0], Mapping):
            initial = args[0]
        else:
            initial = args
        return type(self)(self.name, self.spec, initial)
        
    def iscomplete(self):
        # Assumes all keys in spec are required - returns
        # False if there are mssing attributes, True otherwise
        return not (set(self.data.keys()) - set(self.spec.keys()))
        
    def __dir__(self):
        return list(self.spec.keys())
    
    def __len__(self):
        return len(self.data)
    
    def __repr__(self):
        return f"<{self.name}({self.data})>"


TrickJump = UserContent("TrickJump", {"id": int, "name": str, "difficulty": int})

myjump = TrickJump(1, "Double Jump", 10)

You can enhance this as desired. For example "isinstance" won't work, because it expects the seconf argument to be an actual class -but you could just add a .isinstance method there and check if the testes instance has the same spec.

In the same way, the spec can be as sophisticated as desired. Also, pickling, copying, etc should work out of the box with that

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you very much for your insight! I think it defines my question and an appropriate answer better than my original question, because I had issues describing the actual issue and your clarification that attrs act as data summarizes it very well.
Btw why would mutable mapping be preferable over a simple subclass of dict or a typeddict for a specific collection of attributes?
@JoniKauf a TypeDict is just a plain dict as far as the Python runtime is concerned, it exists entirely for the purposes of type-hinting and static analysis tools (or third-party libraries that use those annotations, e.g. pydantic). Inheriting from dict is generally not great because you typically end up overriding every method ... for example, if you override __getitem__ you have to override .get if you want .get to have the same behavior, because internally the class isn't using __getitem__ in .get as you might expect. But it depends on the specifics of what you want.
Yes - those motives - but, a typed dicd could be made to work as the spec in this case (just pick the keys from self.spec.__annotations__.keys() instead). So you can use the nicer class syntax to declare attributes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.