Summary
This example illustrates how one can change the indentation of an entire block of code, eliminate lines, and change the content much more drastically than what the previous examples have done.
The idea behind this example is to help reduce the amount of typing
required and increases readability when assigning attributes in a
class’s __init__()
method.
Auto self
Python is known for its concise and readable syntax. One exception about the concisiveness is the boiler plate code that has to be written when defining one’s own class, especially if it has many attributes, like:
self.this_variable = this_variable
self.that_variable = that_variable
self.this_other_variable = this_other_variable
self.foo = foo
self.bar = bar
self.baz = [] if baz is None else baz
self.spam = bread + ham
This leads people to ask on various forums, such as this question on StackOverflow, how to do automatic assignment of attributes. The answers most often given are:
Don’t do it; learn to live with the explicit
self
.Use a decorator, with various examples provided.
As programmers create more classes, they find the need to add their own
dunder methods, such as __eq__(self, other)
, __repr__(self)
, etc.
Eventually, they might get annoyed enough at having to re-create these methods
too often, with the occasional typo causing bugs that they jump with
joy when discovering attrs: Classes Without Boilerplate.
Starting with Python 3.7, the standard library includes
dataclasses which shares some
similarity with attrs
. However, it does require to use type hints which,
in my opinion, reduces readability; note that many programmers find that
type hints increase readability.
As a concrete example of using traditional Python notation and dataclasses, let’s consider the code given in PEP 557 but reformatted with Black:
class Application:
def __init__(
self,
name,
requirements,
constraints=None,
path="",
executable_links=None,
executables_dir=(),
):
self.name = name
self.requirements = requirements
self.constraints = {} if constraints is None else constraints
self.path = path
self.executable_links = [] if executable_links is None else executable_links
self.executables_dir = executables_dir
self.additional_items = []
From the same PEP document, this is the proposed code
which gives the same initialization, but using the @dataclass
decorator:
from dataclasses import dataclass
@dataclass
class Application:
name: str
requirements: List[Requirement]
constraints: Dict[str, str] = field(default_factory=dict)
path: str = ''
executable_links: List[str] = field(default_factory=list)
executable_dir: Tuple[str] = ()
additional_items: List[str] = field(init=False, default_factory=list)
This code does more than simply initializing the variables, but I do not find it particularly readable.
So, I was wondering if it might be possible to imagine a simpler syntax.
auto_self
is what I came up with.
That ship has sailed …
I realize that there is zero chance that the following syntax would
be adopted, especially now that the dataclasses
module has been added to
the Python standard library. Still, you can try it out using
auto_self
hook.
class Application:
def __init__(
self,
name,
requirements,
constraints=None,
path="",
executable_links=None,
executables_dir=(),
):
self .= :
name
requirements
constraints = {} if __ is None else __
path
executable_links = [] if __ is None else __
executables_dir
additional_items = []
Here, I am using a new operator, .=
, which is meant to represent
the automatic assignment of a variable to the name that precedes
it (self
in this example). I have seen this idea for such an operator before on
python-ideas but never for introducing a code block as I do here.
By design, any dunder (double underscore), __
, is taken to be equivalent to the variable
being initialized. I chose a dunder instead of a single underscore _
so that it could be used in a REPL without creating conflicts with the
existing use of a single underscore in Python’s REPL. I also find that it
makes it more readable.
Of course, one is not restricted to using self
, or having to use __
everywhere. The following is completely equivalent - although I now
find it less readable, having been used to seeing __
as easy to scan
placeholder:
class Application:
def __init__(
cls,
name,
requirements,
constraints=None,
path="",
executable_links=None,
executables_dir=(),
):
cls .= :
name
requirements
constraints = {} if constraints is None else constraints
path
executable_links = [] if __ is None else executable_links
executables_dir
cls.additional_items = []
Warning
Unlike @dataclass
or attrs
, no additional method is
created by auto_self
.