Assign corresponding parameter value when omitting RHS in __init__

I don’t see this mentioned in the thread: This “assignment-with-missing-rhs” could generalize to other assignments.

In particular one case where one see same kind of repetition as in kwargs and some syntactic suger maybe could help is:

def __init__(self, height, width):
    self.height =
    self.width =

(although I think it looks kind odd and like someone forgot to finish writing that code. Also, dataclasses)

Yeah, that looks kinda weird, and I’m not a fan. Dataclasses certainly cover a lot of the use-cases, but I do sometimes wonder if Python would benefit from a generic way to set up a class with some attributes. Pike allows the class declaration itself to receive arguments, as if it were a function, and those arguments then become class attributes that get automatically populated by the initializer. I wonder, though, can Python do this with a decorator?

@attributes('foo', 'bar', 'spam', 'ham')
class Stuff:
    def __init__(self, initial_count=0):
        self.count = initial_count

would be equivalent to:

class Stuff:
    def __init__(self, foo, bar, spam, ham, *, initial_count=0):
        self.foo, self.bar, self.spam, self.ham = foo, bar, spam, ham
        self.count = initial_count

(Note that I’ve made these arguments positional-only for simplicity, since it’s a bit of a pain to do partial function application with pos-or-kw parameters and a mix of positional and keyword args.)

Would this see use?

I think dataclass inheritance does this better, though. It allows (well, requires) type hints, and is generally more flexible

@dataclass
class Stuff:
    foo: int
    bar: str
    spam: list
    ham: dict
    
@dataclass
class Stuff2(Stuff):
    initial_count: int = 0

edit: I guess one difference is the naming of initial_count vs count in the class itself. I don’t know if dataclasses support that somehow but also not sure how useful it is.

2 Likes

Yeah, that part was deliberate - it’s an attribute and a parameter that aren’t the same. (There are plenty of other ways that something might need an initializer but use a different value, or anything like that.) Point is, any scheme like this should not REPLACE the existing behaviour of __init__, but AUGMENT it by doing the boring part concisely and conveniently.

Having to write two classes kinda makes it pointless though. Obviously that’s great if you actually want most of the behaviour of a dataclass, but I’m talking about a really simple thing for the common case.

I guess I’m not clear on what the common case is, distinct from something dataclass-like.

This gets a little bit toward the earlier discussion of API design, and whether a helper like this is making the wrong thing easier. I often encounter code that could use that decorator (i.e. a class with a ton of init params that get saved as attributes) but in most cases it’d be better to break the class into smaller pieces, some of which might be dataclasses or other more-structured objects.

I think that is covered by “InitVar” and post_init in dataclasses.

@dataclass
class Stuff:
    initial_count: InitVar
    count: int = field(init=False)

    def __post_init__(self, initial_count):
        self.count = initial_count

So yeah. In short, maybe the generalization to other assignments isn’t really relevant. The cases with __init__ containing lots of those repetitions are most likley better covered by dataclasses.

Skimming through the CPython standard library for examples:

git grep -nE 'self.([a-zA-Z0-9_]+)\s*=\s*\1'

# argparse.py:204
    class _Section(object):

        def __init__(self, formatter, parent, heading=None):
            self.formatter = formatter
            self.parent = parent
            self.heading = heading
            self.items = []

# argparse.py:842
class Action(_AttributeHolder):
    def __init__(self,
                 option_strings,
                 dest,
                 nargs=None,
                 const=None,
                 default=None,
                 type=None,
                 choices=None,
                 required=False,
                 help=None,
                 metavar=None):
        self.option_strings = option_strings
        self.dest = dest
        self.nargs = nargs
        self.const = const
        self.default = default
        self.type = type
        self.choices = choices
        self.required = required
        self.help = help
        self.metavar = metavar

# asyncio/exceptions.py:36
class IncompleteReadError(EOFError):
    def __init__(self, partial, expected):
        r_expected = 'undefined' if expected is None else repr(expected)
        super().__init__(f'{len(partial)} bytes read on a total of '
                         f'{r_expected} expected bytes')
        self.partial = partial
        self.expected = expected

# dataclasses.py:309 (can't turn THIS one into a dataclass!)
class Field:
    def __init__(self, default, default_factory, init, repr, hash, compare,
                 metadata, kw_only):
        self.name = None
        self.type = None
        self.default = default
        self.default_factory = default_factory
        self.init = init
        self.repr = repr
        self.hash = hash
        self.compare = compare
        self.metadata = (_EMPTY_METADATA
                         if metadata is None else
                         types.MappingProxyType(metadata))
        self.kw_only = kw_only
        self._field_type = None

Plenty of examples. Sure, some of them could be turned into dataclasses, but a lot of them don’t need it. Especially, they don’t want their reprs to be governed by the particular selection of attributes that just so happen to be conveniently copied in. Why use a dataclass when literally the only part of it you want is the __init__ method?

We’re talking about shortening this:

def foo(x, y):
    bar(x=x, y=y)

to this:

def foo(x, y):
    bar(x=, y=)

But suppose you have:

class Foo:
    def __init__(self, x, y):
        self.x = x
        self.y = y

That can be shortened to:

class Foo:
    def __init__(self, x, y):
        self.x, self.y = x, y

What if the self. could be factored out like this:

class Foo:
    def __init__(self, x, y):
        self.(x, y) = x, y

and then further shortened like for named parameters:

class Foo:
    def __init__(self, x, y):
        self.(x, y) =

That would be another way to write:

class Foo:
    def __init__(self, x, y):
        self.(x) =
        self.(y) =

which would be equivalent to the original:

class Foo:
    def __init__(self, x, y):
        self.x = x
        self.y = y

?

Too cryptic?

Too cryptic and I don’t think it would actually be useful in the real use cases, where x and y have much longer names. If they’re literally named x and y then the original version is barely any longer.

1 Like

NB, someone reported this and requested splitting this idea off to a separate thread, which seemed reasonable enough. However, if there’s general consensus it should be part of the original, I can just merge it back again if you report send another report, thanks.

Nah, that was the right call to split it - thanks! Although I don’t think the original thread had anything left to discuss in it.

1 Like

Here’s a different idea which I implemented just for fun, https://aroberge.github.io/ideas/docs/html/auto_self.html, like most examples in that repository.

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 = []