File: how-does-it-work.rst

package info (click to toggle)
mozjs78 78.15.0-7
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 739,892 kB
  • sloc: javascript: 1,344,214; cpp: 1,215,708; python: 526,544; ansic: 433,835; xml: 118,736; sh: 26,176; asm: 16,664; makefile: 11,537; yacc: 4,486; perl: 2,564; ada: 1,681; lex: 1,414; pascal: 1,139; cs: 879; exp: 499; java: 164; ruby: 68; sql: 45; csh: 35; sed: 18; lisp: 2
file content (100 lines) | stat: -rw-r--r-- 4,325 bytes parent folder | download | duplicates (6)
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
.. _how:

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.

Once you apply the ``@attr.s`` decorator to a class, ``attrs`` searches the class object for instances of ``attr.ib``\ s.
Internally they're a representation of the data passed into ``attr.ib`` along with a counter to preserve the order of the attributes.

In order to ensure that subclassing works as you'd expect it to work, ``attrs`` also walks the class hierarchy and collects the attributes of all base classes.
Please note that ``attrs`` does *not* call ``super()`` *ever*.
It will write dunder methods to work on *all* of those attributes which also has performance benefits due to fewer function calls.

Once ``attrs`` knows what attributes it has to work on, it writes the requested dunder methods and -- depending on whether you wish to have a :term:`dict <dict classes>` or :term:`slotted <slotted classes>` class -- creates a new class for you (``slots=True``) or attaches them to the original class (``slots=False``).
While creating new classes is more elegant, we've run into several edge cases surrounding metaclasses that make it impossible to go this route unconditionally.

To be very clear: if you define a class with a single attribute without a default value, the generated ``__init__`` will look *exactly* how you'd expect:

.. doctest::

   >>> import attr, inspect
   >>> @attr.s
   ... class C(object):
   ...     x = attr.ib()
   >>> print(inspect.getsource(C.__init__))
   def __init__(self, x):
       self.x = x
   <BLANKLINE>

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

Depending on whether a class is a dict class or a slotted class, ``attrs`` uses a different technique to circumvent that limitation in the ``__init__`` method.

Once constructed, frozen instances don't differ in any way from regular ones except that you cannot change its attributes.


Dict Classes
++++++++++++

Dict classes -- i.e. regular classes -- simply assign the value directly into the class' eponymous ``__dict__`` (and there's nothing we can do to stop the user to do the same).

The performance impact is negligible.


Slotted Classes
+++++++++++++++

Slotted classes are more complicated.
Here it uses (an aggressively 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 a standard 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.


Summary
+++++++

You should avoid instantiating lots of frozen slotted classes (i.e. ``@attr.s(slots=True, frozen=True)``) in performance-critical code.

Frozen dict classes have barely a performance impact, unfrozen slotted classes are even *faster* than unfrozen dict classes (i.e. regular classes).