29

Very often, I find myself coding trivial datatypes like

class Pruefer:
    def __init__(self, ident, maxNum=float('inf'), name=""):
        self.ident  = ident
        self.maxNum = maxNum
        self.name   = name

While this is very useful (Clearly I don't want to replace the above with anonymous 3-tuples), it's also very boilerplate.

Now for example, when I want to use the class in a dict, I have to add more boilerplate like

    def __hash__(self):
        return hash(self.ident, self.maxNum, self.name)

I admit that it might be difficult to recognize a general pattern amongst all my boilerplate classes, but nevertheless I'd like to as this question:

  • Are there any popular idioms in python to derive quick and dirty datatypes with named accessors?

  • Or maybe if there are not, maybe a Python guru might want to show off some metaclass hacking or class factory to make my life easier?

pylang
  • 40,867
  • 14
  • 129
  • 121
Jo So
  • 25,005
  • 6
  • 42
  • 59
  • 2
    I think `namedtuple` is good enough (added full answer with code example) – Alexey Kachayev Dec 18 '12 at 12:50
  • [`namedtuple`](https://docs.python.org/3.9/library/collections.html?highlight=namedtuple#collections.namedtuple) now allows default values in 3.7+ – pylang Sep 13 '19 at 17:50

6 Answers6

26
>>> from collections import namedtuple
>>> Pruefer = namedtuple("Pruefer", "ident maxNum name")
>>> pr = Pruefer(1,2,3)
>>> pr.ident
1
>>> pr.maxNum
2
>>> pr.name
3
>>> hash(pr)
2528502973977326415

To provide default values, you need to do little bit more... Simple solution is to write subclass with redefinition for __new__ method:

>>> class Pruefer(namedtuple("Pruefer", "ident maxNum name")):
...     def __new__(cls, ident, maxNum=float('inf'), name=""):
...         return super(Pruefer, cls).__new__(cls, ident, maxNum, name)
... 
>>> Pruefer(1)
Pruefer(ident=1, maxNum=inf, name='')
Alexey Kachayev
  • 6,106
  • 27
  • 24
  • 1
    That's very nice! Do you happen to also know something that allows me to have default values? – Jo So Dec 18 '12 at 13:03
  • 1
    @JoSo -- You could just have a factory function which has defaults and returns a `Pruefer` instance. – mgilson Dec 18 '12 at 13:12
  • This might be just me, but I would prefer `("ident","maxNum","name")` to the version with a whitespace separated string ... It seems a little more obvious to me what is going on. – mgilson Dec 18 '12 at 13:15
  • @mgilson: The docs say that you can use `["a1","a2","a3"]`, too. (But I'm personally fine with the less-ink "a1 a2 a3". Just think `perl`'s `qw(a1 a2 a3)` ;>) – Jo So Dec 18 '12 at 13:17
  • @JoSo Updated answer with default values definition. – Alexey Kachayev Dec 18 '12 at 13:17
  • @mgilson: Regarding factory: Obviously true, embarassed to have missed that. Thanks! – Jo So Dec 18 '12 at 13:18
  • @AlexeyKachayev -- I wouldn't have thought of getting to the default arguments that way. Very clever. (would +1 again if I could) – mgilson Dec 18 '12 at 13:20
  • @AlexeyKachayev: Very very nice. Thank you very much. – Jo So Dec 18 '12 at 13:21
  • @JoSo -- I prefer not to think of `perl`'s anything ... :-p ... I write python to stay away from `perl`. – mgilson Dec 18 '12 at 13:21
  • @mgilson: I recently did some real-world text processing which is where perl really shines. It's quite a different language from python and clearly has its uses. It's great for readability to have all those implicit variables! – Jo So Dec 18 '12 at 13:25
  • Another way to handle the defaults would be to use the `_replace()` method. e.g. `template = namedtuple("Pruefer", "ident maxNum name")(1, float('inf'), "")` and then you can do `template._replace(ident=2)` to get a new object `Pruefer(ident=2, maxNum=inf, name='')` – Duncan Dec 18 '12 at 13:30
  • 1
    @Duncan -- I personally like to add the defaults after the fact: `Pruefer.__new__.func_defaults=(1,float('inf'),"")` (See my late answer) – mgilson Dec 18 '12 at 13:33
  • Gives the error "TypeError: namedtuple() takes 2 positional arguments but 3 were given" – Martin of Hessle Dec 04 '18 at 10:09
13

One of the most promising things from with Python 3.6 is variable annotations. They allow to define namedtuple as class in next way:

In [1]: from typing import NamedTuple

In [2]: class Pruefer(NamedTuple):
   ...:     ident: int
   ...:     max_num: int
   ...:     name: str
   ...:     

In [3]: Pruefer(1,4,"name")
Out[3]: Pruefer(ident=1, max_num=4, name='name')

It same as a namedtuple, but is saves annotations and allow to check type with some static type analyzer like mypy.

Update: 15.05.2018

Now, in Python 3.7 dataclasses are present so this would preferable way of defining DTO, also for backwardcompatibility you could use attrs library.

eirenikos
  • 2,296
  • 25
  • 25
8

Are there any popular idioms in python to derive quick ... datatypes with named accessors?

Dataclases. They accomplish this exact need.

Some answers have mentioned dataclasses, but here is an example.

Code

import dataclasses as dc


@dc.dataclass(unsafe_hash=True)
class Pruefer:
    ident : int
    maxnum : float = float("inf")
    name : str  = ""

Demo

pr = Pruefer(1, 2.0, "3")

pr
# Pruefer(ident=1, maxnum=2.0, name='3')

pr.ident
# 1

pr.maxnum
# 2.0

pr.name
# '3'

hash(pr)
# -5655986875063568239

Details

You get:

  • pretty reprs
  • default values
  • hashing
  • dotted attribute-access
  • ... much more

You don't (directly) get:

  • tuple unpacking (unlike namedtuple)

Here's a guide on the details of dataclasses.

pylang
  • 40,867
  • 14
  • 129
  • 121
1

I don't have much to add to the already excellent answer by Alexey Kachayev -- However, one thing that may be useful is the following pattern:

Pruefer.__new__.func_defaults = (1,float('inf'),"")

This would allow you to create a factory function which returns a new named-tuple which can have default arguments:

def default_named_tuple(name,args,defaults=None):
    named_tuple = collections.namedtuple(name,args)
    if defaults is not None:
        named_tuple.__new__.func_defaults = defaults
    return named_tuple

This may seem like black magic -- It did to me at first, but it's all documented in the Data Model and discussed in this post.

In action:

>>> default_named_tuple("Pruefer", "ident maxNum name",(1,float('inf'),''))
<class '__main__.Pruefer'>
>>> Pruefer = default_named_tuple("Pruefer", "ident maxNum name",(1,float('inf'),''))
>>> Pruefer()
Pruefer(ident=1, maxNum=inf, name='')
>>> Pruefer(3)
Pruefer(ident=3, maxNum=inf, name='')
>>> Pruefer(3,10050)
Pruefer(ident=3, maxNum=10050, name='')
>>> Pruefer(3,10050,"cowhide")
Pruefer(ident=3, maxNum=10050, name='cowhide')
>>> Pruefer(maxNum=12)
Pruefer(ident=1, maxNum=12, name='')

And only specifying some of the arguments as defaults:

>>> Pruefer = default_named_tuple("Pruefer", "ident maxNum name",(float('inf'),''))
>>> Pruefer(maxNum=12)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __new__() takes at least 2 arguments (2 given)
>>> Pruefer(1,maxNum=12)
Pruefer(ident=1, maxNum=12, name='')

Note that as written, It's probably only safe to pass a tuple in as defaults. However, you could easily get more fancy by ensuring you have a reasonable tuple object within the function.

Community
  • 1
  • 1
mgilson
  • 300,191
  • 65
  • 633
  • 696
1

An alternate approach which might help you to make your boiler plate code a little more generic is the iteration over the (local) variable dicts. This enables you to put your variables in a list and the processing of these in a loop. E.g:

class Pruefer:
     def __init__(self, ident, maxNum=float('inf'), name=""):
         for n in "ident maxNum name".split():
             v = locals()[n]  # extract value from local variables
             setattr(self, n, v)  # set member variable

     def printMemberVars(self):
         print("Member variables are:")
         for k,v in vars(self).items():
             print("  {}: '{}'".format(k, v))


P = Pruefer("Id", 100, "John")
P.printMemberVars()

gives:

Member Variables are:
  ident: 'Id'
  maxNum: '100'
  name: 'John'

From the viewpoint of efficient resource usage, this approach is of course suboptimal.

Dietrich
  • 5,241
  • 3
  • 24
  • 36
1

if using Python 3.7 you can use Data Classes; Data Classes can be thought of as "mutable namedtuples with defaults"

https://docs.python.org/3/library/dataclasses.html

https://www.python.org/dev/peps/pep-0557/

Enrique G
  • 342
  • 2
  • 6