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 three 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:

    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. 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:

    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.
    """

    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.

The way I do it is 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))

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

←  Using gnuplot from Python Improving my Python coding  →