jsonpython-3.xclassvariablesjsonpickle

It's a bad design to try to print classes' variable name and not value (eg. x.name print "name" instead of content of name)


The long title contain also a mini-exaple because I couldn't explain well what I'm trying to do. Nonethless, the similar questions windows led me to various implementation. But since I read multiple times that it's a bad design, I would like to ask if what I'm trying to do is a bad design rather asking how to do it. For this reason I will try to explain my use case with a minial functional code.

Suppose I have a two classes, each of them with their own parameters:

class MyClass1:
def __init__(self,param1=1,param2=2):
    self.param1=param1
    self.param2=param2
    

class MyClass2:
    def __init__(self,param3=3,param4=4):
        self.param3=param3
        self.param4=param4

I want to print param1...param4 as a string (i.e. "param1"..."param4") and not its value (i.e.=1...4).

Why? Two reasons in my case:

  1. I have a GUI where the user is asked to select one of of the class type (Myclass1, Myclass2) and then it's asked to insert the values for the parameters of that class. The GUI then must show the parameter names ("param1", "param2" if MyClass1 was chosen) as a label with the Entry Widget to get the value. Now, suppose the number of MyClass and parameter is very high, like 10 classes and 20 parameters per class. In order to minimize the written code and to make it flexible (add or remove parameters from classes without modifying the GUI code) I would like to cycle all the parameter of Myclass and for each of them create the relative widget, thus I need the paramx names under the form od string. The real application I'm working on is even more complex, like parameter are inside other objects of classes, but I used the simpliest example. One solution would be to define every parameter as an object where param1.name="param1" and param1.value=1. Thus in the GUI I would print param1.name. But this lead to a specifi problem of my implementation, that's reason 2:

  2. MyClass1..MyClassN will be at some point printed in a JSON. The JSON will be a huge file, and also since it's a complex tree (the example is simple) I want to make it as simple as possibile. To explain why I don't like to solution above, suppose this situation: class MyClass1: def init(self,param1,param2,combinations=[]): self.param1=param1 self.param2=param2 self.combinations=combinations

    Supposse param1 and param2 are now list of variable size, and combination is a list where each element is composed by all the combination of param1 and param2, and generate an output from some sort of calculation. Each element of the list combinations is an object SingleCombination,for example (metacode):

    param1=[1,2] param2=[5,6] SingleCombination.param1=1 SingleCombination.param2=5 SingleCombination.output=1*5 MyInst1.combinations.append(SingleCombination).

    In my case I will further incapsulated param1,param2 in a object called parameters, so every condition will hace a nice tree with only two object, parameters and output, and expanding parameters node will show all the parameters with their value.

    If I use JSON pickle to generate a JSON from the situation above, it is nicely displayed since the name of the node will be the name of the varaible ("param1", "param2" as strings in the JSON). But if I do the trick at the end of situation (1), creating an object of paramN as paramN.name and paramN.value, the JSON tree will become ugly but especially huge, because if I have a big number of condition, every paramN contains 2 sub-element. I wrote the situation and displayed with a JSON Viewer, see the attached immage

    I could pre processing the data structure before creating the JSON, the problem is that I use the JSON to recreate the data structure in another session of the program, so I need all the pieces of the data structure to be in the JSON.

So, from my requirements, it seems that the workround to avoid print the variable names creates some side effect on the JSON visualization that I don't know how to solve without changing the logic of my program... enter image description here


Solution

    1. If you use dataclasses, getting the field names is pretty straightforward:
    from dataclasses import dataclass, fields
    
    @dataclass
    class MyClass1:
        first:int = 4
    
    
    >>> fields(MyClass1)
    (Field(name='first',type=<class 'int'>,default=4,...),)
    

    This way, you can iterate over the class fields and ask your user to fill them. Note the field has a type, which you could use to eg ask the user for several values, as in your example.

    1. You could add functions to extract programatically the param names (_show_inputs below ) from the class and values from instances (_json below ):
    def blossom(cls):
        """decorate a class with `_json` (classmethod) and `_show_inputs` (bound)"""
        def _json(self):
            return json.dumps(self, cls=DataClassEncoder)
    
        def _show_inputs(cls):
            return {
                field.name: field.type.__name__
                for field in fields(cls)
        }
    
        cls._json = _json
        cls._show_inputs = classmethod(_show_inputs)
        
        return cls
    

    NOTE 1: There's actually no need to decorate the classes with blossom. You could just use its internal functions programatically.

    1. Using a custom json encoder to dump the dataclass objects, including properties:
    import json
    
    class DataClassPropEncoder(json.JSONEncoder):  # https://stackoverflow.com/a/51286749/7814595
        def default(self, o):
            if is_dataclass(o):
                cls = type(o)
    
                # inject instance properties
                props = {
                    name: getattr(o, name)
                    for name, value in cls.__dict__.items() if isinstance(value, property)
                }
                return {
                    **props,
                    **asdict(o)
                }
            return super().default(o)
    
    

    Finally, wrap the computations inside properties so they are serialized as well when using the decorated class. Full code example:

    
    from dataclasses import asdict
    from dataclasses import dataclass
    from dataclasses import fields
    from dataclasses import is_dataclass
    import json
    from itertools import product
    from typing import List
    
    class DataClassPropEncoder(json.JSONEncoder):  # https://stackoverflow.com/a/51286749/7814595
        def default(self, o):
            if is_dataclass(o):
                cls = type(o)
                props = {
                    name: getattr(o, name)
                    for name, value in cls.__dict__.items() if isinstance(value, property)
                }
                return {
                    **props,
                    **asdict(o)
                }
            return super().default(o)
    
    def blossom(cls):
        def _json(self):
            return json.dumps(self, cls=DataClassEncoder)
    
        def _show_inputs(cls):
            return {
                field.name: field.type.__name__
                for field in fields(cls)
        }
    
        cls._json = _json
        cls._show_inputs = classmethod(_show_inputs)
        
        return cls
    
    @blossom
    @dataclass
    class MyClass1:
        param1:int
        param2:int
        
    
    @blossom
    @dataclass
    class MyClass2:
        param3: List[str]
        param4: List[int]
    
        def _compute_single(self, values):  # TODO: implmement this
            return values[0]*values[1]
    
        @property
        def combinations(self):
            # TODO: cache if used more than once
            # TODO: combinations might explode
            field_names = []
            field_values = []
            cls = type(self)
            for field in fields(cls):
                field_names.append(field.name)
                field_values.append(getattr(self, field.name))
    
            results = []
            for values in product(*field_values):
                result = {
                    **{
                        field_names[idx]: value
                        for idx, value in enumerate(values)
                    },
                    "output": self._compute_single(values)
                }
                results.append(result)
                
                
            return results
    
    >>> print(f"MyClass1:\n{MyClass1._show_inputs()}")
    MyClass1:
    {'param1': 'int', 'param2': 'int'}
    >>> print(f"MyClass2:\n{MyClass2._show_inputs()}")
    MyClass2:
    {'param3': 'List', 'param4': 'List'}
    >>> obj_1 = MyClass1(3,4)
    >>> print(f"obj_1:\n{obj_1._json()}")
    obj_1:
    {"param1": 3, "param2": 4}
    >>> obj_2 = MyClass2(["first", "second"],[4,2])._json()
    >>> print(f"obj_2:\n{obj_2._json()}")
    obj_2:
    {"combinations": [{"param3": "first", "param4": 4, "output": "firstfirstfirstfirst"}, {"param3": "first", "param4": 2, "output": "firstfirst"}, {"param3": "second", "param4": 4, "output": "secondsecondsecondsecond"}, {"param3": "second", "param4": 2, "output": "secondsecond"}], "param3": ["first", "second"], "param4": [4, 2]}
    

    NOTE 2: If you need to perform several computations per class, it might be a good idea to abstract away the pattern in the combinations property to avoid repeating code.

    NOTE 3: If you need access to the properties several times and not ust once, you might want to consider caching their values to avoid re-computation.