Standard Classes¶
Immutable classes are similar to standard Python classes, but prevent any modification of class attributes after creation. This makes them ideal for defining constants, enumerations, or configurations that should be completely immutable.
>>> from frigid import Class
Basic Usage¶
Immutable classes can be defined using the Class
metaclass. Attributes must
be defined during class creation.
>>> class Constants( metaclass = Class ):
... PI = 3.14159
... E = 2.71828
... PHI = 1.61803
Once defined, these attributes are protected from any modification:
>>> Constants.PI = 3.14 # Attempt to modify
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'PI'.
They cannot be deleted:
>>> del Constants.E # Attempt to delete
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'E'.
And new attributes cannot be added after class creation:
>>> Constants.SQRT2 = 1.4142 # Attempt to add
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'SQRT2'.
Mutable Attributes¶
While classes are generally immutable, specific attributes can be marked as mutable via a metaclass argument:
>>> class Configuration( metaclass = Class, mutables = ( 'debug_level', ) ):
... debug_level = 0 # Mutable attribute
... VERSION = '1.0' # Immutable attribute
Mutable attributes can be modified after class creation:
>>> Configuration.debug_level = 2 # OK to modify mutable attribute
>>> Configuration.debug_level
2
>>> Configuration.VERSION = '2.0' # But not immutable ones
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'VERSION'.
Mutable attributes can also be added after class creation if they are listed in
mutables
:
>>> class DynamicConfig( metaclass = Class, mutables = ( 'future_setting', ) ):
... initial_setting = 'fixed'
>>> DynamicConfig.future_setting = 'dynamic' # OK to add listed attribute
>>> DynamicConfig.another_setting = 'error' # But not unlisted ones
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'another_setting'.
Decorator Support¶
Classes can be modified during creation using decorators. This allows for programmatic addition of attributes before the class becomes immutable.
>>> def add_computed_constants( cls ):
... cls.TAU = cls.PI * 2
... return cls
...
>>> class CircleConstants( metaclass = Class, decorators = ( add_computed_constants, ) ):
... PI = 3.14159
The decorator-added attributes become part of the immutable class:
>>> CircleConstants.TAU
6.28318
>>> CircleConstants.TAU = 6.28 # Attempt to modify
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'TAU'.
Dynamic Docstrings¶
Classes can be given docstrings dynamically at creation time, which can be useful for generating documentation programmatically:
>>> docstring = 'Configuration for database connection.'
>>> class DBConfig( metaclass = Class, docstring = docstring ):
... ''' This docstring will be replaced. '''
... HOST = 'localhost'
... PORT = 5432
>>> DBConfig.__doc__ == docstring
True
Abstract Base Classes¶
The ABCFactory
metaclass creates immutable abstract base classes. This is
particularly useful for defining stable interfaces that should not change after
definition. All of the behaviors, mentioned for standard classes, also apply to
these.
>>> from frigid import ABCFactory
>>> from abc import abstractmethod
>>> class DataStore( metaclass = ABCFactory ):
... @abstractmethod
... def get( self, key ): pass
...
... @abstractmethod
... def put( self, key, value ): pass
...
... ENCODING = 'utf-8'
The abstract methods and class attributes are protected:
>>> # Cannot modify abstract interface
>>> def new_method( self ): pass
>>> DataStore.list_keys = new_method
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'list_keys'.
>>> # Cannot modify class attributes
>>> DataStore.ENCODING = 'ascii'
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'ENCODING'.
Protocol Classes¶
The ProtocolClass
metaclass creates immutable protocol classes, which is
useful for defining static type interfaces. All of the behaviors, mentioned for
standard classes, also apply to these.
>>> from frigid import ProtocolClass
>>> from typing import Protocol
>>> class Comparable( Protocol, metaclass = ProtocolClass ):
... def __lt__( self, other ) -> bool: ...
... def __gt__( self, other ) -> bool: ...
...
... ORDERING = 'natural'
The protocol interface is protected from modification:
>>> # Cannot modify protocol interface
>>> def eq( self, other ) -> bool: ...
>>> Comparable.__eq__ = eq
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute '__eq__'.
>>> # Cannot modify class attributes
>>> Comparable.ORDERING = 'reverse'
Traceback (most recent call last):
...
frigid.exceptions.AttributeImmutabilityError: Cannot assign or delete attribute 'ORDERING'.