13

While investigating Ruby I came across this to create a simple Struct-like class:

Person = Struct.new(:forname, :surname)
person1 = Person.new('John', 'Doe')
puts person1  #<struct Person forname="John", surname="Doe">

Which raised a few Python questions for me. I have written a [VERY] basic clone of this mechanism in Python:

def Struct(*args):
    class NewStruct:
        def __init__(self):
            for arg in args:
                self.__dict__[arg] = None

    return NewStruct

>>> Person = Struct('forename', 'surname')
>>> person1 = Person()
>>> person2 = Person()
>>> person1.forename, person1.surname = 'John','Doe'
>>> person2.forename, person2.surname = 'Foo','Bar'
>>> person1.forename
'John'
>>> person2.forename
'Foo'
  1. Is there already a similar mechanism in Python to handle this? (I usually just use dictionaries).

  2. How would I get the Struct() function to create the correct __init__() arguments. (in this case I would like to perform person1 = Person('John', 'Doe') Named Arguments if possible: person1 = Person(surname='Doe', forename='John')

I Would like, as a matter of interest, to have Question 2 answered even if there is a better Python mechanism to do this.

martineau
  • 119,623
  • 25
  • 170
  • 301
kjfletch
  • 5,394
  • 3
  • 32
  • 38
  • 2
    There is no struct type in Python because you rarely need it. Most of the time a tuple or a dict is enough, and for more complex cases use a real class. Don't try to write Ruby or Java code in Python - use Python idioms instead. – nikow Aug 12 '09 at 12:43

8 Answers8

17

If you're using Python 2.6, try the standard library namedtuple class.

>>> from collections import namedtuple
>>> Person = namedtuple('Person', ('forename', 'surname'))
>>> person1 = Person('John', 'Doe')
>>> person2 = Person(forename='Adam', surname='Monroe')
>>> person1.forename
'John'
>>> person2.surname
'Monroe'

Edit: As per comments, there is a backport for earlier versions of Python

Alice Purcell
  • 12,622
  • 6
  • 51
  • 57
11

If you're running python <2.6 or would like to extend your class to do more stuff, I would suggest using the type() builtin. This has the advantage over your solution in that the setting up of __dict__ happens at class creation rather than instantiation. It also doesn't define an __init__ method and thus doesn't lead to strange behavior if the class calls __init__ again for some reason. For example:

def Struct(*args, **kwargs):
    name = kwargs.pop("name", "MyStruct")
    kwargs.update(dict((k, None) for k in args))
    return type(name, (object,), kwargs)

Used like so:

>>> MyStruct = Struct("forename", "lastname")

Equivalent to:

class MyStruct(object):
    forename = None
    lastname = None

While this:

>>> TestStruct = Struct("forename", age=18, name="TestStruct")

Is equivalent to:

class TestStruct(object):
    forename = None
    age = 18

Update

Additionally, you can edit this code to very easily prevent assignment of other variables than the ones specificed. Just change the Struct() factory to assign __slots__.

def Struct(*args, **kwargs):
    name = kwargs.pop("name", "MyStruct")
    kwargs.update(dict((k, None) for k in args))
    kwargs['__slots__'] = kwargs.keys()
    return type(name, (object,), kwargs)
André Eriksson
  • 4,296
  • 2
  • 19
  • 16
  • +1 This is the kind of info I was looking for. Regardless if I am using Python <2.6 or not this knowledge can be applied outside of the OP problem (which is more important and was the actual goal). Very concise. – kjfletch Aug 12 '09 at 09:01
  • Am I wrong or it the way you defined Struct() creating classes with only *class* members? Wouldn't then all instances of the new class share the same values for them? Wouldn't you need to supply an \_\_init\_\_ key and function in kwargs, to provide for instance variables?! – ThomasH Aug 12 '09 at 17:15
  • 1
    The Struct() function defines the *default* values. You can override them by either manually setting the values as you normally would, i.e. my_instance.attribute = value – André Eriksson Aug 12 '09 at 18:32
  • That's not what I meant. When you have multiple instances of a constructed class, and change an attribute value of one, wouldn't it change for all others as well? – ThomasH Aug 12 '09 at 21:22
  • Although this allows me to create a class with default attributes it still does not allow arguments to the __init__: person1 = Person(name='Ted', age=42). – kjfletch Aug 13 '09 at 07:22
  • That's not what I mean. If you take the TestStruct from the answer, it is effectively a class, so you can create instances of that class: t1 = TestStruct(); t2 = TestStruct(). If I now set t1.age = 24, what's t2.age gonna be? And why? – ThomasH Aug 13 '09 at 08:39
  • 1
    There is another thing: If you use the second definition of Struct() (the one using __slots__), my Python 2.6 tells me instances of returned classes are read-only! MyStruct2 = Struct('forename','lastname'); m = MyStruct2(); m.forename = "Jack" -> AttributeError: 'MyStruct2' object attribute 'forename' is read-only. - This renders this approach useless, given the OP's requirements, IMHO. – ThomasH Aug 13 '09 at 09:06
  • My previous comment was not directed at you ThomasH. I do see the read-only problem also. I am still looking into class construction to achieve the desired results. Thanks for your input. – kjfletch Aug 13 '09 at 09:22
  • Oups, sorry, now I get it... If you look at my answer, this overcomes the read-only issue. And you should be able to add \*iargs, \*\*ikwargs arguments to the *init()* function, and mix and match them with the default kwargs. You would then be able to parameterize instance creation, I guess. – ThomasH Aug 13 '09 at 12:12
5

As others have said, named tuples in Python 2.6/3.x. With older versions, I usually use the Stuff class:

class Stuff(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

john = Stuff(forename='John', surname='Doe')

This doesn't protect you from mispellings though. There's also a recipe for named tuples on ActiveState:

http://code.activestate.com/recipes/500261/

fraca7
  • 1,178
  • 5
  • 11
3

This is following up on Cide's answer (and probably only interesting for people who want to dig deeper).

I experienced a problem using Cide's updated definition of Struct(), the one using __slots__. The problem is that instances of returned classes have read-only attributes:

>>> MS = Struct('forename','lastname')
>>> m=MS()
>>> m.forename='Jack'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyStruct' object attribute 'forename' is read-only

Seems that __slots__ is blocking instance-level attributes when there are class attributes of same names. I've tried to overcome this by providing an __init__ method, so instance attributes can be set at object creation time:

def Struct1(*args, **kwargs):
    def init(self):
        for k,v in kwargs.items():
            setattr(self, k, v)
    name = kwargs.pop("name", "MyStruct")
    kwargs.update(dict((k, None) for k in args))
    return type(name, (object,), {'__init__': init, '__slots__': kwargs.keys()})

As a net effect the constructed class only sees the __init__ method and the __slots__ member, which is working as desired:

>>> MS1 = Struct1('forename','lastname')
>>> m=MS1()
>>> m.forename='Jack'
>>> m.forename
'Jack'
ThomasH
  • 22,276
  • 13
  • 61
  • 62
  • +1 It seems you have been spending more time thinking about this [mainly] theoretically problem than I have. I will give this a go and play around with it. Thanks. – kjfletch Aug 13 '09 at 19:35
  • You have a missing bracket: Last character of your Struct1() function. – kjfletch Aug 13 '09 at 22:55
3

An update of ThomasH's variant:

def Struct(*args, **kwargs):
    def init(self, *iargs, **ikwargs):
        for k,v in kwargs.items():
            setattr(self, k, v)
        for i in range(len(iargs)):
            setattr(self, args[i], iargs[i])
        for k,v in ikwargs.items():
            setattr(self, k, v)

    name = kwargs.pop("name", "MyStruct")
    kwargs.update(dict((k, None) for k in args))
    return type(name, (object,), {'__init__': init, '__slots__': kwargs.keys()})

This allows parameters (and named parameters) passed into __init__() (without any validation - seems crude):

>>> Person = Struct('fname', 'age')
>>> person1 = Person('Kevin', 25)
>>> person2 = Person(age=42, fname='Terry')
>>> person1.age += 10
>>> person2.age -= 10
>>> person1.fname, person1.age, person2.fname, person2.age
('Kevin', 35, 'Terry', 32)
>>> 

Update

Having a look into how namedtuple() does this in collections.py. The class is created and expanded as a string and evaluated. Also has support for pickling and so on, etc.

kjfletch
  • 5,394
  • 3
  • 32
  • 38
  • +1 I especially like the way how you map positional parameters of instance creation time to positional parameters of class creation time, using the former as values and the latter as keys (*setattr(self, args[i], iargs[i])*). – ThomasH Aug 14 '09 at 09:20
  • I suppose additions can be made to count the number of args to see if the number passed to Struct() match the number passed to __init__(). – kjfletch Aug 14 '09 at 09:48
1

There is namedtuple

>>> from collections import namedtuple
>>> Person = namedtuple("Person", ("forename", "surname"))
>>> john = Person("John", "Doe")
>>> john.forename 
'John'
>>> john.surname 
'Doe'
Otto Allmendinger
  • 27,448
  • 7
  • 68
  • 79
0

The Python package esu brings a struct that can provide almost the same functionality:

from esu import Struct

Customer = Struct(
        'Customer',
        'name', 'address',
        methods={
            'greeting': lambda self: "Hello {}".format(self.__dict__['name'])
        })

dave = Customer()
dave.name = 'Dave'
dave.greeting() # => Hello Dave

from https://torokmark.github.io/post/python-struct/

StandardNerd
  • 4,093
  • 9
  • 46
  • 77
0

Since python 3.3 you can use SimpleNamespace which was created exactly for this use case. Quoting the documentation:

SimpleNamespace may be useful as a replacement for class NS: pass.

In [1]: from types import SimpleNamespace
   ...: 
   ...: foo = SimpleNamespace()
   ...: foo.a = 3
   ...: foo.b = "text"
   ...: foo
Out[1]: namespace(a=3, b='text')

In [2]: foo.__dict__
Out[2]: {'a': 3, 'b': 'text'}
patxiska
  • 134
  • 4