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