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.

1
  • 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. Commented Aug 12, 2009 at 12:43

8 Answers 8

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

Sign up to request clarification or add additional context in comments.

2 Comments

Named tuple for older python (works at 2.4) code.activestate.com/recipes/500261
namedtuple is not mutable, so it's not the same as struct
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) 

9 Comments

+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.
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?!
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
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?
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.
|
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/

Comments

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' 

2 Comments

+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.
You have a missing bracket: Last character of your Struct1() function.
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.

2 Comments

+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])).
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__().
1

There is namedtuple

>>> from collections import namedtuple >>> Person = namedtuple("Person", ("forename", "surname")) >>> john = Person("John", "Doe") >>> john.forename 'John' >>> john.surname 'Doe' 

Comments

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/

Comments

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'} 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.