I want to refactor a class into a dataclass, here is what I have:
class Tenant:
def __init__(self, tenant: dict, api_client: str) -> None:
self.name = tenant["name"]
self.tenant_id= tenant["id"]
self.subdomain = tenant["subdomain"]
self.api_client= api_client
What would be the same class as a dataclass? I tried with something like that, but I don't know how to separate this dict into name, tenant_id and subdomain:
@dataclass
class Tenant:
tenant: dict
api_client: str
CodePudding user response:
I would add a class method to the data class to extract the necessary values from a dict
.
@dataclass
class Tenant:
name: str
tenant_id: int
subdomain: str
api_client: str
@classmethod
def from_dict(cls, tenant: dict, api_client: str):
return cls(tenant["name"],
tenant["id"],
tenant["subdomain"],
api_client)
t1 = Tenant("alice", 5, "bar", "client")
t2 = Tenant.from_dict({"name": "bob", "id": 6, "subdomain": "foo"},
"client")
I would take the same approach even if Tenant
were not a dataclass. An instance of Tenant
is only interested in the values to assign to its attributes, not how those values are packaged prior to the instance being created.
If you must preserve the existing API for Tenant
, you'll need to use an InitVar
and the __post_init__
method.
from dataclasses import dataclass, InitVar, field
@dataclass
class Tenant:
tenant: InitVar[dict]
name: str = field(init=False)
tenant_id: int = field(init=False)
subdomain: str = field(init=False)
api_client: str
# Approximate __init__ method generated
# def __init__(self, tenant, api_client):
# self.api_client = api_client
# self.__post_init__(tenant)
def __post_init__(self, tenant):
self.name = tenant["name"]
self.tenant_id = tenant["id"]
self.subdomain = tenant["subdomain"]
t = Tenant({"name": "bob", "id": 6, "subdomain": "foo"},
"client")
tenant
, as an InitVar
, is passed to __init__
and __post_init__
, but will not be used as an attribute for the other autogenerated methods. name
, tenant_id
, and subdomain
will not be accepted as arguments to __init__
, but will be used by the other autogenerated methods. You, however, are responsible for ensuring they are set correctly in __post_init__
.
A possible hybrid approach to define a "private" class, and make the name Tenant
refer to the class method.
def _from_dict(cls, tenant, api_client):
return cls(tenant["name"],
tenant["id"],
tenant["subdomain"],
api_client)
# Using make_dataclass just to make the class name
# 'Tenant' instead of '_Tenant'. You can use an
# ordinary class statement and patch _Teant.__name__
# instead.
_Tenant = dataclasses.make_dataclass(
'Tenant',
[('name', str),
('tenant_id', int),
('subdomain', str),
('api_client', str)],
namespace={'from_dict': classmethod(_from_dict)}
)
Tenant = _Tenant.from_dict
CodePudding user response:
As @chepner suggested, the idea of implementing a .from_dict
method is definely pythonic and readable too, so i'm going to implement it in this code too.
Since the OP wants that the Class must have 2 arguments I would suggest to use a collections.namedtuple
.
collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)¶
Returns a new tuple subclass named typename. The new subclass is used to create tuple-like objects that have fields accessible by attribute lookup as well as being indexable and iterable
WARNING: I haven't tested the code yet
TenantnoAPI = namedtuple("Tenant_noAPI","name tenant_id subdomain")
@dataclass
class Tenant:
tenantnoapi:TenantnoAPI
api_client: str
@classmethod
def from_dict(cls, tenant: dict, api_client: str):
return cls(TenantnoAPI(tenant["name"],
tenant["id"],
tenant["subdomain"]),
api_client)