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

## .NET 9 に対応
昨年(2024年)末、IronPython 3.4.2 がリリースされました。
.NET 8 が正式なターゲットになり、加えて **.NET 9 でも動作可能になりました**。
折角ですので、.NET 9 で新たに導入された LINQ のメソッドを IronPython 3.4.2 で試してみます。

IronPython に関する細かい点は過去の拙稿もご参照ください。

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

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


## 環境
+ Windows 11 Pro（Ver. 24H2）および Ubuntu 24.04 on WSL2
+ .NET Runtime 9.0.0 (64-bit)
+ IronPython 3.4.2 (3.4.2.1000)

## 検証
LINQ に新たに導入されたメソッドの詳細については、優れた記事が数多く見つかりますのでここでは触れません。
早速、３つのメソッド(`Index`, `CountBy`, `AggregateBy`)について検証します。

### 0. 準備
以下で LINQ の拡張メソッドが使えるようになります。
```Python:IronPython 3.4.2
import System, clr
clr.AddReference('System.Core')
clr.ImportExtensions(System.Linq)
```
IronPython での LINQ の扱いは[こちら](https://qiita.com/Mt_SQ/items/ddbcf39c19ad6591bd96#linq-%E3%81%AE%E5%88%A9%E7%94%A8)に詳しく書きました。

### 1. Index メソッド
想定した結果が得られますが、戻り値が`ValueTuple[Int32,TSource]`ではそのまま使えないので、タプルに変換しています。
```Python:IronPython 3.4.2
l = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
[*map(tuple, l.Index())]
# [(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D'), (4, 'E'), (5, 'F'), (6, 'G'), (7, 'H'), (8, 'I'), (9, 'J')]
```
`Index`メソッドを IronPython で使う機会は限られるでしょう。
お気づきかと思いますが Python は20年以上も前の Ver. 2.3 から`enumerate`関数を用意しています。
```Python:IronPython 3.4.2
[*enumerate(l)]
# [(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D'), (4, 'E'), (5, 'F'), (6, 'G'), (7, 'H'), (8, 'I'), (9, 'J')]
```
とはいえ、IronPython + LINQ の可動域が拡がったことは間違いありません。
### 2. CountBy メソッド
`CountBy`と次項の`AggregateBy`メソッドは次のデータを使って検証します。
気象庁からダウンロードしたデータを検証用に加工したもので、データ構造に特別な意味はありません。
```Python:IronPython 3.4.2
# (日付, 都市, (日の最高気温, 最低気温))
temperature = [
    ('2024/12/1', '仙台', (11.0, 4.9)),
    ('2024/12/1', '東京', (16.6, 6.7)),
    ('2024/12/1', '福岡', (16.3, 7.5)),
    ('2024/12/2', '仙台', (13.0, 3.6)),
    ('2024/12/2', '東京', (16.8, 7.6)),
    ('2024/12/2', '福岡', (19.8, 10.3)),
    ('2024/12/3', '仙台', (16.6, 5.4)),
    ('2024/12/3', '東京', (17.5, 7.4)),
    ('2024/12/3', '福岡', (17.5, 8.8)),
    ('2024/12/4', '仙台', (13.9, 4.9)),
    ('2024/12/4', '東京', (17.8, 8.4)),
    ('2024/12/4', '福岡', (13.6, 7.8)),
    ('2024/12/5', '仙台', (10.5, 3.2)),
    ('2024/12/5', '東京', (15.5, 8.9)),
    ('2024/12/5', '福岡', (13.7, 8.1)),
    ('2024/12/6', '仙台', (10.1, 2.5)),
    ('2024/12/6', '東京', (16.7, 5.2)),
    ('2024/12/6', '福岡', (14.4, 11.3)),
    ('2024/12/7', '仙台', (6.5, 0.6)),
    ('2024/12/7', '東京', (12.4, 5.9)),
    ('2024/12/7', '福岡', (11.9, 7.0))
]
```
上記データから、2024年12月1日～7日のうち１日の気温差が8℃以上になった日数を**都市ごとにカウント**してみます。
```Python:IronPython 3.4.2
from System.Collections.Generic import EqualityComparer
from System.Linq.Enumerable import ToDictionary

ToDictionary(
    temperature
    .Where(lambda data: round(data[2][0] - data[2][1], 1) >= 8.0)
    .CountBy(lambda data: data[1], EqualityComparer[str].Default)
)
# Dictionary[str, Int32]({'東京' : 5, '福岡' : 3, '仙台' : 3})
```
簡潔です。👏

`CountBy`メソッドの戻り値は`KeyValuePair[TKey,Int32]`のシーケンスです。
**IronPython では、.NET の`Dictionary`も`dict`同様に操作できる**ので、`ToDictionary`メソッドを使って`Dictionary[TKey,Int32]`に変換しておくと便利に使えます。
（IronPython の辞書型については[こちら](https://qiita.com/Mt_SQ/items/a6276eb48682f3a8e97a#%E8%BE%9E%E6%9B%B8%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)に書きました。）

なお、C# や F# と違って IronPython では`CountBy`の**第２引数を省略できません**。
通常のデータ型であれば、上の例のように`EqualityComparer[TKey].Default`と書いておけばよいでしょう。

余談ですが、この例では`ToDictionary`を静的メソッドとして使っています。
LINQ らしく拡張メソッドにしたいのですが、その場合は引数を要求されます。
具体的には次の形です。
```Python:IronPython 3.4.2
# 強引に拡張メソッドで記述する例
# 読みやすさ優先で改行するために ( ) で囲んでいます
(
    temperature
    .Where(lambda data: round(data[2][0] - data[2][1], 1) >= 8.0)
    .CountBy(lambda data: data[1], EqualityComparer[str].Default)
    .ToDictionary(lambda kv: kv.Key, lambda kv: kv.Value) # 引数を入れないとエラー
)
# Dictionary[str, Int32]({'東京' : 5, '福岡' : 3, '仙台' : 3})
```

一方、辞書型ではなく、タプルのリストにするならばこんな感じでしょうか。
```Python:IronPython 3.4.2
[
    *temperature
    .Where(lambda data: round(data[2][0] - data[2][1], 1) >= 8.0)
    .CountBy(lambda data: data[1], EqualityComparer[str].Default)
    .Select(lambda kv: kv.Deconstruct())
]
# [('東京', 5), ('福岡', 3), ('仙台', 3)]
```
IronPython では`KeyValuePair.Deconstruct`メソッドで Key と Value のタプルが返ります。

### 3. AggregateBy メソッド
こちらも痒いところに手が届く感じかもしれません。
先ほどの気温データで検証します。

2024年12月1日～7日の**期間中の最高気温と最低気温を都市ごとに集計**してみます。
```Python:IronPython
ToDictionary(
    temperature.AggregateBy(
        lambda data: data[1], # 都市をキーにしてグループ化
        (-100.0, 100.0), # アキュムレータの初期値として仮の最高気温・最低気温を設定
        lambda acc, data: (max(acc[0], data[2][0]), min(acc[1], data[2][1])),
        EqualityComparer[str].Default # 等値比較子は省略不可
    )
)
# Dictionary[str, object]({'仙台' : (16.600000000000001, 0.59999999999999998), '東京' : (17.800000000000001, 5.2000000000000002), '福岡' : (19.800000000000001, 7.0)})

# IronPython は浮動小数点の丸め表示に消極的です
```
グループ単位の集計作業が簡潔に表現できました。
`AggregateBy`メソッドも`CountBy`同様、最後の等値比較子が省略できませんが、問題なく使うことができました。

以上で検証を終了いたします。