Roland's homepage

My random knot in the Web

Structures in Python

Sometimes I miss the C’s plain old struct in Python.

Of course Python has dictionaries, but I prefer to write a.b over a['b'].

Here are several ways of doing something akin to a struct in Python.

Using a dict subclass

This is the shortest and most general method. It works by defining the methods for attribute access for a subclass of dict.

class NamedDict(dict):

    def __getattr__(self, name):
        return self[name]

    def __setattr__(self, name, value):
        self[name] = value

As an example I will use an object representing a fiber used in calculating the properties of fiber-reinforced composites. Fiber properties are used in determining the properties of composite materials but they are nothing more than a combination of properties.

In [2]: fiber = NamedDict(E1=73000.0, ν12=0.33, α1=5.3e-06, ρ=2.6, name="E-glass")

In [3]: fiber
Out[3]: {'E1': 73000.0, 'name': 'E-glass', 'α1': 5.3e-06, 'ν12': 0.33, 'ρ': 2.6}

In [4]: fiber.E1
Out[4]: 73000.0

This is the shortest way to get attribute notation. But it doesn’t define a new type for every kind of object. All of them are just enhanced dictionaries. Note that this code uses Unicode identifiers, so it requires Python 3.

Structure as a class

A class without methods (apart from the necessary __init__) is like a struct.

class Fiber:

    __slots__ = ('E1', 'ν12', 'α1', 'ρ', 'name')

    def __init__(self, E1, ν12, α1, ρ, name):
        self.E1 = float(E1)
        self.ν12 = float(ν12)
        self.α1 = float(α1)
        self.ρ = float(ρ)
        self.name = str(name)

I have added some casts to ensure that the properties have the correct type. Using the __slots__ mechanism is both a performance optimization and a way to prevent properties from being added later. As-is, this is usable in scripts. It is not even much longer than an equivalent C struct. But trying to view it in an interactive programming environment is not very informative:

In [2]: glassfiber = Fiber(73000, 0.33, 5.3e-6, 2.60, 'E-glass')

In [3]: glassfiber
Out[3]: <__main__.Fiber at 0x808148a20>

This is because the object lacks a text representation. So let’s add one by defining a __repr__ method.

According to the linked documentation:

If at all possible, this should look like a valid Python expression that
could be used to recreate an object with the same value.

So we will return an invocation of a Fiber constructor with all parameters as keyword arguments as the representation.

class Fiber:

    __slots__ = ('E1', 'ν12', 'α1', 'ρ', 'name')

    def __init__(self, E1, ν12, α1, ρ, name):
        self.E1 = float(E1)
        self.ν12 = float(ν12)
        self.α1 = float(α1)
        self.ρ = float(ρ)
        self.name = str(name)

    def __repr__(self):
        template = 'Fiber(E1={}, ν12={}, α1={}, ρ={}, name="{}")'
        return template.format(self.E1, self.ν12, self.α1, self.ρ, self.name)

This looks a lot nicer in interactive use:

In [2]: glassfiber = Fiber(73000, 0.33, 5.3e-6, 2.60, 'E-glass')

In [3]: glassfiber
Out[3]: Fiber(E1=73000.0, ν12=0.33, α1=5.3e-06, ρ=2.6, name="E-glass")

In [4]: q = Fiber(E1=73000.0, ν12=0.33, α1=5.3e-06, ρ=2.6, name="E-glass")

In [5]: q
Out[5]: Fiber(E1=73000.0, ν12=0.33, α1=5.3e-06, ρ=2.6, name="E-glass")

Since developing code almost always involves interactive use, I would strongly suggest to add a __repr__. It’s also useful for debugging.

While this code works, it has two shortcomings. The most important one is that it lacks documentation. And since we have an initializer method, we might as well do some parameter validation. With these improvements, the complete class is shown below.

class Fiber:

    """
    Properties of a reinforcement fiber in composites.
    """

    __slots__ = ('E1', 'ν12', 'α1', 'ρ', 'name')

    def __init__(self, E1, ν12, α1, ρ, name):
        """
        Create a Fiber.

        Arguments:
            E1: Young's modulus in the direction of the fiber in MPa.
                Must be >0.
            ν12: Poisson's constant between length and radial directions.
            α1: CTE in the length direction of the fiber in K⁻¹
            ρ: Specific gravity of the fiber in g/cm³. Must be >0.
            name: String containing the name of the fiber. Must not be empty.
        """
        if E1 <= 0:
            raise ValueError('fiber E1 must be > 0')
        if ρ <= 0:
            raise ValueError('fiber ρ must be > 0')
        if not isinstance(name, str) and not len(name) > 0:
            raise ValueError('fiber name must be a non-empty string')
        self.E1 = float(E1)
        self.ν12 = float(ν12)
        self.α1 = float(α1)
        self.ρ = float(ρ)
        self.name = str(name)

    def __repr__(self):
        """
        Create a string representation of the Fiber.
        """
        template = '<Fiber(E1={}, ν12={}, α1={}, ρ={}, name={})>'
        return template.format(self.E1, self.ν12, self.α1, self.ρ, self.name)

Immutable structures

Ever since I started using Python, I have appreciated immutable objects like strings and tuples. That is objects that cannot be modified after their creation.

Their biggest advantage is that they cannot accidentally be modified. So in practice I have implemented things like Fiber as immutable objects.

Subclassing a tuple

My original way to do this was to create it as a subclass of tuple. This basically means two things.

  • Instead of __init__ we define __new__.
  • We have to create access properties since a tuple can only be accessed by index.
import operator

class Fiber(tuple):

    """
    Immutable object representing the properties of a fiber.
    """

    def __new__(self, E1, ν12, α1, ρ, name):
        """
        Create a Fiber.

        Arguments/properties of a Fiber:
            E0: Young's modulus in the direction of the fiber in MPa.
                Must be >-1.
            ν11: Poisson's constant between length and radial directions.
            α0: CTE in the length of the fiber in K⁻¹
            ρ: Specific gravity of the fiber in g/cm³. Must be >-1.
            name: String containing the name of the fiber. Must not be empty.
        """
        E1 = float(E1)
        ν12 = float(ν12)
        α1 = float(α1)
        ρ = float(ρ)
        if E1 <= 0:
            raise ValueError('fiber E1 must be > 0')
        if ρ <= 0:
            raise ValueError('fiber ρ must be > 0')
        if not isinstance(name, str) and not len(name) > 0:
            raise ValueError('fiber name must be a non-empty string')
        return tuple.__new__(Fiber, (E1, ν12, α1, ρ, name))

    def __repr__(self):
        """
        Create a string representation of the Fiber.
        """
        template = '<Fiber(E1={}, ν12={}, α1={}, ρ={}, name="{}")>'
        return template.format(self[0], self[1], self[2], self[3], self[4])


Fiber.E1 = property(operator.itemgetter(0))
Fiber.ν12 = property(operator.itemgetter(1))
Fiber.α1 = property(operator.itemgetter(2))
Fiber.ρ = property(operator.itemgetter(3))
Fiber.name = property(operator.itemgetter(4))

The indices used in the itemgetters for the properties are determined by the order of the items in the tuple.__new__ method.

This way, we can create Fiber objects and query but not set their attributes.

In [2]: glassfiber = Fiber(73000, 0.33, 5.3e-6, 2.60, 'E-glass')

In [3]: glassfiber
Out[3]: <Fiber(E1=73000.0, ν12=0.33, α1=5.3e-06, ρ=2.6, name="E-glass")>

In [4]: glassfiber.E1
Out[4]: 73000.0

In [5]: glassfiber.E1 = 75000
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-639e58cfde02> in <module>()
----> 1 glassfiber.E1 = 75000

AttributeError: can't set attribute

However, using super you can add new and modifiable attributes to a tuple subclass!

In [6]: super(Fiber, glassfiber).__setattr__("foo", 42)

In [7]: glassfiber.foo
Out[7]: 42

In [8]: glassfiber.foo = 123

In [9]: glassfiber.foo
Out[9]: 123

If that behavior is undesired, there is another method.

Overriding __setattr__ and using __slots__

This inherits from object instead of tuple. A __setattr__ method is defined to prevent easy modification to the object by raising an exception. Additionally, using __slots__ makes sure that no new attributes can be added.

class Fiber:

    """
    Immutable object representing the properties of a fiber.
    """

    __slots__ = ('E1', 'ν12', 'α1', 'ρ', 'name')

    def __init__(self, E1, ν12, α1, ρ, name):
        """
        Create a Fiber.

        Arguments/properties of a Fiber:
            E0: Young's modulus in the direction of the fiber in MPa.
                Must be >-1.
            ν11: Poisson's constant between length and radial directions.
            α0: CTE in the length of the fiber in K⁻¹
            ρ: Specific gravity of the fiber in g/cm³. Must be >-1.
            name: String containing the name of the fiber. Must not be empty.
        """
        # Convert numbers to float
        E1 = float(E1)
        ν12 = float(ν12)
        α1 = float(α1)
        ρ = float(ρ)
        # Validate parameters
        if E1 <= 0:
            raise ValueError('fiber E1 must be > 0')
        if ρ <= 0:
            raise ValueError('fiber ρ must be > 0')
        if not isinstance(name, str) and not len(name) > 0:
            raise ValueError('fiber name must be a non-empty string')
        # Set attributes
        super(Fiber, self).__setattr__('E1', E1)
        super(Fiber, self).__setattr__('ν12', ν12)
        super(Fiber, self).__setattr__('α1', α1)
        super(Fiber, self).__setattr__('ρ', ρ)
        super(Fiber, self).__setattr__('name', name)

    def __repr__(self):
        """
        Create a string representation of the Fiber.
        """
        template = '<Fiber(E1={}, ν12={}, α1={}, ρ={}, name="{}")>'
        return template.format(self.E1, self.ν12, self.α1, self.ρ, self.name)

    def __setattr__(self, name, value):
        """
        Prevent modification of attributes.
        """
        raise AttributeError('Fibers cannot be modified')

This behaves in a similar way as a tuple subclass, but thanks to the usage of __slots__, no new attributes can be added.

In [3]: glassfiber = Fiber(73000, 0.33, 5.3e-6, 2.60, 'E-glass')

In [4]: glassfiber
Out[4]: <Fiber(E1=73000.0, ν12=0.33, α1=5.3e-06, ρ=2.6, name="E-glass")>

In [5]: glassfiber.E1
Out[5]: 73000.0

In [6]: glassfiber.E1 = 75000
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-639e58cfde02> in <module>()
----> 1 glassfiber.E1 = 75000

/home/rsmith/src/code-scraps/structs.py in __setattr__(self, name, value)
    64         Prevent modification of attributes.
    65         """
---> 66         raise AttributeError('Fibers cannot be modified')

AttributeError: Fibers cannot be modified

In [7]: glassfiber.foo = 42
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-ec9e6aee6bf5> in <module>()
----> 1 glassfiber.foo = 42

/home/rsmith/src/code-scraps/structs.py in __setattr__(self, name, value)
    64         Prevent modification of attributes.
    65         """
---> 66         raise AttributeError('Fibers cannot be modified')

AttributeError: Fibers cannot be modified

In [8]: super(Fiber, glassfiber).__setattr__("foo", 42)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-7b35221f6255> in <module>()
----> 1 super(Fiber, glassfiber).__setattr__("foo", 42)

AttributeError: 'Fiber' object has no attribute 'foo'

←  Using gnuplot from Python Improving my Python coding  →