Objects

Immutable objects do not allow instance attributes to be assigned, reassigned, or deleted after instantiation. Immutable objects can be created via decoration (by @immutable) or inheritance (from Object).

>>> from frigid import Object, immutable

Decorator

The @immutable decorator can be applied to existing classes to make their instances immutable after initialization:

>>> @immutable
... class Temperature:
...     def __init__( self, kelvin ):
...         if kelvin < 0:
...             raise ValueError( "Temperature cannot be below absolute zero" )
...         self.kelvin = kelvin
...         self.celsius = kelvin - 273.15
...         self.fahrenheit = self.celsius * 9/5 + 32

The class works normally during initialization:

>>> water_boiling = Temperature( 373.15 )
>>> water_boiling.celsius
100.0

But becomes immutable afterwards:

>>> water_boiling.kelvin = 0  # Attempt to modify
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'kelvin'.

The decorator preserves the validation logic in the class:

>>> impossible = Temperature( -1 )  # Attempt invalid initialization
Traceback (most recent call last):
...
ValueError: Temperature cannot be below absolute zero

Mutable Attributes

The mutables argument can allow some attributes to remain mutable after assignment.

>>> @immutable( mutables = ( 'version', ) )
... class VersionedConfig:
...     def __init__( self, name, version ):
...         self.name = name
...         self.version = version
...
>>> config = VersionedConfig( 'MyApp', '1.0.0' )

Reassignment of mutable attribute:

>>> config.version = '1.0.1'  # This works fine
>>> config.version
'1.0.1'

Deletion of mutable attribute:

>>> del config.version  # This works with mutable attributes
>>> hasattr( config, 'version' )
False

Docstrings

The docstring argument can set or override the docstring of the decorated class. This is useful when docstrings need to be computed dynamically:

>>> @immutable( docstring = 'A configuration class with custom documentation.' )
... class DocumentedConfig:
...     '''Original docstring that will be replaced.'''
...     def __init__( self, name ):
...         self.name = name
...
>>> print( DocumentedConfig.__doc__ )
A configuration class with custom documentation.

Slotted Classes

The @immutable decorator works with classes which use __slots__ for attribute storage. Remember to include the _behaviors_ slot:

>>> @immutable
... class Vector:
...     __slots__ = ( 'x', 'y', 'z', '_behaviors_' )
...
...     def __init__( self, x, y, z ):
...         self.x = x
...         self.y = y
...         self.z = z
...
>>> v = Vector( 1, 2, 3 )
>>> v.x = 0  # Attempt to modify
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.

Compatibility

The @immutable decorator cannot be applied to classes that define their own __setattr__ or __delattr__ methods, as this would conflict with the immutability enforcement:

>>> @immutable  # This will fail
... class Mutable:
...     def __setattr__( self, name, value ):
...         # Custom attribute setting logic
...         super( ).__setattr__( name, value )
Traceback (most recent call last):
...
frigid.exceptions.DecoratorCompatibilityError: Cannot decorate class 'Mutable' which defines '__setattr__'.

Base Class

The Object class serves as a base for creating immutable objects. Attributes must be set in the __init__ method of the derived class before calling super( ).__init__( ), after which the object becomes immutable.

Here’s an example of a point class with immutable coordinates:

>>> class Point( Object ):
...     def __init__( self, x, y ):
...         self.x = x
...         self.y = y
...         super( ).__init__( )

The object behaves normally during initialization:

>>> point = Point( 3, 4 )
>>> point.x
3

After initialization, attributes cannot be modified:

>>> point.x = 5
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'x'.

Nor can they cannot be deleted:

>>> del point.y
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'y'.

And new attributes cannot be added:

>>> point.z = 0
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'z'.

Warning

When working with built-in types, such as exception types, in multiple inheritance hierarchies, avoid using the Object base class which uses __slots__. Instead, apply the @accretive decorator directly to your class.

Multiple Inheritance Considerations

When using the Object class with multiple inheritance, be aware of potential layout conflicts with built-in types that have their own memory layout:

>>> # This would raise a TypeError due to memory layout conflict
>>> # class InvalidCombination( BaseException, Object ):
>>> #     pass

Instead, use the @immutable decorator directly:

>>> @immutable
... class ValidException( BaseException ):
...     ''' An exception with immutable behavior. '''
...     pass
...
>>> ex = ValidException( 'Something went wrong' )
>>> ex.context = 'Additional information'
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'context'.