-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
追加: OJT 音素のバリデーション #1004
base: master
Are you sure you want to change the base?
追加: OJT 音素のバリデーション #1004
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
型周りでいろいろ調べてみたので相談コメントをさせていただきました 🙇
正直Pythonの型システムはイマイチなので、どこまで頑張りましょうという感じです。
Literalの音素型と実体の音素リストがあると、抜け漏れを検知できなかったりと課題がありそうな気がしました!
ということで片方の定義だけで済む方法を探ってたのですが、どーーーーーにもうまい方法が思いつきませんね・・・・・・・。
pydantic v2を使って、型定義だけから実行する方法がこんな感じでした。
結構ややこしくなってしまったのですが、とりあえずここにメモさせていただきます。。
from typing import Literal, cast
from pydantic import TypeAdapter
class OjtUnknownPhonemeError(Exception):
pass
class NonOjtPhonemeError(Exception):
pass
Phoneme = Literal["a", "e", "i", "o", "u"]
UnknownPhoneme = Literal["xx"]
_PhonemeAdapter = TypeAdapter(Phoneme)
_UnkownPhonemeAdapter = TypeAdapter(UnknownPhoneme)
def hoge(p: str) -> Phoneme:
# 便利関数定義すればもうちょっとマシになりそう
try:
return cast(Phoneme, _PhonemeAdapter.validate_python(p))
except:
try:
_UnkownPhonemeAdapter.validate_python(p)
except:
raise NonOjtPhonemeError()
raise OjtUnknownPhonemeError()
try:
print(hoge("a"))
except Exception as e:
print(type(e))
もう1つ、実体のsetとTypeGuardを使って、Literal型をなくしてみました。こっちはシンプルかも・・・?
(pyrightならたぶんTypeGuardもいらない)
from typing import NewType, TypeGuard
class OjtUnknownPhonemeError(Exception):
pass
class NonOjtPhonemeError(Exception):
pass
Phoneme = NewType("Phoneme", str)
phonemes = {"a", "e", "i", "o", "u"}
unknown_phoneme = "xx"
def is_phoneme(p: str) -> TypeGuard[Phoneme]:
return p in phonemes
def hoge(p: str) -> Phoneme:
if is_phoneme(p):
return p
elif p == unknown_phoneme:
raise OjtUnknownPhonemeError()
else:
raise NonOjtPhonemeError()
try:
print(hoge("xx"))
except Exception as e:
print(type(e))
elif p == "xx": | ||
raise OjtUnknownPhonemeError() | ||
else: | ||
# NOTE: mypy が型推論に失敗。pyright の推論した型が返り値型と一致することをマニュアル確認済み @2024-01-10 tarepan |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このNOTEはずっと残ることを想定されていますか? 👀
もしそうであれば、typing.TypeGuardを使うととりあえず外す方法がわかったので、プルリク送ろうかなと考えています。
あるいはtyping.castを使えば、とりあえず# type: ignore
は避けられると思います。
(解決策がだいぶいまいちですが。。)
@@ -47,6 +48,55 @@ | |||
] | |||
OjtUnknown = Literal["xx"] | |||
OjtPhoneme = OjtVowel | OjtConsonant | OjtUnknown | |||
_OJT_PHONEMES: list[OjtPhoneme] = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tupleか、もっと言えばsetのが良さそうかなと!
あとtyping.Final
使えば再代入も(静的に)禁止できそうでした。
👍
👍 Literal型廃止実体を list から tuple (≠ set) へ変更することで、Literal Union が (pyright で) 型推論されることを確認しました。mypiは型推論に失敗します。 型推論/TypeGuardTypeGuard を用いた改善に関して、認識の確認をさせてください。 |
ちょっと 今自分が把握している必要なとこは↓で、 |
あ、こちらのPRどうでしょう? 👀 (なにか返答待ち中でしたらすみません 🙇 ) |
(お待たせしました🙇) 現状(時間が空いたので、現状再確認) 変更点は以下の2つ:
Aは問題無し。 Bでは、Literal Union narrowing の mypy 非対応を原因とする、 レビュー議論
現在のこのコードを: def phoneme(self) -> Vowel | Consonant | Literal["sil"]:
"""このラベルに含まれる音素。子音 or 母音 (無音含む)。"""
p = self.contexts["p3"]
if p not in _OJT_PHONEMES:
raise NonOjtPhonemeError()
elif p == "xx":
raise OjtUnknownPhonemeError()
else:
return p # type: ignore タイプガードで次のように改良する: def phoneme(self) -> Vowel | Consonant | Literal["sil"]:
"""このラベルに含まれる音素。子音 or 母音 (無音含む)。"""
p = self.contexts["p3"]
if is_ojt_phoneme(p):
raise NonOjtPhonemeError()
elif is_ojt_unknown(p):
raise OjtUnknownPhonemeError()
else:
return p という提案だと理解しました。 この提案は is_ojt_phoneme(p: str) -> TypeGuard[OJT_PHONEMES]:
return p in _OJT_PHONEMES しかし なにか良い解決策あるでしょうか? |
型の方はただのstrの Consonant = NewType("Consonant", str)
Vowel = NewType("Vowel", str)
Phoneme = NewType("Phoneme", str)
vowels: set[Vowel] = {"a", "e", "i"}
consonants: set[Consonant] = {"k", "s"}
unknown_phoneme = "xx"
phonemes: set[Phoneme] = vowels | consonants | {unknown_phoneme}
def is_vowel(p: str) -> TypeGuard[Vowel]:
return p in vowels
def is_consonant(p: str) -> TypeGuard[Consonant]:
return p in consonants
def is_phoneme(p: str) -> TypeGuard[Phoneme]:
return p in phonemes こうした場合、if elseの順番を考えたりしないといけないので若干厄介ですが、なんだかんだこれがいいのかなと思っています・・・。 |
なるほど。 ちなみに pylance (pyright) 導入はどんな温度感でしょうか? |
型検査部分だけならpylanceのが良さそうなのですが、良い感じのCLIツールが無いことが割りと致命的に感じてます。 たしかGithub ActionsがあるのでCIは簡単に解決すると思います。VSCode使ってる人はストレスは少なめかもです。 mypyもだいぶ辛いですが、pylanceも別の辛さがあって、さてどっちのがマシかなって感じです 😇 みたいな温度感です! (大きめのOSSでの導入事例があると真似すればいいかも。) |
内容
OJT 音素のバリデーション追加
VOICEVOX ドメインへ渡されていた
xx
OJT音素をLabel
クラスの段階でエラーとした。また OJT出力そのもののバリデーションをおこなった。
現行の
xx
に対する振る舞いを記述した(臨時の)異常系テストtest_create_accent_phrases_toward_unknown()
を廃止した。関連 Issue
#958 (comment)
#982