Welcome, guest | Sign In | My Account | Store | Cart

Python already provides immutable versions of many of the mutable built-in types. Dict is the notable exception. Regardless, here is a protocol that objects may implement that facilitates turning immutable object mutable and vice-versa.

Python, 108 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
"""freeze module

The freeze and unfreeze functions special-case some of the built-in
types.  If these types grow the appropriate methods then that will
become unnecessary.

"""

__all__ = ("Freezable", "UnFreezable", "freeze", "unfreeze")


import builtins
from abc import ABCMeta, abstractmethod


class Freezable(metaclass=ABCMeta):

    @abstractmethod
    def __freeze__(self):
        """Returns an immutable version of this object."""


class UnFreezable(metaclass=ABCMeta):

    @abstractmethod
    def __unfreeze__(self):
        """Returns a mutable version of this object."""


def freeze(obj):
    """Returns the immutable version of the object."""

    if hasattr(type(obj), "__freeze__"):
        return obj.__freeze__()
    try:
        handler = _freeze_registry[type(obj)]
    except KeyError:
        pass
    else:
        return handler(obj)
    #if hasattr(type(obj), "__unfreeze__"):
    #    return obj

    msg = "Don't know how to freeze a {} object"
    raise TypeError(msg.format(type(obj)))


def unfreeze(obj, strict=False):
    if hasattr(type(obj), "__unfreeze__"):
        return obj.__unfreeze__()
    try:
        handler = _unfreeze_registry[type(obj)]
    except KeyError:
        pass
    else:
        return handler(obj)
    #if hasattr(type(obj), "__freeze__"):
    #    return obj

    msg = "Don't know how to unfreeze a {} object"
    raise TypeError(msg.format(type(obj)))


#################################################
# special-casing built-in types

_freeze_registry = {}
_unfreeze_registry = {}
def register(f, cls=None):
    action, typename = f.__name__.split("_")
    if cls is None:
        cls = getattr(builtins, typename)
    if action == "freeze":
        _freeze_registry[cls] = f
        Freezable.register(cls)
    elif action == "unfreeze":
        _unfreeze_registry[cls] = f
        UnFreezable.register(cls)
    else:
        raise TypeError
    return f


@register
def freeze_dict(obj):
    raise NotImplementedError

@register
def unfreeze_dict(obj):
    return obj


@register
def freeze_list(obj):
    return tuple(obj)

@register
def unfreeze_list(obj):
    return obj


@register
def freeze_tuple(obj):
    return obj

@register
def unfreeze_tuple(obj):
    return list(obj)

Important Note

After I submitted this recipe, I found that there is an almost identical PEP for this idea, PEP 351. That PEP was rejected for concerns expressed in this thread. From the little I've read so far I'm not entirely sure it's as bad as all that. I'll update this note if further reading changes my mind.

[update: yeah, the arguments against it center on hashing (not so applicable) and on the idea not being not worth the trouble. If you find this recipe useful, leave a note, maybe with a real-life use-case. At the least, better alternatives for the use-case could develop. At best, the recipe gets validated! :)]

This topic has come up a few times on the various Python mailing lists in the few years that I have been following, so it's certainly not a terribly original idea of mine. I will note, however, that this recipe is strictly the result of a stream-of-consciousness hack-it-out episode. I was pleasantly surprised that the FLUFL had come up with a nearly identical idea over 6 years ago. :)

Hashable

I originally had Freezable as a subclass of collections.abc.Hashable, but figured a frozen object is not necessarily hashable and vice-versa. However, PEP 351 (and related commentary) seems to tie Freezable to Hashable as an invariant. I'm still not sure about that, but I'll be the first to concede that those folks know a lot more than me!

Examples

>>> freeze({})
Traceback (most recent call last):
  ...
NotImplementedError
>>> unfreeze({})
{}
>>> class X(dict):
...   def __freeze__(self):
...     return tuple(sorted(self.items()))
... 
>>> freeze(X(a=1, b=2))
(('a', 1), ('b', 2))