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.

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.

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:

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.

classFiber:""" 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. """ifE1<=0:raiseValueError('fiber E1 must be > 0')ifρ<=0:raiseValueError('fiber ρ must be > 0')ifnotisinstance(name,str)andnotlen(name)>0:raiseValueError('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={})>'returntemplate.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.

importoperatorclassFiber(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(ρ)ifE1<=0:raiseValueError('fiber E1 must be > 0')ifρ<=0:raiseValueError('fiber ρ must be > 0')ifnotisinstance(name,str)andnotlen(name)>0:raiseValueError('fiber name must be a non-empty string')returntuple.__new__(Fiber,(E1,ν12,α1,ρ,name))def__repr__(self):""" Create a string representation of the Fiber. """template='<Fiber(E1={}, ν12={}, α1={}, ρ={}, name="{}")>'returntemplate.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]:glassfiberOut[3]:<Fiber(E1=73000.0,ν12=0.33,α1=5.3e-06,ρ=2.6,name="E-glass")>In[4]:glassfiber.E1Out[4]:73000.0In[5]:glassfiber.E1=75000---------------------------------------------------------------------------AttributeErrorTraceback(mostrecentcalllast)<ipython-input-5-639e58cfde02>in<module>()---->1glassfiber.E1=75000AttributeError:can't set attribute

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

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.

classFiber:""" 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 floatE1=float(E1)ν12=float(ν12)α1=float(α1)ρ=float(ρ)# Validate parametersifE1<=0:raiseValueError('fiber E1 must be > 0')ifρ<=0:raiseValueError('fiber ρ must be > 0')ifnotisinstance(name,str)andnotlen(name)>0:raiseValueError('fiber name must be a non-empty string')# Set attributessuper(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="{}")>'returntemplate.format(self.E1,self.ν12,self.α1,self.ρ,self.name)def__setattr__(self,name,value):""" Prevent modification of attributes. """raiseAttributeError('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.