Skip to content

Commit

Permalink
Document immutability better
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Aug 27, 2016
1 parent eaa8c0d commit ce0f82a
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 29 deletions.
50 changes: 28 additions & 22 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ By default, all features are added, so you immediately have a fully functional d

As shown, the generated ``__init__`` method allows for both positional and keyword arguments.

If playful naming turns you off, ``attrs`` comes with no-nonsense aliases:
If playful naming turns you off, ``attrs`` comes with serious business aliases:

.. doctest::

Expand Down Expand Up @@ -444,16 +444,35 @@ Slot classes are a little different than ordinary, dictionary-backed classes:
All in all, setting ``slots=True`` is usually a very good idea.


Other Goodies
-------------
Immutability
------------

Do you like Rich Hickey?
I'm glad to report that Clojure's `core feature <https://clojuredocs.org/clojure.core/assoc>`_ is part of ``attrs``: :func:`attr.assoc`!
I guess that means Clojure can be shut down now, sorry Rich!
Sometimes you have instances that shouldn't be changed after instantiation.
Immutability is especially popular in functional programming and is generally a very good thing.
If you'd like to enforce it, ``attrs`` will try to help:

.. doctest::

>>> @attr.s
>>> @attr.s(frozen=True)
... class C(object):
... x = attr.ib()
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
...
attr.exceptions.FrozenInstanceError: can't set attribute
>>> i.x
1

Please note that true immutability is impossible in Python but it will :ref:`get <how-frozen>` you 99% there.
By themselves, immutable classes are useful for long-lived objects that should never change; like configurations for example.

In order to use them in regular program flow, you'll need a way to easily create new instances with changed attributes.
In Clojure that function is called `assoc <https://clojuredocs.org/clojure.core/assoc>`_ and ``attrs`` shamelessly imitates it: :func:`attr.assoc`:

.. doctest::

>>> @attr.s(frozen=True)
... class C(object):
... x = attr.ib()
... y = attr.ib()
Expand All @@ -466,22 +485,9 @@ I guess that means Clojure can be shut down now, sorry Rich!
>>> i1 == i2
False

If you're still not convinced that Python + ``attrs`` is the better Clojure, maybe immutable-ish classes can change your mind:

.. doctest::

>>> @attr.s(frozen=True)
... class C(object):
... x = attr.ib()
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
...
attr.exceptions.FrozenInstanceError: can't set attribute
>>> i.x
1
>>> attr.assoc(i, x=2).x
2
Other Goodies
-------------

Sometimes you may want to create a class programmatically.
``attrs`` won't let you down and gives you :func:`attr.make_class` :
Expand Down
38 changes: 37 additions & 1 deletion docs/how-does-it-work.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
How Does It Work?
=================


Boilerplate
-----------

``attrs`` certainly isn't the first library that aims to simplify class definition in Python.
But its **declarative** approach combined with **no runtime overhead** lets it stand out.

Expand Down Expand Up @@ -33,8 +37,40 @@ No magic, no meta programming, no expensive introspection at runtime.

Everything until this point happens exactly *once* when the class is defined.
As soon as a class is done, it's done.
And it's just a regular Python class like any other, except for a single ``__attrs_attrs__`` attribute that can be used for introspection or for writing your own tools and decorators on top of ``attrs`` (like :func:`attr.asdict`.
And it's just a regular Python class like any other, except for a single ``__attrs_attrs__`` attribute that can be used for introspection or for writing your own tools and decorators on top of ``attrs`` (like :func:`attr.asdict`).

And once you start instantiating your classes, ``attrs`` is out of your way completely.

This **static** approach was very much a design goal of ``attrs`` and what I strongly believe makes it distinct.


.. _how-frozen:

Immutability
------------

In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises a :exc:`attr.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute.

In order to circumvent that ourselves in ``__init__``, ``attrs`` uses (an agressively cached) :meth:`object.__setattr__` to set your attributes. This is (still) slower than a plain assignment:

.. code-block:: none
$ pyperf timeit --rigorous \
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True)" \
"C(1, 2, 3)"
........................................
Median +- std dev: 378 ns +- 12 ns
$ pyperf timeit --rigorous \
-s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \
"C(1, 2, 3)"
........................................
Median +- std dev: 676 ns +- 16 ns
So on my notebook the difference is about 300 nanoseconds (1 second is 1,000,000,000 nanoseconds).
It's certainly something you'll feel in a hot loop but shouldn't matter in normal code.
Pick what's more important to you.

****

Once constructed, frozen instances differ in no way from regular ones except that you cannot change its attributes.
12 changes: 6 additions & 6 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
:class:`properties <property>`).
If *these* is not `None`, the class body is *ignored*.
:type these: :class:`dict` of :class:`str` to :func:`attr.ib`
:param str repr_ns: When using nested classes, there's no way in Python 2
Expand Down Expand Up @@ -200,15 +201,14 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
2. True immutability is impossible in Python.
3. This *does* have a minor a runtime performance impact when
initializing new instances. In other words: ``__init__`` is
slightly slower with ``frozen=True``.
3. This *does* have a minor a runtime performance :ref:`impact
<how-frozen>` when initializing new instances. In other words:
``__init__`` is slightly slower with ``frozen=True``.
.. _slots: https://docs.python.org/3.5/reference/datamodel.html#slots
.. versionadded:: 16.0.0 *slots*
.. versionadded:: 16.1.0 *frozen*
.. versionadded:: 16.0.0 *slots*
.. versionadded:: 16.1.0 *frozen*
"""
def wrap(cls):
if getattr(cls, "__class__", None) is None:
Expand Down

0 comments on commit ce0f82a

Please sign in to comment.