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.
Update As of 2019, I like to use types.SimpleNamespace from the standard library for this instead. This is available from version 3.3 onwards.
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'
For comments, please send me an e-mail.
Related articles
- Profiling Python scripts(6): auto-orient
- Profiling with pyinstrument
- From python script to executable with cython
- On Python speed
- Python 3.11 speed comparison with 3.9