3

I'm facing the following problem. I have a bunch of inputs and need to write a function that instantiates several objects (dataclass instances) that need a subset of these inputs.

So I've got something like:

def create_objects(**kwargs):
    # as you can see some attrs are used in different objects.
    obj1 = Object1(attr1=kwargs.get("attr1"), attr2=kwargs.get("attr2"))
    obj2 = Object2(attr3=kwargs.get("attr3"), attr2=kwargs.get("attr2"))

Since all these dataclasses require multiple (between 2 and 10) attributes passed on to them, I would like to do some magic like, to check if the Object1 class has a certain property defined in the dataclass:

    obj1 = Object1(**{k: v for k, v in kwargs.items() if hasattr(Object1, k)})

Obviously this doesn't work. Is there a way to do this pythonically?

Or is there a better approach?

martineau
  • 119,623
  • 25
  • 170
  • 301
forkerino
  • 123
  • 9
  • are obj1 and obj2 two instances of the same class? – Thecave3 Dec 31 '19 at 11:41
  • 2
    This really depends heavily on context, as well as what the classes `Object1` and `Object2` are, how they're implemented, and how much you know about them in advance. You could use `inspect.signature` to inspect what arguments they take for initialization, and I have done this before in cases where high flexibility was needed, but it does have a bit of a code smell. I would recommend trying to refactor first if you can, unless this needs to be truly dynamic. – Iguananaut Dec 31 '19 at 11:41
  • I am trying this now: `**{ k: v for k, v in kwargs.items() if k in [f.name for f in dataclasses.fields(Object1)] }` And it seems to work, but is rather verbose. Is there an even better way? – forkerino Dec 31 '19 at 11:41
  • @Thecave3 nope, all different. – forkerino Dec 31 '19 at 11:43
  • 1
    That seems like the best solution. I don't think it's overly verbose. If you didn't want to do the list comprehension you could use the `__dataclass_fields__` attribute, but that seems unadvisable for the sake of avoiding a few extra words. `**{ k: v for k, v in kwargs.items() if k in Object1.__dataclass_fields__}` – PyPingu Dec 31 '19 at 11:45
  • @Iguananaut I'll try `inspect.signature`. I agree it is a bit of a smell, but perhaps better than what's there now. I'll need to think a bit more on how to refactor. – forkerino Dec 31 '19 at 11:47
  • `inspect.signature` is the safest way. Here is a solution using it on dataclasses via a classmethod: https://stackoverflow.com/questions/54678337/how-does-one-ignore-extra-arguments-passed-to-a-data-class/55096964#55096964 Replacing `env` with `**kwargs` should alter the call in the way that you want it. – Arne Jan 03 '20 at 09:27

3 Answers3

2

You might want to take a look at the dacite library.

from dacite import from_dict

def create_objects(**kwargs):
    obj1 = from_dict(data_class=Object1, data=kwargs)
    obj2 = from_dict(data_class=Object2, data=kwargs)

As long as Config.strict is set to False, which is the default option, it will automatically ignore additional dictionary keys.

Kent Shikama
  • 3,910
  • 3
  • 22
  • 55
1

Current solution I've implemented is the following helper function, inside the function (kwargs is in the closure):

def get_kwargs_for_class(cls):
    return {
        k: v
        for k, v in kwargs.items()
        if k in [f.name for f in dataclasses.fields(cls)]
    }

which enables me to do this:

    obj1 = Class1(**get_kwargs_for_class(Class1))
    obj2 = Class2(**get_kwargs_for_class(Class2))

which doesn't look too bad to me.

forkerino
  • 123
  • 9
1

One solution is to use operator.itemgetter.

For example:

from operator import itemgetter
from dataclasses import dataclass

@dataclass
class Object1:
    name: str
    price: float

@dataclass
class Object2:
    name: str
    title: str

d = {'name':'some name', 'price': 42, 'title': 'some title'}

def create_objects(d):
    i1 = itemgetter(*Object1.__dataclass_fields__)
    i2 = itemgetter(*Object2.__dataclass_fields__)

    obj1 = Object1(*i1(d))
    obj2 = Object2(*i2(d))

    print(obj1)
    print(obj2)

create_objects(d)

Prints:

Object1(name='some name', price=42)
Object2(name='some name', title='some title')
Andrej Kesely
  • 168,389
  • 15
  • 48
  • 91