FP:辞書は、複数あん入りのおまんじゅう
このワークシートはMath by Codeの一部です。
1.辞書は複数あん入りのおまんじゅう
<辞書は複数あん入りのおまんじゅう>
辞書というのは単語帳ではありません。
辞書を英和辞書や和英辞書のように、
キー(k)を日本語、値(v)を英語にした、ペアアイテム(items)で辞書イメージを伝える人がいます。
これは、Dictionaryクラスのせまい使い方です。
辞書は、あっち、こっちという別のものをたばねたものです。
辞書のキー(k)はただの目印程度の軽いものです。
値(v)は数値、文字列、関数名、。。。なんでもいいのです。オブジェクトです。
だから、辞書はオブジェクトの固まりですよね。
それを、順番ではなく、キーで区別できるように
複数のおいしいあんを詰め込んだ
おまんじゅうです。
2.辞書のFP的な不変性重視の使い方とは
<データリストをバケツリレーでフィルタリング>
辞書にフィルタ関数を連続で使うと、
複数条件のフィルタリングがカンタンにできます。
たとえば、アドレス情報から、大人だけ拾って、Emailアドレスを取り出し、
そのドメインをリスト化したいなら次のようにできます。
records = [
{"name": "Haskell Function", "email": "Haskell@example.com", "age": 25},
{"name": "Marry Jane", "email": "Marry@example.net", "age": 15},
{"name": "Johson Jonson", "email": "Jonson@gmail.com", "age": 10},
{"name": "Steve Apple", "email": "Steve@gmail.com", "age": 20},
# Additional records...
]
def is_adult(user_record):
return user_record["age"] >=18
def email_address(user_record):
return user_record["email"]
def email_domain(email):
return email.split('@')[1]
adult_domains = map(
email_domain,
map(
email_address,
filter(is_adult, records)
)
)
print(list(adult_domains))
[OUT]
['example.com', 'gmail.com']
<関数辞書を作ろう>
値はオブジェクトならなんでもいいです。
だから関数を値にして、キーをつけた関数辞書というのがよくあるFPでの使い方になります。
たとえば、類似する関数を連続して使うときは、1つの変数にくるみ、キーとデータだけ変えるだけでいいですね
def process_text(text):
return text.lower()
def join(txt1, txt2):
return txt1 + "\n" + txt2
action_map = {
"proc": process_text,
"join": join
}
def execute_action(action, *args):
if action in action_map:
return action_map[action](*args)
else:
raise ValueError("Invalid action")
text1 = execute_action("proc", "Which do you like OOP or FP?")
text2 = execute_action("proc", "I like FP.")
text3 = execute_action("join", text1, text2)
text4 = execute_action("proc", "Because, FP is lite and safe.")
text5 = execute_action("join", text3, text4)
print(text5)
[OUT]
which do you like oop or fp?
i like fp.
because, fp is lite and safe.
ビルダパターンFPでは、
複数の処理をラムダ関数にキーをつけて丸めた関数辞書を作りましたね。
text_builder_env = {
"add_title": lambda c, text: f"{c}【★ {text} ★】\n",
"add_paragraph": lambda c, text: f"{c} {text}\n",
"add_bullet_point": lambda c, item: f"{c} ・ {item}\n",
"add_formula": lambda c, expr: f"{c} [式: {expr}]\n"
}
こうすれば、辞書のキーは交通整理のキーでしかない。
キーはただの名前。
辞書の中身のオブジェクトが重要。
辞書形式で1つにくるむことで、1変数として、処理関数にまるっと渡せる。
キーに反応すべき関数だけ、中身を開ければよかったね。
<辞書の合併は|演算子で>
リストは+でたし算できました。
辞書は|でたし算できます。もしもキーが|の左右でかぶっているときは、右を優先します。
これは、左のデータが更新されるという意味ではありません。
|合併は、安心してイミュータブルな演算として使えます。
たとえば、
>>> d = {'spam': 1, 'eggs': 2, 'cheese': 3}
>>> e = {'cheese': 'cheddar', 'aardvark': 'Ethel'}
>>> d | e
{'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}
>>> e | d
{'cheese': 3, 'aardvark': 'Ethel', 'spam': 1, 'eggs': 2}
おまんじゅう辞書が2つあります。まんd+まんeは、d|eとかき、e優先です。
ああ、dがつぶれる!
そんなことはりません。もとのdのとなりにd|eの新品まんじゅうができてます。
1つめのおまんじゅうに2つめのおまんじゅうをかぶせて合体してみよう。
# 左右でおまんじゅうを合体。焼き印(キー)が被ったら、右(あと)のあんこが勝つルール
d = {'つぶあん': 1, 'しろあん': 2, '栗あん': 3}
e = {'栗あん': '特製大粒', '抹茶あん': '宇治'}
res = d | e
print(f"d (元のおまんじゅう) (ID: {id(d)}): {d}")
print(f"res (新築おまんじゅう) (ID: {id(res)}): {res}")
[OUT]
d (元のおまんじゅう) (ID: 2502432854720): {'つぶあん': 1, 'しろあん': 2, '栗あん': 3}
res (新築おまんじゅう) (ID: 2502432861696): {'つぶあん': 1, 'しろあん': 2, '栗あん': '特製大粒', '抹茶あん': '宇治'}
<辞書はアンパック演算子で無傷で更新できる>
PEP 448で、アンパック演算子*(辞書の場合は**)が導入されました。
変数前に演算子をつけると、そのコレクションは展開されますが、その使用する位置で挙動が変わります。
コレクションの要素をフラット化して要素を展開するのことになります。
>>> dict(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}
アンパック演算子は、キーがかぶってないと、その順にただ展開されるだけです。
>>> {'x': 1, **{'y': 2}}
{'x': 1, 'y': 2}
しかし、
キーがかぶると、右(あと)が優先されるので、このロジック無傷の更新ができるわけです。
>>> {'x': 1, **{'x': 2}}
{'x': 2}
>>> {**{'x': 2}, 'x': 1}
{'x': 1}
メディエータFPでは、
1つののキーが部品名component_name、その値が各部品の状態辞書。
そして、状態辞書にはvisible,valueの2つのキーと対応する値をつけ、初期化した状態をまるっと書きました。
initial_state = {
"A": {"visible": True, "value": None},
"B": {"visible": True, "value": None},
"C": {"visible": True, "value": None}
}
state_v0=initial_state
state_v1 = consult_mediator(state_v0, "A", "TOGGLE", "ON")としたときの親玉関数
def consult_mediator(state: dict, component_name: str, event_type: str, value=None) -> dict:
# ユーザーの入力を現在の状態に反映したベースとなる状態を作成
updated_state = {
**state,
component_name: {**state[component_name], "value": value}
}
# マッチングのために条件をタプルにまとめる
match (component_name, event_type, value):
.....
これが面白いですね。
**stateで、initial_stateがズラーっと展開されます。
変えたい部分を後ろにつけているので、そこで、キーがかぶっていたら、変わるということです。
まず、部品名が変わるかもしれません。次に部品の状態も展開しておき、変更のあるキーの値だけ変えられます。
3.振り返り
<振り返り>
dic.update(up)は、dic | up で安全に複数更新できますね。
dic[k] = vとdel dic[v]の更新と削除はどうでしょう。
telenum = {'sasaki': '09000900990', 'yamamoto': '09011231234', 'suzuki':'09056569898' }
tel2 = {**telenum | {'sasaki':'08012345678'}} #1キー更新
keyをk、valueをvとすると、
1アイテムはk:vになり、
全アイテムは.items()になります。
その中からk,vのペアを取り出しながらk!=deleteTargetKeyとすると内包表記削除ができるね
tel3 = {k: v for k, v in telenum.items() if k!='sasaki'} #1キー削除
print(f"telenum (過去):(ID: {id(telenum)}) {telenum}")
print(f"tel2 (更新):(ID: {id(tel2)}) {tel2}")
print(f"tel3 (削除):(ID: {id(tel3)}) {tel3}")
[OUT]
telenum (過去):(ID: 2502432542592) {'sasaki': '09000900990', 'yamamoto': '09011231234', 'suzuki': '09056569898'}
tel2 (更新):(ID: 2502432538688) {'sasaki': '08012345678', 'yamamoto': '09011231234', 'suzuki': '09056569898'}
tel3 (削除):(ID: 2502433152000) {'yamamoto': '09011231234', 'suzuki': '09056569898'}
課題:ジオジェブラで辞書のようなものは作れますか?
位置ではなくキーに対応する本当の辞書形式のデータ構造はありません。
でも、異なるタイプのデータタイプの対応表のイメージは作れます。
n=slider(1,3,1)
names={"sasaki", "suzuki",yamada"}
nums={1234,5678,9101}
funcs={x+1, x^2, log(x)}
f(x)=funcs(n,x)
text1=""+names(n)+":"+nums(n)+"y=" + f + ""
text2=TableText(names,nums,funcs}
番号がキーのおまんじゅうでした。