I want to create two classes, one for attributes and one for functional behaviors. The point of the attributes class was to hold all of the attributes along with property methods (setters/getters), and I wanted to split the classes up this way so that everything related to the attributes would not fill up the entire other class.
Due to the way python handles mutable default arguments, I have to handle the attributes using None
for default arguments, but I have a very large number of parameters. This has caused the code to become unnecessarily long when passing arguments around on the initialization:
class Attributes:
def __init__(self,
arg1,
arg2,
arg3,
arg4,
arg5):
self.arg1 = default_arg1 if arg1 is None else arg1
self.arg2 = default_arg2 if arg2 is None else arg2
self.arg3 = default_arg3 if arg3 is None else arg3
self.arg4 = default_arg4 if arg4 is None else arg4
self.arg5 = default_arg5 if arg5 is None else arg5
# attribute methods like getters and setters
class Functionality(Attributes):
def __init__(self,
arg1 = None,
arg2 = None,
arg3 = None,
arg4 = None,
arg5 = None):
super(Functionality, self).__init__(
arg1,
arg2,
arg3,
arg4,
arg5
)
# Methods that give functionality to the class
I want to be able to use this class as follows:
example_obj = Functionality(
arg1 = example_arg1,
arg4 = example_arg4
)
Is there any cleaner way to do this? I want to add more attributes (more than 20 attributes instead of 5), but the above code involves writing basically the same thing way too many times.
2 Answers 2
I'm going to address your existing code in a manner that generally goes from most minimal to most significant changes. I'd strongly recommend actually using the solution at the very bottom of this post; the minimal changes are more useful:
- From a historical (pre-
dataclasses
) perspective - For understanding how classes and inheritance work
but there's no reason to torture yourself hand-writing all of this when dataclasses
will do the work for you.
Delegating to defaults of super class
When you're inheriting __init__
from another class, you typically don't want to reproduce their parameters explicitly; it creates too much confusion and too much interdependency (making more opportunities for code to get out of sync). There are two standard solutions here:
Option 1: Explicitly delegate via **kwargs
When Functionality
doesn't make direct use of any parameter, don't accept it by name. Just accept **kwargs
(when you're talking about this many defaulted parameters, no one should be passing arguments positionally anyway; it's a nightmare for readability/maintainability) and pass it along. So Functionality
would look like:
class Functionality(Attributes):
def __init__(self, **kwargs):
# Other stuff required for initialization
super().__init__(**kwargs) # Python 3.x doesn't require you to pass args to super in most cases
# Other stuff required for initialization
# Methods that give functionality to the class
Option 2: Inherit __init__
implicitly
In this case, it might be even simpler though; the __init__
of Functionality
doesn't do anything beyond delegate to Attributes
. If your real code is the same (nothing but a super().__init__
call), you'd just omit the definition of __init__
on Functionality
and let it inherit Attributes
's __init__
directly.
class Functionality(Attributes):
# No __init__ defined at all; uses Attributes.__init__ automatically
# Methods that give functionality to the class
Either way, you're no longer using separate defaults for each class.
Note: If the parent class must not accept default arguments, but the child class should, see the end of this answer for Preferred solution (especially if the child must not have defaults, and the parent class should): dataclasses
everywhere (not described here since it relies on techniques for solving your mutable defaults problem, which I haven't gotten to yet).
Avoid problems with mutable defaults
There are two simple solutions for avoiding the problems with mutable defaults.
Option 1: Deep copy unconditionally
For the defaulted class itself, go ahead and use mutable arguments as defaults, but the safe way, making deep copies. This is often safer even when not passed as defaults, since without copying, you'd be aliasing values from the caller, and changes made by either you or the caller would affect the other.
Adding explicit mutable defaults, you end up with:
from copy import deepcopy
class Attributes:
def __init__(self, arg1=[], arg2={}, arg3=set(), arg4=MyMutable(), arg5=OtherMutable()):
self.arg1 = deepcopy(arg1)
self.arg2 = deepcopy(arg2)
self.arg3 = deepcopy(arg3)
self.arg4 = deepcopy(arg4)
self.arg5 = deepcopy(arg5)
# attribute methods like getters and setters
Option 2: Let dataclasses
do your work for you
As an alternative to hand-writing all of Attributes
, I'd suggest making it a dataclass
, which means you don't need to repeat the names, and allows you to define default_factory
s for each field
to generate default values on demand. That would allow you to write a simpler Attributes
(with far less name repetition) matching what I wrote above like so:
from dataclasses import dataclass, field
@dataclass
class Attributes:
arg1: list = field(default_factory=list)
arg2: dict = field(default_factory=dict)
arg3: set = field(default_factory=set)
arg4: MyMutable = field(default_factory=MyMutable)
arg5: OtherMutable = field(default_factory=OtherMutable)
# attribute methods like getters and setters
and it will generate __init__
(as well as __repr__
and __eq__
for good measure; you can turn them off, or other features on, by parameterizing the @dataclass
decorator) for you, including automatically calling your default_factory
to initialize the field if and only if the caller did not provide the parameter.
Note: Unlike the other solution, this doesn't ensure caller-provided arguments are copied, so the risk of aliasing would remain. You could always define a __post_init__
to copy any fields you think this is likely to be a problem for.
Side-note: For my example, I just annotated the types as list
, dict
, and set
; to annotate properly, you'd usually the typing
classes (List
, Dict
, Set
) and annotate with the types the containers are expected to hold, e.g. arg1: List[int] = field(default_factory=list)
if arg1
is expected to be a list
of int
s.
Preferred solution (especially if the child must not have defaults, and the parent class should): dataclasses
everywhere
In your example, Attributes()
fails for lack of arguments, while Functionality()
does not (substituting defaults),
dataclasses
still cover that case adequately. You'd define both parent and child as dataclasses, but not provide defaults for the fields on the parent:
from dataclasses import dataclass, field
@dataclass
class Attributes:
arg1: list
arg2: dict
arg3: set
arg4: MyMutable
arg5: OtherMutable
# Optionally define __post_init__ to modify arguments, perform other work
# attribute methods like getters and setters
@dataclass
class Functionality(Attributes):
arg1: list = field(default_factory=list)
arg2: dict = field(default_factory=dict)
arg3: set = field(default_factory=set)
arg4: MyMutable = field(default_factory=MyMutable)
arg5: OtherMutable = field(default_factory=OtherMutable)
# Optionally define __post_init__ to modify arguments, perform other work
# Methods that give functionality to the class
Since no defaults are set on Attributes
, direct use of Attributes
will require you all arguments to be provided. But dataclasses
are helpful again here; the fields of a child class of a dataclass are:
- All of the fields of the parent(s)
- Plus all of the fields of the child
- When a given field is defined in both, the child's definition wins
So we can provide default_factory
s solely on the child (and get defaulting behavior), but not on the parent (so it never defaults).
-
1\$\begingroup\$ Works wonderfully! Will have to try looking more into dataclasses but for now using everything else suffices for me. \$\endgroup\$Simply Beautiful Art– Simply Beautiful Art2020年11月03日 19:44:38 +00:00Commented Nov 3, 2020 at 19:44
-
\$\begingroup\$ @SimplyBeautifulArt: Glad it helps.
dataclasses
is one of those things where, technically, everything it does could be written by hand (by definition; the module is written in Python), but it would be a royal pain to handle all the corner cases (e.g. your scenario whereAttributes
lacks default arguments, whileFunctionality
takes mutable default arguments). What would otherwise be substantial changes often just means flipping a flag (e.g. if the class should be immutable/hashable, you just use@dataclass(frozen=True)
and it generates__hash__
and protects against instance mutation). \$\endgroup\$ShadowRanger– ShadowRanger2020年11月03日 20:08:19 +00:00Commented Nov 3, 2020 at 20:08 -
\$\begingroup\$ I see. Good to know if I ever want a more general use of a class for handling data. \$\endgroup\$Simply Beautiful Art– Simply Beautiful Art2020年11月03日 20:15:39 +00:00Commented Nov 3, 2020 at 20:15
By passing all of the arguments inside of a dictionary one avoids a lengthy __init__
in Functionality
and also allows setting all default arguments in 3 lines without causing issues with mutable arguments.
class Attributes:
def __init__(self, attributes):
default_attributes = {
'arg1' : default_arg1,
'arg2' : default_arg2,
'arg3' : default_arg3,
'arg4' : default_arg4,
'arg5' : default_arg5
}
for attribute in default_attributes.keys():
if attribute not in attributes:
attributes[attribute] = default_attributes[attribute]
arg1 = attributes['arg1']
arg2 = attributes['arg2']
arg3 = attributes['arg3']
arg4 = attributes['arg4']
arg5 = attributes['arg5']
class Functionality(Attributes):
def __init__(self, attributes = None):
super(Functionality, self).__init__({} if attributes is None else attributes)
The only difference in usage is it now has to be used with a dictionary.
example_obj = Functionality({
'arg1' : example_arg1,
'arg2' : example_arg2
})
Explore related questions
See similar questions with these tags.