可組合的循環

版本:

0.12

日期:

2024 年 1 月 18 日

文件

https://matplotlib.dev.org.tw/cycler

PyPI

https://pypi.python.org/pypi/Cycler

GitHub

https://github.com/matplotlib/cycler

cycler API

cycler()

從單個位置參數、一對位置參數或關鍵字參數的組合建立一個新的 Cycler 物件。

Cycler(left[, right, op])

可組合的循環。

concat(left, right)

串聯 Cycler,如同使用 itertools.chain 鏈接起來一樣。

cycler 的公開 API 由一個類別 Cycler、一個工廠函式 cycler() 和一個串聯函式 concat() 組成。工廠函式提供一個簡單的介面來建立「基礎」Cycler 物件,而類別則負責組合和迭代邏輯。

Cycler 用法

基礎

單個條目的 Cycler 物件可以用於輕鬆地循環單個樣式。要建立 Cycler,請使用 cycler() 函式將鍵/樣式/關鍵字參數連結到一系列值。鍵必須是可雜湊的(因為它最終將被用作 dict 中的鍵)。

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'])

Cycler 知道其長度和鍵

In [5]: len(color_cycle)
Out[5]: 3

In [6]: color_cycle.keys
Out[6]: {'color'}

迭代此物件將產生一系列以標籤為鍵的 dict 物件

In [7]: for v in color_cycle:
   ...:     print(v)
   ...: 
{'color': 'r'}
{'color': 'g'}
{'color': 'b'}

Cycler 物件可以作為參數傳遞給 cycler(),它會返回一個帶有新標籤但值相同的新的 Cycler

In [8]: cycler(ec=color_cycle)
Out[8]: cycler('ec', ['r', 'g', 'b'])

迭代 Cycler 會產生有限的條目列表,若要取得無限循環,請呼叫 Cycler 物件(類似於產生器)

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'}

組成

可以輕鬆地用單個 for 迴圈取代單個 CyclerCycler 物件的強大之處在於它們可以組合以輕鬆建立複雜的多鍵循環。

加法

可以將具有不同鍵的等長 Cycler 相加,以取得兩個循環的「內部」乘積

In [11]: lw_cycle = cycler(lw=range(1, 4))

In [12]: wc = lw_cycle + color_cycle

結果具有相同的長度,並且具有兩個輸入 Cycler 的聯集鍵。

In [13]: len(wc)
Out[13]: 3

In [14]: wc.keys
Out[14]: {'color', 'lw'}

而迭代結果則是兩個輸入循環的 zip

In [15]: for s in wc:
   ....:     print(s)
   ....: 
{'lw': 1, 'color': 'r'}
{'lw': 2, 'color': 'g'}
{'lw': 3, 'color': 'b'}

與算術一樣,加法是可交換的

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}

為了方便起見,cycler() 函式可以有多個鍵值對,並且會透過加法自動將它們組合成單個 Cycler

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}

乘法

可以將任何一對 Cycler 相乘

In [21]: m_cycle = cycler(marker=['s', 'o'])

In [22]: m_c = m_cycle * color_cycle

這會產生兩個循環的「外積」(與 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'}

請注意,與加法不同,乘法不可交換(類似於矩陣)

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'}

整數乘法

Cycler 也可以乘以整數值以增加長度。

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'])

串聯

Cycler 物件可以透過 Cycler.concat() 方法串聯

In [30]: color_cycle.concat(color_cycle)
Out[30]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])

或頂層 concat() 函式

In [31]: from cycler import concat

In [32]: concat(color_cycle, color_cycle)
Out[32]: cycler('color', ['r', 'g', 'b', 'r', 'g', 'b'])

切片

可以使用 slice 物件對循環進行切片

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'])

以將循環的子集作為新的 Cycler 返回。

檢查 Cycler

要檢查轉置 Cycler 的值,請使用 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']}

這個 dict 可以變更,並用於建立具有更新值的新的 Cycler

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']))

範例

我們可以使用 Cycler 實例來循環一個或多個 kwargplot

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)

原始碼pnghires.pngpdf

_images/index-1.png
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)

原始碼pnghires.pngpdf

_images/index-2.png

持久循環

透過字典查找將給定的標籤與樣式關聯,並動態產生該映射會很有用。這可以使用 defaultdict 輕鬆完成。

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

取得有限的樣式集

In [43]: finite_cy_iter = iter(cyl)

In [44]: dd_finite = defaultdict(lambda : next(finite_cy_iter))

或重複使用

In [45]: loop_cy_iter = cyl()

In [46]: dd_loop = defaultdict(lambda : next(loop_cy_iter))

當繪製具有分類和標籤的複雜資料時,這會很有幫助

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])

這將導致每個具有相同 groupdata 都以相同的樣式繪製。

例外

如果將長度不等的 Cycler 加在一起,則會引發 ValueError

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

或者如果組合了兩個具有重疊鍵的循環

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

動機

當繪製多條線時,通常會希望能夠循環使用一個或多個繪圖樣式。在簡單的情況下,可以輕鬆完成

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)

(原始碼, png, hires.png, pdf)

_images/index-3.png

但是,如果您想做更複雜的事情

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)

(原始碼, png, hires.png, pdf)

_images/index-4.png

繪圖邏輯可能會很快變得非常複雜。為了解決這個問題並允許輕鬆循環使用任意 kwargs,開發了 Cycler 類別,這是一個可組合的關鍵字引數迭代器。