Composable cycles¶
- Version:
0.12
- Datum:
18. Jan 2024
Docs |
|
PyPI |
|
GitHub |
cycler API¶
|
Erstellen Sie ein neues |
|
Zusammensetzbare Zyklen. |
|
Verketten Sie |
Die öffentliche API von cycler besteht aus einer Klasse Cycler, einer Factory-Funktion cycler() und einer Verkettungsfunktion concat(). Die Factory-Funktion bietet eine einfache Schnittstelle zum Erstellen von 'Basis'-Cycler-Objekten, während die Klasse sich um die Kompositions- und Iterationslogik kümmert.
Cycler Verwendung¶
Basis¶
Ein einzelnes Cycler-Objekt kann verwendet werden, um einfach über einen einzelnen Stil zu iterieren. Um den Cycler zu erstellen, verwenden Sie die Funktion cycler(), um ein Schlüsselwortargument/Stil/Schlüsselwort an eine Reihe von Werten zu binden. Der Schlüssel muss hashbar sein (da er schließlich als Schlüssel in einem dict verwendet wird).
In [1]: from __future__ import print_function
In [2]: from cycler import cycler
In [3]: color_cycle = cycler(color=['r', 'g', 'b'])
In [4]: color_cycle
Out[4]: cycler('color', ['r', 'g', 'b'])
Der Cycler kennt seine Länge und Schlüssel
In [5]: len(color_cycle)
Out[5]: 3
In [6]: color_cycle.keys
Out[6]: {'color'}
Die Iteration über dieses Objekt liefert eine Reihe von dict-Objekten, die mit dem Label verschlagwortet sind
In [7]: for v in color_cycle:
...: print(v)
...:
{'color': 'r'}
{'color': 'g'}
{'color': 'b'}
Cycler-Objekte können als Argument an cycler() übergeben werden, was einen neuen Cycler mit einem neuen Label, aber denselben Werten zurückgibt.
In [8]: cycler(ec=color_cycle)
Out[8]: cycler('ec', ['r', 'g', 'b'])
Die Iteration über einen Cycler ergibt die endliche Liste von Einträgen. Um einen unendlichen Zyklus zu erhalten, rufen Sie das Cycler-Objekt auf (ähnlich einem Generator)
In [9]: cc = color_cycle()
In [10]: for j, c in zip(range(5), cc):
....: print(j, c)
....:
0 {'color': 'r'}
1 {'color': 'g'}
2 {'color': 'b'}
3 {'color': 'r'}
4 {'color': 'g'}
Komposition¶
Ein einzelner Cycler kann genauso leicht durch eine einzige for-Schleife ersetzt werden. Die Stärke von Cycler-Objekten liegt darin, dass sie kombiniert werden können, um einfach komplexe Multi-Key-Zyklen zu erstellen.
Addition¶
Gleich lange Cyclers mit unterschiedlichen Schlüsseln können addiert werden, um das 'innere' Produkt zweier Zyklen zu erhalten
In [11]: lw_cycle = cycler(lw=range(1, 4))
In [12]: wc = lw_cycle + color_cycle
Das Ergebnis hat die gleiche Länge und Schlüssel, die die Vereinigung der beiden Eingabe-Cyclers sind.
In [13]: len(wc)
Out[13]: 3
In [14]: wc.keys
Out[14]: {'color', 'lw'}
und die Iteration über das Ergebnis ist das Zip der beiden Eingabezyklen
In [15]: for s in wc:
....: print(s)
....:
{'lw': 1, 'color': 'r'}
{'lw': 2, 'color': 'g'}
{'lw': 3, 'color': 'b'}
Wie bei der Arithmetik ist die Addition kommutativ
In [16]: lw_c = lw_cycle + color_cycle
In [17]: c_lw = color_cycle + lw_cycle
In [18]: for j, (a, b) in enumerate(zip(lw_c, c_lw)):
....: print('({j}) A: {A!r} B: {B!r}'.format(j=j, A=a, B=b))
....:
(0) A: {'lw': 1, 'color': 'r'} B: {'color': 'r', 'lw': 1}
(1) A: {'lw': 2, 'color': 'g'} B: {'color': 'g', 'lw': 2}
(2) A: {'lw': 3, 'color': 'b'} B: {'color': 'b', 'lw': 3}
Zur Vereinfachung kann die Funktion cycler() mehrere Schlüssel-Wert-Paare aufnehmen und sie automatisch durch Addition zu einem einzigen Cycler kombinieren
In [19]: wc = cycler(c=['r', 'g', 'b'], lw=range(3))
In [20]: for s in wc:
....: print(s)
....:
{'c': 'r', 'lw': 0}
{'c': 'g', 'lw': 1}
{'c': 'b', 'lw': 2}
Multiplikation¶
Jedes Paar von Cyclers kann multipliziert werden
In [21]: m_cycle = cycler(marker=['s', 'o'])
In [22]: m_c = m_cycle * color_cycle
was das 'äußere Produkt' der beiden Zyklen ergibt (dasselbe wie itertools.product())
In [23]: len(m_c)
Out[23]: 6
In [24]: m_c.keys
Out[24]: {'color', 'marker'}
In [25]: for s in m_c:
....: print(s)
....:
{'marker': 's', 'color': 'r'}
{'marker': 's', 'color': 'g'}
{'marker': 's', 'color': 'b'}
{'marker': 'o', 'color': 'r'}
{'marker': 'o', 'color': 'g'}
{'marker': 'o', 'color': 'b'}
Beachten Sie, dass die Multiplikation im Gegensatz zur Addition nicht kommutativ ist (wie bei Matrizen)
In [26]: c_m = color_cycle * m_cycle
In [27]: for j, (a, b) in enumerate(zip(c_m, m_c)):
....: print('({j}) A: {A!r} B: {B!r}'.format(j=j, A=a, B=b))
....:
(0) A: {'color': 'r', 'marker': 's'} B: {'marker': 's', 'color': 'r'}
(1) A: {'color': 'r', 'marker': 'o'} B: {'marker': 's', 'color': 'g'}
(2) A: {'color': 'g', 'marker': 's'} B: {'marker': 's', 'color': 'b'}
(3) A: {'color': 'g', 'marker': 'o'} B: {'marker': 'o', 'color': 'r'}
(4) A: {'color': 'b', 'marker': 's'} B: {'marker': 'o', 'color': 'g'}
(5) A: {'color': 'b', 'marker': 'o'} B: {'marker': 'o', 'color': 'b'}
Ganzzahlige Multiplikation¶
Cyclers können auch mit Ganzzahlen multipliziert werden, um die Länge zu erhöhen.
In [28]: color_cycle * 2
Out[28]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])
In [29]: 2 * color_cycle
Out[29]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])
Verkettung¶
Cycler-Objekte können entweder über die Methode Cycler.concat()
In [30]: color_cycle.concat(color_cycle)
Out[30]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])
oder die Top-Level-Funktion concat() verkettet werden
In [31]: from cycler import concat
In [32]: concat(color_cycle, color_cycle)
Out[32]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])
Slicing¶
Zyklen können mit slice-Objekten geschnitten werden
In [33]: color_cycle[::-1]
Out[33]: cycler('color', ['b', 'g', 'r'])
In [34]: color_cycle[:2]
Out[34]: cycler('color', ['r', 'g'])
In [35]: color_cycle[1:]
Out[35]: cycler('color', ['g', 'b'])
um eine Teilmenge des Zyklus als neuen Cycler zurückzugeben.
Inspektion des Cycler¶
Um die Werte des transponierten Cycler zu inspizieren, verwenden Sie die Methode Cycler.by_key
In [36]: c_m.by_key()
Out[36]:
{'marker': ['s', 'o', 's', 'o', 's', 'o'],
'color': ['r', 'r', 'g', 'g', 'b', 'b']}
Dieses dict kann mutiert und verwendet werden, um einen neuen Cycler mit den aktualisierten Werten zu erstellen
In [37]: bk = c_m.by_key()
In [38]: bk['color'] = ['green'] * len(c_m)
In [39]: cycler(**bk)
Out[39]: (cycler('marker', ['s', 'o', 's', 'o', 's', 'o']) + cycler('color', ['green', 'green', 'green', 'green', 'green', 'green']))
Beispiele¶
Wir können Cycler-Instanzen verwenden, um über ein oder mehrere kwarg für plot zu iterieren
from cycler import cycler
from itertools import cycle
fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True,
figsize=(8, 4))
x = np.arange(10)
color_cycle = cycler(c=['r', 'g', 'b'])
for i, sty in enumerate(color_cycle):
ax1.plot(x, x*(i+1), **sty)
for i, sty in zip(range(1, 5), cycle(color_cycle)):
ax2.plot(x, x*i, **sty)
(Quellcode, png, hires.png, pdf)
from cycler import cycler
from itertools import cycle
fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True,
figsize=(8, 4))
x = np.arange(10)
color_cycle = cycler(c=['r', 'g', 'b'])
ls_cycle = cycler('ls', ['-', '--'])
lw_cycle = cycler('lw', range(1, 4))
sty_cycle = ls_cycle * (color_cycle + lw_cycle)
for i, sty in enumerate(sty_cycle):
ax1.plot(x, x*(i+1), **sty)
sty_cycle = (color_cycle + lw_cycle) * ls_cycle
for i, sty in enumerate(sty_cycle):
ax2.plot(x, x*(i+1), **sty)
(Quellcode, png, hires.png, pdf)
Persistente Zyklen¶
Es kann nützlich sein, ein gegebenes Label mit einem Stil über eine Wörterbuchsuche zu verknüpfen und diese Zuordnung dynamisch zu generieren. Dies kann einfach mit einem defaultdict erreicht werden
In [40]: from cycler import cycler as cy
In [41]: from collections import defaultdict
In [42]: cyl = cy('c', 'rgb') + cy('lw', range(1, 4))
Um eine endliche Menge von Stilen zu erhalten
In [43]: finite_cy_iter = iter(cyl)
In [44]: dd_finite = defaultdict(lambda : next(finite_cy_iter))
oder wiederholend
In [45]: loop_cy_iter = cyl()
In [46]: dd_loop = defaultdict(lambda : next(loop_cy_iter))
Dies kann hilfreich sein, wenn komplexe Daten geplottet werden, die sowohl eine Klassifizierung als auch ein Label haben
finite_cy_iter = iter(cyl)
styles = defaultdict(lambda : next(finite_cy_iter))
for group, label, data in DataSet:
ax.plot(data, label=label, **styles[group])
wodurch jedes data mit demselben group mit demselben Stil geplottet wird.
Ausnahmen¶
Ein ValueError wird ausgelöst, wenn ungleiche Cyclers addiert werden
In [47]: cycler(c=['r', 'g', 'b']) + cycler(ls=['-', '--'])
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[47], line 1
----> 1 cycler(c=['r', 'g', 'b']) + cycler(ls=['-', '--'])
File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:283, in Cycler.__add__(self, other)
275 """
276 Pair-wise combine two equal length cyclers (zip).
277
(...)
280 other : Cycler
281 """
282 if len(self) != len(other):
--> 283 raise ValueError(
284 f"Can only add equal length cycles, not {len(self)} and {len(other)}"
285 )
286 return Cycler(
287 cast(Cycler[Union[K, L], Union[V, U]], self),
288 cast(Cycler[Union[K, L], Union[V, U]], other),
289 zip
290 )
ValueError: Can only add equal length cycles, not 3 and 2
oder wenn zwei Zyklen mit überlappenden Schlüsseln kombiniert werden
In [48]: color_cycle = cycler(c=['r', 'g', 'b'])
In [49]: color_cycle + color_cycle
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[49], line 1
----> 1 color_cycle + color_cycle
File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:286, in Cycler.__add__(self, other)
282 if len(self) != len(other):
283 raise ValueError(
284 f"Can only add equal length cycles, not {len(self)} and {len(other)}"
285 )
--> 286 return Cycler(
287 cast(Cycler[Union[K, L], Union[V, U]], self),
288 cast(Cycler[Union[K, L], Union[V, U]], other),
289 zip
290 )
File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:179, in Cycler.__init__(self, left, right, op)
176 else:
177 self._right = None
--> 179 self._keys: set[K] = _process_keys(self._left, self._right)
180 self._op: Any = op
File ~/code/cycler/venv/lib64/python3.12/site-packages/cycler/__init__.py:84, in _process_keys(left, right)
82 r_key: set[K] = set(r_peek.keys())
83 if l_key & r_key:
---> 84 raise ValueError("Can not compose overlapping cycles")
85 return l_key | r_key
ValueError: Can not compose overlapping cycles
Motivation¶
Beim Plotten mehrerer Linien ist es üblich, über einen oder mehrere Künstlerstile iterieren zu wollen. Für einfache Fälle kann dies ohne zu große Probleme erfolgen
fig, ax = plt.subplots(tight_layout=True)
x = np.linspace(0, 2*np.pi, 1024)
for i, (lw, c) in enumerate(zip(range(4), ['r', 'g', 'b', 'k'])):
ax.plot(x, np.sin(x - i * np.pi / 4),
label=r'$\phi = {{{0}}} \pi / 4$'.format(i),
lw=lw + 1,
c=c)
ax.set_xlim([0, 2*np.pi])
ax.set_title(r'$y=\sin(\theta + \phi)$')
ax.set_ylabel(r'[arb]')
ax.set_xlabel(r'$\theta$ [rad]')
ax.legend(loc=0)
(Quellcode, png, hires.png, pdf)
Wenn Sie jedoch etwas Komplexeres tun möchten
fig, ax = plt.subplots(tight_layout=True)
x = np.linspace(0, 2*np.pi, 1024)
for i, (lw, c) in enumerate(zip(range(4), ['r', 'g', 'b', 'k'])):
if i % 2:
ls = '-'
else:
ls = '--'
ax.plot(x, np.sin(x - i * np.pi / 4),
label=r'$\phi = {{{0}}} \pi / 4$'.format(i),
lw=lw + 1,
c=c,
ls=ls)
ax.set_xlim([0, 2*np.pi])
ax.set_title(r'$y=\sin(\theta + \phi)$')
ax.set_ylabel(r'[arb]')
ax.set_xlabel(r'$\theta$ [rad]')
ax.legend(loc=0)
(Quellcode, png, hires.png, pdf)
kann die Plot-Logik schnell sehr aufwendig werden. Um dies zu beheben und das einfache Iterieren über beliebige kwargs zu ermöglichen, wurde die Klasse Cycler, ein zusammensetzbarer Keyword-Argument-Iterator, entwickelt.