py3k feature proposal: field auto-assignment in constructors

C

coldpizza

There is a pattern that occurs fairly often in constructors in Python
and other OOP languages.

Let's take an example:

class Server(object):
def __init__(self, host, port, protocol, bufsize, timeout):
self.host = host
self.port = port
self.protocol = protocol
self.bufsize = bufsize
self.maxthreads = maxthreads
self.timeout = timeout

Imho, in the class above the assignment to instance fields does not
contain much programming logic and therefore can be safely 'abstracted
away' by the language itself with a syntax which would look something
like this:

class Server(object):
def __init__(self, @host, @port, @protocol, @bufsize, @timeout):
pass

This would be equivalent to the first example above, yet it does not
obfuscate the code in any way. Or does it? It does look much cleaner
to me.

Of course, the ampersand is just an arbitrary choice and might have
bad connotations for those who read it as 'take address of' but @ has
some allusion to delegates which maybe is ok.

I am not an experienced programmer and I am not sure if this is
necessarily a good idea, so I wanted to get some feedback from more
experienced Pythonistas before submitting it elsewhere.
 
A

André

There is a pattern that occurs fairly often in constructors in Python
and other OOP languages.

Let's take an example:

class Server(object):
def __init__(self, host, port, protocol, bufsize, timeout):
self.host = host
self.port = port
self.protocol = protocol
self.bufsize = bufsize
self.maxthreads = maxthreads
self.timeout = timeout

Imho, in the class above the assignment to instance fields does not
contain much programming logic and therefore can be safely 'abstracted
away' by the language itself with a syntax which would look something
like this:

class Server(object):
def __init__(self, @host, @port, @protocol, @bufsize, @timeout):
pass

This would be equivalent to the first example above, yet it does not
obfuscate the code in any way. Or does it? It does look much cleaner
to me.

Of course, the ampersand is just an arbitrary choice and might have
bad connotations for those who read it as 'take address of' but @ has
some allusion to delegates which maybe is ok.

I am not an experienced programmer and I am not sure if this is
necessarily a good idea, so I wanted to get some feedback from more
experienced Pythonistas before submitting it elsewhere.

If you search on this list, you will find that there has been *many*
proposals to remove self (which, I realize is slightly different than
what yo propose) and that the main argument can be summarized as
"Explicit is better than implicit."

Personally, I like the idea you suggest, with the modification that I
would use "." instead of "@", as in

class Server(object):
def __init__(self, .host, .port, .protocol, .bufsize, .timeout):
pass

André
 
W

Wildemar Wildenburger

André said:
Personally, I like the idea you suggest, with the modification that I
would use "." instead of "@", as in

class Server(object):
def __init__(self, .host, .port, .protocol, .bufsize, .timeout):
pass
I like :)

However, you can probably cook up a decorator for this (not certain, I'm
not a decorator Guru), which is not that much worse.

Still, I'd support that syntax (and the general idea.).

/W
 
D

Diez B. Roggisch

Wildemar said:
I like :)

However, you can probably cook up a decorator for this (not certain, I'm
not a decorator Guru), which is not that much worse.

Still, I'd support that syntax (and the general idea.).

Just for the fun of it, I implemented a decorator:

from functools import *
from inspect import *

def autoassign(_init_):
@wraps(_init_)
def _autoassign(self, *args, **kwargs):
argnames, _, _, _ = getargspec(_init_)
for name, value in zip(argnames[1:], args):
setattr(self, name, value)
_init_(self, *args, **kwargs)

return _autoassign

class Test(object):
@autoassign
def __init__(self, foo, bar):
pass



t = Test(10, 20)

print t.bar


Diez
 
T

Torsten Bronger

Hallöchen!

Wildemar said:
I like :)

However, you can probably cook up a decorator for this (not
certain, I'm not a decorator Guru), which is not that much worse.

Still, I'd support that syntax (and the general idea.).

Well, you save one or two lines per class. Not enough in my
opinion.

Tschö,
Torsten.
 
W

Wildemar Wildenburger

Diez said:
Just for the fun of it, I implemented a decorator:

from functools import *
from inspect import *

def autoassign(_init_):
@wraps(_init_)
def _autoassign(self, *args, **kwargs):
argnames, _, _, _ = getargspec(_init_)
for name, value in zip(argnames[1:], args):
setattr(self, name, value)
_init_(self, *args, **kwargs)

return _autoassign

This is neat. :) Could that maybe be extended to only assign selected
args to the instance and let others pass unchanged. So that, for instance:

@autoassign("foo", "bar")
def __init__(self, foo, bar, baz):
super(baz)

?W
 
A

André

Diez said:
Just for the fun of it, I implemented a decorator:
from functools import *
from inspect import *
def autoassign(_init_):
@wraps(_init_)
def _autoassign(self, *args, **kwargs):
argnames, _, _, _ = getargspec(_init_)
for name, value in zip(argnames[1:], args):
setattr(self, name, value)
_init_(self, *args, **kwargs)
return _autoassign

This is neat. :) Could that maybe be extended to only assign selected
args to the instance and let others pass unchanged. So that, for instance:

@autoassign("foo", "bar")
def __init__(self, foo, bar, baz):
super(baz)

?W

If one goes back to the original idea instead, the decision of using
automatic assignment should depend on the signature of the __init__
function. Here's an implementation (using "_" instead of "." as it
would lead to a syntax error):

from functools import *
from inspect import *

def autoassign(_init_):
@wraps(_init_)
def _autoassign(self, *args, **kwargs):
argnames, _, _, _ = getargspec(_init_)
for name, value in zip(argnames[1:], args):
if name.startswith("_"):
setattr(self, name[1:], value)
_init_(self, *args, **kwargs)

return _autoassign

class Test(object):
@autoassign
def __init__(self, _foo, _bar, baz):
print 'baz =', baz

t = Test(1, 2, 3)
print t.foo
print t.bar
print t.baz

#== the output is

baz = 3
1
2
Traceback (most recent call last):
File "/Users/andre/CrunchySVN/branches/andre/src/tools_2k.py", line
24, in exec_code
exec code in local_dict
File "User's code", line 23, in <module>
AttributeError: 'Test' object has no attribute 'baz'

#======
André
 
D

Dustan

Hallöchen!








Well, you save one or two lines per class. Not enough in my
opinion.

Are you referring to the alternate syntax or to the decorator? Either
way, you could be saving 4 or 5 or more lines, if you have enough
arguments.
 
W

Wildemar Wildenburger

Dustan said:
Are you referring to the alternate syntax or to the decorator? Either
way, you could be saving 4 or 5 or more lines, if you have enough
arguments.

OK, but then again, every decent IDE should give you the tools to write
an automation for that. Not that I don't like the idea of
auto-assignment, but, you know ...

/W
 
T

Terry Reedy

If one goes back to the original idea instead, the decision of using
automatic assignment should depend on the signature of the __init__
function. Here's an implementation (using "_" instead of "." as it
would lead to a syntax error):

from functools import *
from inspect import *

def autoassign(_init_):
@wraps(_init_)
def _autoassign(self, *args, **kwargs):
argnames, _, _, _ = getargspec(_init_)
for name, value in zip(argnames[1:], args):
if name.startswith("_"):
setattr(self, name[1:], value)
_init_(self, *args, **kwargs)

return _autoassign

class Test(object):
@autoassign
def __init__(self, _foo, _bar, baz):
print 'baz =', baz

t = Test(1, 2, 3)
print t.foo
print t.bar
print t.baz

#== the output is

baz = 3
1
2
Traceback (most recent call last):
File "/Users/andre/CrunchySVN/branches/andre/src/tools_2k.py", line
24, in exec_code
exec code in local_dict
File "User's code", line 23, in <module>
AttributeError: 'Test' object has no attribute 'baz'
=================================

I think this version, with this name convention, is nice enough to possibly
go in the stdlib if there were an appropriate place for it. Not sure where
though. If there were a classtools module....

tjr
 
B

Ben Finney

André said:
Personally, I like the idea you suggest, with the modification that I
would use "." instead of "@", as in

class Server(object):
def __init__(self, .host, .port, .protocol, .bufsize, .timeout):
pass

-1.

That leading dot is too easy to miss when looking over the code.
 
S

Steven D'Aprano

OK, but then again, every decent IDE should give you the tools to write
an automation for that. Not that I don't like the idea of
auto-assignment, but, you know ...

You know, not everybody uses a "decent IDE", by choice or necessity, and
even if they did, having what is essentially a macro to type for you
doesn't solve the essential problem that you're writing the same thing
THREE TIMES instead of once. And then you have to keep it all in sync
through who knows how many code revisions and refactorings.

class Parrot(object): # after many revisions...
def __init__(self, genus, species, variety, name, age, colours,
wingspan, beaksize, healthstate, language, vocabulary):
self.wingspan = wingspan
self.beaksize = beaksize
self.name = name
self.age = age
self.binomial_name = (genus, species)
self.breed = variety
self.colour = colour
self.language = language
self.state = get_state(healthstate)
self.words = vocabulary
self.colors = colours


What IDE will spot the error(s) in the above?


Here's another version, assuming syntax support for auto-assignment for
names starting with an ampersand:

class Parrot(object): # after many revisions...
def __init__(self, genus, species, variety, &name, &age, &colours,
&wingspan, &beaksize, healthstate, &language, vocabulary):
self.binomial_name = (genus, species)
self.breed = variety
self.state = get_state(healthstate)
self.words = vocabulary

See how much easier it is to keep the attributes synced with the
arguments? Don't Repeat Yourself in action.


I think the biggest minus on this proposal is that it creates new syntax
that is only meaningful in the __init__ method of a class. "Special cases
aren't special enough to break the rules." I'd vote for it, but
conditionally. What should this do?

def foo(x, y, &z):
pass

Create foo.z perhaps?
 
W

Wildemar Wildenburger

Ben said:
-1.

That leading dot is too easy to miss when looking over the code.

class Server(object):
def __init__(self, self.host, self.port,
self.protocol, self.bufsize, self.timeout):
pass

?

/W
 
P

Paul McGuire

I think this version, with this name convention, is nice enough to possibly
go in the stdlib if there were an appropriate place for it.  Not sure where
though.  If there were a classtools module....

tjr

+1

I thought at one time there was to be a "decorators" module in the
stdlib for just this kind of useful item. At minimum, you could post
this to the Python wiki at http://wiki.python.org/moin/PythonDecoratorLibrary.

-- Paul
 
M

MRAB

You know, not everybody uses a "decent IDE", by choice or necessity, and
even if they did, having what is essentially a macro to type for you
doesn't solve the essential problem that you're writing the same thing
THREE TIMES instead of once. And then you have to keep it all in sync
through who knows how many code revisions and refactorings.

class Parrot(object): # after many revisions...
def __init__(self, genus, species, variety, name, age, colours,
wingspan, beaksize, healthstate, language, vocabulary):
self.wingspan = wingspan
self.beaksize = beaksize
self.name = name
self.age = age
self.binomial_name = (genus, species)
self.breed = variety
self.colour = colour
self.language = language
self.state = get_state(healthstate)
self.words = vocabulary
self.colors = colours

What IDE will spot the error(s) in the above?

Here's another version, assuming syntax support for auto-assignment for
names starting with an ampersand:

class Parrot(object): # after many revisions...
def __init__(self, genus, species, variety, &name, &age, &colours,
&wingspan, &beaksize, healthstate, &language, vocabulary):
self.binomial_name = (genus, species)
self.breed = variety
self.state = get_state(healthstate)
self.words = vocabulary

See how much easier it is to keep the attributes synced with the
arguments? Don't Repeat Yourself in action.

I think the biggest minus on this proposal is that it creates new syntax
that is only meaningful in the __init__ method of a class. "Special cases
aren't special enough to break the rules." I'd vote for it, but
conditionally. What should this do?

def foo(x, y, &z):
pass

Create foo.z perhaps?
Well, if:

def __init__(self, &foo):
pass

does:

def __init__(self, foo):
self.foo = foo

then:

def foo(x, y, &z):
pass

does:

def foo(x, y, &z):
x.z = z

Don't think that's useful, though...
 
C

Christian Heimes

Paul said:
I thought at one time there was to be a "decorators" module in the
stdlib for just this kind of useful item. At minimum, you could post
this to the Python wiki at http://wiki.python.org/moin/PythonDecoratorLibrary.

Please take the idea to the Python developer list. Several decorators
are either already implemented (e.g. the first decorator is
functools.wraps) and others are too special but some of the decorators
including auto assignment seem useful.

Christian
 
S

Steven D'Aprano

On Sun, 27 Jan 2008 19:13:27 -0500, Terry Reedy wrote:

[snip]
class Test(object):
@autoassign
def __init__(self, _foo, _bar, baz):
print 'baz =', baz
[snip]

I think this version, with this name convention, is nice enough to
possibly go in the stdlib if there were an appropriate place for it.
Not sure where though. If there were a classtools module....

-1/2

I don't like the name convention. _name already has a perfectly good
convention: it's a private name, don't mess with it. That includes in
function/method signatures. With your convention, _foo is public.

I suppose you could write __foo for a private name, and ___foo for a
*really* private name, relying on the decorator to strip one of the
underscores. But counting all those underscores is a PITA, and what
happens if you don't actually want that private name set as an instance
attribute?

As nice as this feature would be, and I vote +2 on the functionality, I
wonder whether the amount of line noise in method definitions now will be
approaching Perlish levels? We've got default values, type annotations
(in Python3), *args and **kwargs, _ private names, and now we want to add
auto-assignment.

If we do get syntax support, I vote +1 on &foo, +1/2 on @foo, -1 on .foo
and -1 on self.foo. (It's explicit, but it's long...).
 
A

Arnaud Delobelle

However, you can probably cook up a decorator for this (not certain, I'm
not a decorator Guru), which is not that much worse.
Still, I'd support that syntax (and the general idea.).

Just for the fun of it, I implemented a decorator:

from functools import *
from inspect import *

def autoassign(_init_):
     @wraps(_init_)
     def _autoassign(self, *args, **kwargs):
         argnames, _, _, _ = getargspec(_init_)
         for name, value in zip(argnames[1:], args):
             setattr(self, name, value)
         _init_(self, *args, **kwargs)

     return _autoassign

Nice! I've got a slight variation without magic argument names:

def autoassign(*names):
def decorator(f):
def decorated(self, *args, **kwargs):
for name in names:
setattr(self, name, kwargs.pop(name))
return f(self, *args, **kwargs)
return decorated
return decorator

class Test(object):
@autoassign('foo', 'bar')
def __init__(self, baz):
print 'baz =', baz

t = Test(foo=1, bar=2, baz=6)
# or Test(6, foo=1, bar=2)

print t.foo
print t.bar
print t.baz
 
A

Arnaud Delobelle

On Jan 28, 1:47 am, Steven D'Aprano <st...@REMOVE-THIS-
cybersource.com.au> wrote:
[...]
As nice as this feature would be, and I vote +2 on the functionality, I
wonder whether the amount of line noise in method definitions now will be
approaching Perlish levels? We've got default values, type annotations
(in Python3), *args and **kwargs, _ private names, and now we want to add
auto-assignment.

Don't forget keyword only arguments! I find signatures too
complicated already, please let's not clutter them even more.
 
A

André

On Sun, 27 Jan 2008 19:13:27 -0500, Terry Reedy wrote:

[snip]
class Test(object):
@autoassign
def __init__(self, _foo, _bar, baz):
print 'baz =', baz
[snip]

I think this version, with this name convention, is nice enough to
possibly go in the stdlib if there were an appropriate place for it.
Not sure where though. If there were a classtools module....

-1/2

I don't like the name convention. _name already has a perfectly good
convention: it's a private name, don't mess with it. That includes in
function/method signatures. With your convention, _foo is public.

I suppose you could write __foo for a private name, and ___foo for a
*really* private name, relying on the decorator to strip one of the
underscores. But counting all those underscores is a PITA, and what
happens if you don't actually want that private name set as an instance
attribute?

As nice as this feature would be, and I vote +2 on the functionality, I
wonder whether the amount of line noise in method definitions now will be
approaching Perlish levels? We've got default values, type annotations
(in Python3), *args and **kwargs, _ private names, and now we want to add
auto-assignment.

If we do get syntax support, I vote +1 on &foo, +1/2 on @foo, -1 on .foo
and -1 on self.foo. (It's explicit, but it's long...).


Here's a version that
1. does not require new syntax
2. does not *necessarily* override the "_" prefix convention
3. follows the "Explicit is better than implicit" convention when
being called.

(Note: I do not necessarily recommend the "self_" choice)
========
from functools import wraps
from inspect import getargspec

def autoassign(prefix):
def _autoassign(_init_):
@wraps(_init_)
def __autoassign(self, *args, **kwargs):
argnames, _, _, _ = getargspec(_init_)
for name, value in zip(argnames[1:], args):
if name.startswith(prefix):
setattr(self, name[len(prefix):], value)
_init_(self, *args, **kwargs)

return __autoassign
return _autoassign

class Test(object):
@autoassign('self_')
def __init__(self, self_foo, self_bar, baz):
print 'baz =', baz

t = Test(1, 2, 3)
print t.foo
print t.bar
print t.baz # raises an exception

=============
André
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,055
Latest member
SlimSparkKetoACVReview

Latest Threads

Top