IronPython との付き合いかた（その２）

## はじめに
先日「IronPython との付き合いかた」を投稿しましたが、辞書や pip の扱いなど、割愛した内容を続編としてまとめました。
引き続き、個人の備忘メモとご理解ください。

https://qiita.com/Mt_SQ/items/ddbcf39c19ad6591bd96

以降、上記を「本編」と表記します。
## 確認環境
+ Windows 11 Pro（Ver. 23H2）
+ .NET Runtime 8.0.2 (64-bit)
+ IronPython 3.4.1 (3.4.1.1000)

今回は、加えて Chromebook ASUS CX1500CKA-EJ0015(Celeron N4500、メモリ4GB) の Linux 開発環境（debian 12 + .NET Runtime 8.0.2）でも動作することを確認しています。

## IronPython のバージョンについて
周回遅れのバージョン3.4 とはいえ、IronPython3 のリリースまでこぎつけた開発チームには心から敬意を表します。

バージョンにかかわらず一部の機能は先行導入されており、すでに本編でも利用しています。特に下記機能のおかげでコードが簡潔になります。

+ [PEP 448](https://www.python.org/dev/peps/pep-0448): Additional Unpacking Generalizations（アンパッキング演算子）--> _3.5_ で追加
+ [PEP 498](https://www.python.org/dev/peps/pep-0498): Formatted string literals（f 文字列）--> _3.6_ で追加

## pip について
pip は使えますが、インストールできないパッケージも多く、残念ながら <b>NumPy や Pandas はインストールできません。

公式にはわずかな情報が載っています。

https://github.com/IronLanguages/ironpython3/blob/master/Documentation/package-compatibility.md#package-compatibility


上のリンクを見ると HTTP の活用に便利な _<b>requests</b>_ <sup>[&#10697;](https://pypi.org/project/requests/)</sup> はインストールできそうです。

### pip の準備
#### 準備の前に
_ipy.dll_ の場合 `-m` オプションを使って pip を実行します。

本編のとおり IronPython を起動するためのコマンドラインが長すぎるので、バッチファイルやシェルスクリプトを書きます。
```bat:ipy3.bat（Windows + .NET9 の例）
@dotnet --roll-forward LatestMajor "C:\Users\User\IronPython\3.4\net8.0\ipy.dll" %*
```
Linux の場合は、スクリプトファイルを作って（ファイル名を _ipy3_ としておきます）
```bash:ipy3（Linux + .NET9 の例）
#!/bin/sh
dotnet --roll-forward LatestMajor ~/ironpython/3.4/net8.0/ipy.dll $@
```
 実行権限を与えます。
```bash
chmod +x ipy3
sudo mv ipy3 /usr/local/bin/
```
以降は _ipy3.bat_ により例示します。
#### 最初は _ensurepip_
IronPython 3.4 にも <b>_ensurepip_</b> が入っています。
**最初だけ**次の手順を踏んでください。
```powershell:PowerShell
.\ipy3.bat -m ensurepip
.\ipy3.bat -m pip install --upgrade pip --user
```
スクショを載せておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/7f2695be-2807-94bc-f2d9-05bb75649074.png)
pip のバージョンは 19.1.1 です。

### パッケージのインストール
準備が整ったら _requests_ をインストールしてみます。
```powershell
.\ipy3.bat -m pip install requests --user
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/54a36cef-12cc-ad95-b9dd-c16ec4635141.png)
投稿時における _requests_ の最新バージョンは 2.31.0 ですが、バージョン 2.21.0 がインストールされます。

ダメもとでしたが、_<b>more-itertools</b>_ <sup>[&#10697;](https://pypi.org/project/more-itertools/)</sup> もインストールできました。
```powershell
.\ipy3.bat -m pip install more-itertools --user
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/eb1ce037-f235-809d-fdfd-f8c3c376da84.png)

投稿時における _more-itertools_ の最新バージョンは 10.2.0 ですが、インストールされたバージョンは 7.2.0 です。
これは Python 3.4 をサポートする最終バージョンです。

### 注意点（実際に経験したこと）
+ パッケージの<b><font color=red>最新バージョンがインストールされるとは限らない</b></font>
+ インストールに成功しても、<b><font color=red>実行時エラー</font></b>を吐く場合がある
+ OS など、環境の違いによって成否が異なる場合もある
+ <b><font color=red>「インストールできたらラッキー」</font></b>ぐらいの気構えがよい

## 辞書について
本編で割愛した `dict` と `Dictonary`, `SortedDictonary` との相互変換を含め、辞書の扱いに関するメモです。

### `dict` から `Dictonary`, `SortedDictonary` に変換する
`Dictionary[TKey,TValue]()` の引数として `dict` を渡すとコントラクタとして働きます。
`SortedDictonary` も同様です。

なお、`SortedDictionary` を使用する場合は、本編で触れたように <sup>[&#10697;](https://qiita.com/Mt_SQ/items/ddbcf39c19ad6591bd96#importerror-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)</sup> アセンブリへの参照設定が必要です。
```py:IronPython
import System, clr
clr.AddReference('System.Collections')  # アセンブリへの参照設定

from System.Collections.Generic import Dictionary, SortedDictionary

# とりあえず dict を用意
py_dict = {51: 'イチロー', 55: '松井', 17: '大谷'}

# コンストラクタに渡す
NET_Dict = Dictionary[int, str](py_dict)
NET_SortDict = SortedDictionary[int, str](py_dict)
```
確認します。
```py:IronPython
# 型の確認
type(NET_Dict)
# <class 'Dictionary[int, str]'>
type(NET_SortDict)
# <class 'SortedDictionary[int, str]'>

# 中身の表示
print(*NET_Dict)
# [51, イチロー] [17, 大谷] [55, 松井]
print(*NET_SortDict)
# [17, 大谷] [51, イチロー] [55, 松井]
```
`Dictonary` や `SortedDictonary` は `KeyValuePair`(System.Collections.Generic.KeyValuePair) のコレクションなので、`print` 内で展開できます。

::: note
`print` は `KeyValuePair` を表示する際、リストと同じ様式を使います。
:::

#### `ToDictionary` の利用
LINQ の `ToDictionary` も利用できます。
```py:IronPython
# LINQ の準備
clr.AddReference('System.Core')
clr.ImportExtensions(System.Linq)

# ToDictionary を使う
uniformNum = ['イチローの背番号は51', '松井の背番号は55', '大谷の背番号は17']
sep = 'の背番号は'
NET_DictLinq = uniformNum.ToDictionary(
    lambda s: int(s.split(sep)[1]), lambda s: s.split(sep)[0]
)

type(NET_DictLinq)
# <class 'Dictionary[object, object]'>
print(*NET_DictLinq)
# [51, イチロー] [55, 松井] [17, 大谷]
```
余談ですが、引数なしのインスタンスメソッド `dict.ToDictionary()` は失敗します。
用途はないと思いますが、静的メソッドなら成功しました。
```py:IronPython
# dict にも ToDictionary メソッドが使用できそうだが
'ToDictionary' in dir(py_dict)
# True

# 残念ながら失敗する
py_dict.ToDictionary()
# TypeError: ToDictionary() takes at least 1 argument (0 given)

# ところが静的メソッドは動作する
System.Linq.Enumerable.ToDictionary(py_dict)
# Dictionary[object, object]({51 : 'イチロー', 17 : '大谷', 55 : '松井'})
```
### `Dictonary`, `SortedDictonary` の操作
`dict` と `Dictonary`, `SortedDictonary` は別物ですので属性も異なります。
とはいえ、操作性は概ね直感どおりです。
#### `dict` への変換
```py:IronPython
dict(NET_Dict)
# {51: 'イチロー', 17: '大谷', 55: '松井'}
dict(NET_SortDict)
# {51: 'イチロー', 17: '大谷', 55: '松井'}    dict に変換すると順番は崩れる
```
#### 辞書の追加・更新・削除
追加・更新は `dict` と同じ操作です。
<b>削除に `del` 文は使えない</b>ので `Remove` メソッドで削除します。（削除の例示は省略）
```py:IronPython
# 追加
NET_Dict[11] = '野茂'
NET_SortDict[11] = '野茂'
print(*NET_Dict)
# [51, イチロー] [17, 大谷] [55, 松井] [11, 野茂]
print(*NET_SortDict)
# [11, 野茂] [17, 大谷] [51, イチロー] [55, 松井]

# 更新
NET_Dict[11] = 'ダルビッシュ'
NET_SortDict[11] = 'ダルビッシュ'
print(*NET_Dict)
# [51, イチロー] [17, 大谷] [55, 松井] [11, ダルビッシュ]
print(*NET_SortDict)
# [11, ダルビッシュ] [17, 大谷] [51, イチロー] [55, 松井]
```
#### 要素の取り出し
例として、辞書の要素をタプルのリストにしてみます。
```py:IronPython
# dict に変換して items メソッドを使う方法
[*dict(NET_Dict).items()]
# [(51, 'イチロー'), (17, '大谷'), (11, 'ダルビッシュ'), (55, '松井')]

# Deconstruct メソッドを使う
[*map(lambda kvp: kvp.Deconstruct(), NET_Dict)]
# [(51, 'イチロー'), (17, '大谷'), (55, '松井'), (11, 'ダルビッシュ')]

# 内包表記のほうがスッキリ
[kvp.Deconstruct() for kvp in NET_SortDict]
# [(11, 'ダルビッシュ'), (17, '大谷'), (51, 'イチロー'), (55, '松井')]
```
ちなみに `Deconstruct` メソッドは `KeyValuePair[TKey, TValue]` のインスタンスメソッドですが、IronPython は返値をタプルにします。
```py:IronPython
[*map(lambda kvp: type(kvp.Deconstruct()), NET_Dict)]
# [<class 'tuple'>, <class 'tuple'>, <class 'tuple'>, <class 'tuple'>]
```

## 活用事例
以下の事項を織り込んだ活用事例を考えてみました。
+ 外部パッケージ（_requests_ と _more-itertools_）の利用
+ 辞書の利用
+ LINQ におけるジェネレータの扱い

### 事例　～ 2024年度の３連休以上を探す ～
4月からの新年度で３日以上の連休を拾いあげます。
ありきたりですが、次の手順で進めます。

1. 国民の祝日.csv <sup>[&#10697;](https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv)</sup> を取得（GET リクエスト）して、来年度の範囲を抽出する
1. 来年度の土日と、上記 1. で抽出した祝日とを統合する
1. ３日以上連続する休日を拾う

手順 1 には _requests_ を、手順 3 には _more-itertools_ を利用します。

### 手順 1
結果は次の変数に入ります。
　`pubHolsAll`（1955～2025年の全祝日）
　`pubHols24`（2024/4/1～2025/3/31の祝日）

`pubHolsAll` は `Dictionary[object, object]` ですが、`pubHols24` は `KeyValuePair[object, object]` のイテレータになります。
```py:IronPython
# LINQ 使用の準備
import System, clr
clr.AddReference('System.Core')
clr.ImportExtensions(System.Linq)

# 手順 1
import requests
from System import DateOnly

url = 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv'
pubHolsAll = (
    requests.get(url)                                   # 1955年からの祝日データ(csv)
    .content.decode('cp932')                            # 文字コードは Shift-JIS
    .rstrip()                                           # 末尾の改行文字を除去
    .split('\r\n')[1:]                                  # CRLFで分割, 見出しを除去
    .Select(lambda hol: hol.split(','))                 # 日にちと名称を分割
    .ToDictionary(
        lambda holData: DateOnly.Parse(holData[0]),     # キーは DateOnly
        lambda holData: holData[1]                      # 値は str (祝日等の名称)
    )
)

# 2024年度を抽出
start24 = DateOnly(2024, 4, 1)
start25 = start24.AddYears(1)
pubHols24 = pubHolsAll.Where(lambda hol: start24 <= hol.Key < start25)
```
`pubHolsAll` と `pubHols24` の内容を確認します。
```py:IronPython
# pubHolsAll を確認する
print(len(pubHolsAll))
# 1032
print(pubHolsAll[DateOnly(1955, 5, 3)])
# 憲法記念日
print(pubHolsAll[DateOnly(1966, 10, 10)])
# 体育の日
print(pubHolsAll[DateOnly(2025, 10, 13)])
# スポーツの日

# pubHols24 を確認する
print(*pubHols24, sep='\n')
# [2024/04/29, 昭和の日]
# [2024/05/03, 憲法記念日]
# ・・・ 途中省略 ・・・
# [2025/02/24, 休日]
# [2025/03/20, 春分の日]
```
`pubHolsAll` は日数(1032日)と抽出による確認ですが問題ないようです。
`pubHols24` も想定どおりの結果です。
### 手順 2
24年度の土日（`holidays24`）を `SortedDictionary` として作成し、祝日（`pubHols24` の要素）を上書きすることで統合します。
```py:IronPython
# 手順 2
clr.AddReference('System.Collections')                  # SortedDictionary は要参照設定
from System.Collections.Generic import SortedDictionary
# 24年度の土日を作成
holidays24 = SortedDictionary[DateOnly, str](
    range(start24.DayNumber, start25.DayNumber)         # 24/4/1 から 25/3/31 の DayNumber
    .Select(lambda dNum: DateOnly.FromDayNumber(dNum))  # DayNumber から DateOnly に変換
    .Where(lambda d: not (0 < int(d.DayOfWeek) < 6))    # 土日を抽出 (not 平日)
    .ToDictionary(lambda d: d, lambda d: '')            # キーは DateOnly , 値は str ('')
)
# 祝日等を上書き追加
for hol in pubHols24:
    holidays24[hol.Key] = hol.Value
```

::: note warn
<b>Python と .NET の曜日の扱い</b>
Python の `datetime.date.weekday()` メソッドが返す値は、<b>月曜日が 0</b> です。
.NET の `System.DateOnly.DayOfWeek` プロパティの値（`System.DayOfWeek` 列挙）は、<b>日曜日が 0</b> です。
:::

`holidays24` を出力して、土日と祝日が統合されているか確認します。
```py:IronPython
print(len(holidays24))
# 118
print(*holidays24, sep='\n')
# [2024/04/06, ]
# [2024/04/07, ]
# ・・・ 途中省略 ・・・
# [2024/04/28, ]
# [2024/04/29, 昭和の日]
# [2024/05/03, 憲法記念日]
# ・・・ 途中省略 ・・・
# [2025/03/20, 春分の日]
# [2025/03/22, ]
# [2025/03/23, ]
# [2025/03/29, ]
# [2025/03/30, ]
```
24年度の休日数は118日で、祝日も上書きされています。

### 手順 3
`more_itertools.consecutive_groups` <sup>[&#10697;](https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.consecutive_groups)</sup> は、値が連続する要素ごとにグループ化するジェネレータです。ここに `holidays24` を渡して、３日以上の連休を探します。

先にコードを示し、次の項で簡単に解説します。
```py:IronPython
# 手順 3
from more_itertools import consecutive_groups
# 連続する日でグループ化
moreHols = list(
    consecutive_groups(holidays24, lambda hol: hol.Key.DayNumber)
    .Cast[map]()
    .Select(
        lambda hols: [
            f'{hol.Key.ToString("M/d(ddd)")} {hol.Value}'.strip() for hol in hols
        ]
    )
    .Where(lambda holsList: len(holsList) > 2)
)
```
３日以上の連休（`moreHols`）になっているか出力してみます。
```py:IronPython
from pprint import pprint

pprint(moreHols)
# [['4/27(土)', '4/28(日)', '4/29(月) 昭和の日'],
#  ['5/3(金) 憲法記念日', '5/4(土) みどりの日', '5/5(日) こどもの日', '5/6(月) 休日'],
#  ['7/13(土)', '7/14(日)', '7/15(月) 海の日'],
#  ['8/10(土)', '8/11(日) 山の日', '8/12(月) 休日'],
#  ['9/14(土)', '9/15(日)', '9/16(月) 敬老の日'],
#  ['9/21(土)', '9/22(日) 秋分の日', '9/23(月) 休日'],
#  ['10/12(土)', '10/13(日)', '10/14(月) スポーツの日'],
#  ['11/2(土)', '11/3(日) 文化の日', '11/4(月) 休日'],
#  ['1/11(土)', '1/12(日)', '1/13(月) 成人の日'],
#  ['2/22(土)', '2/23(日) 天皇誕生日', '2/24(月) 休日']]
```
目的の結果を得ることができました。

### LINQ におけるジェネレータの扱いについて
前項「手順 3」の解説を兼ねて `consecutive_groups` の返値を `next` でたどってみます。
```py:IronPython
cg = consecutive_groups(holidays24, lambda hol: hol.Key.DayNumber)
type(cg)
# <class 'generator'>                     consecutive_groups はジェネレータ
type(next(cg))
# <class 'map'>                           呼ぶたびにグループを map で返す
type(next(next(cg)))
# <class 'KeyValuePair[DateOnly, str]'>   map は KeyValuePair を返す
```
`consecutive_groups` がジェネレータであり、返値の構造も確認できました。

上記の `map` を展開して値を出力してみます。
すでに `next(cg)` を２回使っているので３番めのグループから表示されます。
```py:IronPython
print(*next(cg))
# [2024/04/20, ] [2024/04/21, ]
print(*next(cg))
# [2024/04/27, ] [2024/04/28, ] [2024/04/29, 昭和の日]
```
#### ジェネレータに LINQ は使えるか？
ここからが本題です。
手っ取り早く `dir(cg)` で確認します。
```py:IronPython
pprint(dir(cg), width=80, compact=True)
# ['AsParallel', 'AsQueryable', 'Cast', 'Equals', 'GetHashCode', 'GetType',
#  'MemberwiseClone', 'OfType', 'ReferenceEquals', 'ToString', '__class__',
#  '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
#  '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__',
#  '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__',
#  '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code',
#  'gi_frame', 'gi_running', 'send', 'throw']
```
`Cast` や `OfType` が見つかりました。

`dir(cg.Cast[map]())` してみます。
```py:IronPython
pprint(dir(cg.Cast[map]()), width=80, compact=True)
# ['Aggregate', 'All', 'Any', 'Append', 'AsEnumerable', 'AsParallel',
#  'AsQueryable', 'Average', 'Cast', 'Chunk', 'Concat', 'Contains', 'Count',
#  'DefaultIfEmpty', 'Dispose', 'Distinct', 'DistinctBy', 'ElementAt',
#  'ElementAtOrDefault', 'Equals', 'Except', 'ExceptBy', 'First',
# ・・・ 以下省略 ・・・
```
`Cast[map]`（`Cast[object]` も可）などを介することで、ジェネレータに対しても間接的に LINQ の拡張メソッドがサポートされます。

前項「手順 3」では、`Select` メソッドで `KeyValuePair` を文字列に整形し、`Where` メソッドで要素数が３以上のものを拾っています。

## おわりに
個人的な備忘メモとはいえ、どなたかのお役に立てば幸いです。