Composable cycles¶
- Version:
0.12
- Datum:
18. Januar 2024
Docs |
|
PyPI |
|
GitHub |
cycler API¶
|
Erstellt ein neues |
|
Zusammensetzbare Zyklen. |
|
Verkettet |
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 einen Schlüssel/Stil/Schlüsselwortargument an eine Reihe von Werten zu koppeln. 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 seine 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 indiziert 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 (wie einen 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 einzelne for-Schleife ersetzt werden. Die Stärke von Cycler-Objekten liegt darin, dass sie zusammengesetzt werden können, um einfach komplexe Mehrfachschlüsselzyklen zu erstellen.
Addition¶
Cycler mit gleicher Länge und 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-Cycler sind.
In [13]: len(wc)
Out[13]: 3
In [14]: wc.keys
Out[14]: {'color', 'lw'}
und die Iteration über das Ergebnis ist die Kombination 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 entgegennehmen und sie automatisch durch Addition zu einem einzigen Cycler zusammensetzen
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 Cycler 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 (wie bei 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¶
Cycler 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 zum Erstellen eines neuen Cycler mit den aktualisierten Werten verwendet werden
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, einem gegebenen Label über ein Wörterbuch-Lookup einen Stil zuzuordnen und diese Zuordnung dynamisch zu generieren. Dies kann leicht 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 sich 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])
was dazu führt, dass jedes data mit demselben group mit demselben Stil geplottet wird.
Ausnahmen¶
Ein ValueError wird ausgelöst, wenn Cycler mit ungleicher Länge zusammen 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 komponiert 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 können. Für einfache Fälle kann dies ohne große Schwierigkeiten geschehen
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 Komplizierteres 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 kompliziert werden. Um dies zu beheben und eine einfache Iteration über beliebige kwargs zu ermöglichen, wurde die Klasse Cycler, ein komponierbarer Schlüsselwortargument-Iterator, entwickelt.