LoginSignup
2
1

IronPython との付き合いかた(その2)

Last updated at Posted at 2024-03-06

はじめに

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

以降、上記を「本編」と表記します。

確認環境

  • 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: Additional Unpacking Generalizations(アンパッキング演算子)--> 3.5 で追加
  • PEP 498: Formatted string literals(f 文字列)--> 3.6 で追加

pip について

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

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

上のリンクを見ると HTTP の活用に便利な requests はインストールできそうです。

pip の準備

準備の前に

ipy.dll の場合 -m オプションを使って pip を実行します。

本編のとおり IronPython を起動するためのコマンドラインが長すぎるので、バッチファイルやシェルスクリプトを書きます。

ipy3.bat(Windows + .NET8 の例)
@dotnet --roll-forward LatestMajor "C:\Users\User\IronPython\3.4\net6.0\ipy.dll" %*

Linux の場合は、スクリプトファイルを作って(ファイル名を ipy3 としておきます)

ipy3(Linux + .NET8 の例)
#!/bin/sh
dotnet --roll-forward LatestMajor ~/ironpython/3.4/net6.0/ipy.dll $@

実行権限を与えます。

chmod +x ipy3
sudo mv ipy3 /usr/local/bin/

以降は ipy3.bat により例示します。

最初は ensurepip

IronPython 3.4 にも ensurepip が入っています。
最初だけ次の手順を踏んでください。

PowerShell
.\ipy3.bat -m ensurepip
.\ipy3.bat -m pip install --upgrade pip --user

スクショを載せておきます。
image.png
pip のバージョンは 19.1.1 です。

パッケージのインストール

準備が整ったら requests をインストールしてみます。

.\ipy3.bat -m pip install requests --user

image.png
投稿時における requests の最新バージョンは 2.31.0 ですが、バージョン 2.21.0 がインストールされます。

ダメもとでしたが、more-itertools もインストールできました。

.\ipy3.bat -m pip install more-itertools --user

image.png

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

注意点(実際に経験したこと)

  • パッケージの最新バージョンがインストールされるとは限らない
  • インストールに成功しても、実行時エラーを吐く場合がある
  • OS など、環境の違いによって成否が異なる場合もある
  • 「インストールできたらラッキー」ぐらいの気構えがよい

辞書について

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

dict から Dictonary, SortedDictonary に変換する

Dictionary[TKey,TValue]() の引数として dict を渡すとコントラクタとして働きます。
SortedDictonary も同様です。

なお、SortedDictionary を使用する場合は、本編で触れたように アセンブリへの参照設定が必要です。

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)

確認します。

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, 松井]

DictonarySortedDictonaryKeyValuePair(System.Collections.Generic.KeyValuePair) のコレクションなので、print 内で展開できます。

printKeyValuePair を表示する際、リストと同じ様式を使います。

ToDictionary の利用

LINQ の ToDictionary も利用できます。

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() は失敗します。
用途はないと思いますが、静的メソッドなら成功しました。

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 の操作

dictDictonary, SortedDictonary は別物ですので属性も異なります。
とはいえ、操作性は概ね直感どおりです。

dict への変換

IronPython
dict(NET_Dict)
# {51: 'イチロー', 17: '大谷', 55: '松井'}
dict(NET_SortDict)
# {51: 'イチロー', 17: '大谷', 55: '松井'}    dict に変換すると順番は崩れる

辞書の追加・更新・削除

追加・更新は dict と同じ操作です。
削除に del 文は使えないので Remove メソッドで削除します。(削除の例示は省略)

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, 松井]

要素の取り出し

例として、辞書の要素をタプルのリストにしてみます。

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 は返値をタプルにします。

IronPython
[*map(lambda kvp: type(kvp.Deconstruct()), NET_Dict)]
# [<class 'tuple'>, <class 'tuple'>, <class 'tuple'>, <class 'tuple'>]

活用事例

以下の事項を織り込んだ活用事例を考えてみました。

  • 外部パッケージ(requestsmore-itertools)の利用
  • 辞書の利用
  • LINQ におけるジェネレータの扱い

事例 ~ 2024年度の3連休以上を探す ~

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

  1. 国民の祝日.csv を取得(GET リクエスト)して、来年度の範囲を抽出する
  2. 来年度の土日と、上記 1. で抽出した祝日とを統合する
  3. 3日以上連続する休日を拾う

手順 1 には requests を、手順 3 には more-itertools を利用します。

手順 1

結果は次の変数に入ります。
 pubHolsAll(1955~2025年の全祝日)
 pubHols24(2024/4/1~2025/3/31の祝日)

pubHolsAllDictionary[object, object] ですが、pubHols24KeyValuePair[object, object] のイテレータになります。

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)

pubHolsAllpubHols24 の内容を確認します。

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 の要素)を上書きすることで統合します。

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

Python と .NET の曜日の扱い
Python の datetime.date.weekday() メソッドが返す値は、月曜日が 0 です。
.NET の System.DateOnly.DayOfWeek プロパティの値(System.DayOfWeek 列挙)は、日曜日が 0 です。

holidays24 を出力して、土日と祝日が統合されているか確認します。

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 は、値が連続する要素ごとにグループ化するジェネレータです。ここに holidays24 を渡して、3日以上の連休を探します。

先にコードを示し、次の項で簡単に解説します。

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

3日以上の連休(moreHols)になっているか出力してみます。

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 でたどってみます。

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) を2回使っているので3番めのグループから表示されます。

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) で確認します。

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

CastOfType が見つかりました。

dir(cg.Cast[map]()) してみます。

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 メソッドで要素数が3以上のものを拾っています。

おわりに

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1