Pythonの新機能t-stringsがキター!SQLもHTMLも楽々記述で開発効率爆上がり!?
引用元:https://news.ycombinator.com/item?id=43748512
これ結構イケてるじゃん。db.execute(”QUERY WHERE name = ?”, (name,))
がdb.execute(t”QUERY WHERE name = {name}”)
になるってことだよね。
このシンタックスシュガーは、言語の複雑さを増す以上の価値があると思うな。ライブラリ開発者が{}の展開を自由にできるのは良いことだし、言語全体でテンプレート構文が統一されるのも良いことだと思うよ。
20年くらい前にこれの安全なOCaml実装をしたよ。最新バージョンはこれ:https://github.com/darioteixeira/pgocaml
変数はコンパイル時に安全に補完されるんだ。型チェックもされてて、コンパイル時にカラム型とデータベースをチェックしてる。
あんたのやったことはPythonのやつよりすごいし、20年も前なんだね。お見事。でも現実は2025年、Pythonの人気は止まらないし、OCamlなんて誰も気にしちゃいない(Pythonより良い言語はたくさんあるのに)。悲しいね。
多数派が「より良い」言語を使わないってのが面白いよね。多数派の判断ってそんなに間違ってるのかな?それとも「より良い」ってのは、時間の経過とともに普及率で決まるのかな?
みんな自分の最適化したい指標が違うだけだよ。Pythonは学びやすいから良いんだと思う。
最近のLinuxならコマンドプロンプトでpython
って打てばREPLが起動するし、Windowsなら公式サイトからインストーラーをダウンロードしてpy
って打てばいい。
Pythonを教えるのにimport
なんて必要ないし、標準ライブラリだけでも色々できる。venvは依存関係が競合するプロジェクトが出てきてからで良いんだよ。Pythonは教えやすいし、実用的だから先生も教えるんだよ。boilerplateも少ないしね。
サーバーサイドのパラメータバインディングの利点は、SQLインジェクション対策だけじゃないよね?PGの拡張プロトコル(バイナリ)を使ったり、パラメータ化されたプリペアドステートメントをキャッシュしたりとか。
あとdb.execute(t”QUERY WHERE name = {name}”)
ってdb.execute(f”QUERY WHERE name = {name}”)
とほぼ同じじゃん。一文字違うだけでインジェクションされやすくなる。
この新しいフォーマット指定子はSQLクエリには向いてないと思う。
>Aren’t there other benefits to server-side parameter binding besides just SQL-injection safety? For instance, using PG’s extended protocol (binary) instead of just raw SQL strings. Caching parameterized prepared statements, etc.
>テンプレート文字列の上で実装できるよ。
>A single character difference and now you’ve just made yourself trivially injectible.
>違う型だからdb.execute
で静的にも動的にも拒否できるよ。
>I don’t think
>その通り。
>this new format specifier is in any way applicable to SQL queries.
>PEP 750のモチベーションの一つだよ。
もう一回コメント読んでみてよ。「f」とか「t」に気づくかどうか、の話だってば。見た目めっちゃ似てるじゃん。
いやいや、エラーになるって。だって、stringとtemplateは型が違うし、インターフェースも違うもん。
parentを何回かクリックして、このスレッドの最初のコード例を見てよ。ユーザーが意図的にstring(f-stringを含む)とt-stringを使ったかどうか区別できないような使い方してるんだよ。
ライブラリで使うなら、こんな感じかな。
sh(t”stat {some_file}”)
t-stringを使えば、some_file
の中身をエスケープしてからshellに渡せる。stat “$( base64 -d [base64 encoded content of some_file] )”
みたいにすれば、セキュリティも上げられるかもね。
気になるところは、オーバーライドしようとしてるパターンと見た目が似すぎてることかな。
db.execute(f”QUERY WHERE name = {name}”)
と
db.execute(t”QUERY WHERE name = {name}”)
でも、f stringの方はname
パラメータがないからエラーにならない?
execute
関数がt-stringだと認識して、name
がユーザー入力から来てたらSQL injectionを防げるってこと。f-stringはすぐにstringに評価されるけど、t-stringはtemplateオブジェクトに評価されて、stringにするには追加の処理が必要。
それなら、便利なのはexecute
関数を自分で書く必要があるってことだね(コメントにあるみたいにただの置き換えじゃない)。追加の関数があれば、f-stringに入れる値の安全性を確認できる。
db.execute(f”QUERY WHERE name = {safe(name)}”)みたいにする方が良い気がする。
この例の問題点は、safe
をどこから持ってくるかってことだよね?テンプレートをdb.execute
に渡せば、db
インスタンスが接続先のバックエンドに合わせて安全性を処理してくれるじゃん。そうでなければ、文字列を適切にサニタイズするために、db
接続を使ってsafe
関数を作る必要があるし。さらに、safe
がただの文字列を返すだけだと、db.execute
がパラメータを別の方法で渡す能力も失われちゃうんだよね。変数が文字列に埋め込まれてるって情報がなくなっちゃう。
db.safe
は、t-string
のために作る安全性チェック付きの新しいdb.execute
と同じだね。でも、値をさらに使ったり、もっと複雑なケースではメリットもあると思うよ(今のところ自分のコードではファンじゃないけど)。
せやけど、db.safe(”SELECT * FROM table WHERE id = {}”, row_id)
みたいになるんちゃう?db.execute(t”SELECT * FROM table WHERE id = {row_id}”)
の代わりに。個人的には後者の方がええな。
いやいや、db.execute(f”QUERY WHERE name = {db.safe(name)}”)
でしょ。db.execute
で暗黙的に安全性を確保するんじゃなくて、db.safe
の中で明示的に安全性を追加するんだよ。もし気が向いたら、name
をdb.safe
の中でdb.foos
に代入して、後で(execute
の中でも)使えるようにすることもできる。
でも、もし誰かが 誰かが 記事には書いてあったけど、ここの例ではそれが当然のこととして扱ってるよね。 “they”ってどういう意味?テンプレート補完関数のこと? サニタイズのことだよ。昔の もしt-stringがdb.executeで使われてて、それがt-stringに対応してないならエラーになるよ。でも、もしdb-executeが対応してたら、外部パラメーターを使うのと同じくらい安全なはず。で、もしt-stringじゃないものが使われたら、最終的には拒否されるべきだね。 t-stringを受け入れる関数だからって、デフォルトでサニタイズ処理が行われてるわけじゃないからね。 関数がtemplateを受け入れるなら(stringとは違う種類のオブジェクト)、サニタイズしてるか、明示的にサニタイズなしでtemplateを実装してるかのどっちかだよー。うっかりやっちゃうのは難しいと思うな! なんでサニタイズなしで使うのが難しいと思うのかわかんないなー。値のチェックは必須じゃないし、単に便利に使ってるだけかもよ。 SQLで値じゃないもの(例えばカラム名)もフォーマットする必要があるとして、 フォーマット指定子のコンパイル時チェックがないのが残念だね。 Pythonもコード実行前にいくつかチェックするよ。例えば: これからは明示的に書かなくても、t-stringに慣れてない人が(ほとんどの人がそうだろうけど。f-stringとそのフォーマット機能を知ってる人すら少ないし)うっかりf-string使っちゃうだけで、大変なことになるかもね。 まともなライブラリなら、テンプレートを期待してるところにstringを渡したらエラー出すでしょ。それに、そのライブラリは型も持ってるから、IDEが事前に教えてくれるはずだよ。 マジかー、t-stringsにそんな期待してるの?HTMLとかSQLに変換されるって、どうやって判断するんだろ?文字列の書き方から推測するしかないじゃん?それって場当たり的だし、t-stringsの機能とは関係なくね?今の設計だと、何のコンテンツになるか全然わかんないし。変換関数が全部処理するんでしょ?sql えー、そう思う?最初は変に感じるかもね!Paulも言ってるけど、PEP 750を作る時、色々考えたんだよ。結局、PEPはツールが採用できる色々なアプローチを残してるし(他の人も言ってるけど)、ツールコミュニティが協力して解決すべき問題だから、PEPの範囲外にしたんだ。だから、エコシステムが適応してくれると嬉しいな! html(t” 型ヒントを使うのもありだよね。title: HTMLTemplate = t” 元のPEPと議論では、それも検討してたんだ。でも、後で出てくるようにするために削除したんだ。言語にシグナルを送る方法は色々あるけど、ツールに優しいものもあれば、より堅牢なものもある。 PyCharm(と他のJetBrains IDE)は、少なくとも10年前から、コメントを使ってPython文字列に言語を注入するのをサポートしてるよ[0]。最悪の場合でも、フォーマッターが自動フォーマットを適用するために使えるはずだよ。でも、それだけじゃない!IntelliJ for Javaは、引数にアノテーションをつけるのもサポートしてる[1]。そうすると、特定の関数で文字列リテラルを使う場所全部で、シンタックスハイライトが効くんだ。Pythonでは、typing.Annotatedがまさにこういう目的のために設計されたみたい[2]。だから、こんな感じにできるはず。SQL = Annotated[Template, “language”, “SQL”] PEPの作者の一人です。JetBrainsにもいます。PyCharmチームと話してます。 型アノテーションでできるんじゃない?例えば、SQLAlchemyがSQL型を持ってて、mypyみたいなツールがTemplateインスタンスを見て、安全に使ってるか確認できる。Black, Ruff, SQLFluffは、Annotated[Template, SQL]を探して、テンプレートをSQLとしてフォーマットできるって気づく。Djangoは、Annotated[Template, Email], Annotated[Template, HTML], Annotated[Template, JSX]を持ってて、同じテンプレート構文がどのコンテキストを対象にしてるかを示すこともできる。 これ、PEPの最初の改訂で議論したことなんだよね( へー、面白いね。背景情報ありがとう。Astralがこの分野で何をするのか興味津々だけど、資金が尽きたらどうなるか心配だよね。 >テンプレート文字列が有効なHTMLまたはSQLに変換されると推測する唯一の方法は、文字列内の明らかな構文を基にすることだ” Pythonは10年前から型アノテーションがあるし、最近のIDEはそれを解釈できるんだよ。 これって、次のようなSQLの構文をneatに書けるようになるってこと? TclのSQLite拡張も似たようなことができるよ。 結局のところ、プリペアドステートメントを使うのが適切な方法じゃないの?もしちゃんとプリペアドステートメントを使ってるなら、このt-stringってSQLの利用において何の意味があるの? 記事にもあるように、みんなプリペアドステートメントを使ってないんだよ。代わりに、f-stringの方が便利だからそっちを使っちゃうんだ。 後方互換性を維持するために、テンプレートしか受け付けない新しいメソッドが追加されるだろうね。そうなると、文字列を渡すのを止めさせる努力が無駄になっちゃう。PHPを始めた15年前には、プリペアドステートメントがSQLクエリを実行する推奨方法だったのに。SQLインジェクションに対して脆弱なコードを書くべきじゃないよ。 >This would be the nicest way to use SQL I have seen yet.” 長年使ってたけど、モデル作るのにビジュアルデザイナー使わなきゃいけなくて、これがまた遅くてバグだらけだったんだよね。マジで毎日イライラもんだったわ。UIがデータベースの関係壊すの見張ってなきゃいけないとか、マジ勘弁。 今どき誰も使ってないでしょ、たぶん過去5年くらいは。今は https://learn.microsoft.com/en-us/ef/core/modeling/#use-data… を使うのが普通。これは完全に独立した、レガシーなVSの拡張機能で、EFとかEF Coreとは別物。 マジ勘弁。シンタックスシュガーとしては良いけど、SQLインジェクションの脆弱性とパラメーター化されたクエリの違いが、見落としやすい一文字だけになっちゃった。 t-stringは__str__()メソッドのないTemplateオブジェクトを生成するから、f-stringと間違えて使うことはないよ。コードが文字列を期待するならTemplate渡したらエラーになるし、Templateを期待するなら文字列渡したらエラーになる。 >コードがTemplateを期待するなら文字列渡したらエラーになる。 db.execute(t”Select Count(1) from someTable”) 他の場所ではそんなことしなくていいじゃん?変数入れないのにfつけることないし。Pythonの入力は寛容なのが普通だし。タプルが欲しいところにリスト渡せたり、floatが欲しいところにint渡せたり、単一のアイテムのタプルの代わりにアイテム直接渡せたりするし。Regex関数も普通の文字列でもRegex文字列でも受け付けるし、Regex文字列を強制しないし。 えー…それ違うくない?Pythonは寛容な場合もあるけど、いつもじゃないよ。完全にツール次第。ライブラリが追いつくまで時間がかかるけど、t-stringを強制するライブラリを作る人は絶対いると思う。たとえ裏でレガシーライブラリのために分解しててもね。 何が違うの?Pythonの入力は普通寛容じゃん。いつもって言ったわけじゃないし。Pythonで入力に厳格なのが普通で、寛容なのが例外だって言うの? Pythonが正当な理由もなく異なる型を盲目的に交換できるってこと?そんなことないよ。型が違う場合は、Pythonは入力に厳格なのが普通だよ。例えば、Decimal(‘3.0’) / 1.5 を試してみて。エラーになるでしょ、当然だけど。 えーと…でも普通はそうじゃん?例えば、Decimal(’3.0’) / 2を試してみて。ちゃんと動くよ。floatだとダメなのは当然。それがポイントでしょ。基本的には型には寛容だけど、そうしない理由がある時は別ってこと。4 + Trueとかやっても5が返ってくるし。これこそ「意味もなく違う型を混ぜてる」って言えるんじゃない?Regexもr-string限定じゃないし。Pythonは入力には寛容な姿勢だよ。type hintingだって後付けだし。JavaScriptほどじゃないけど、かなり寛容だと思うけどな。そうじゃないって言うのは無理があると思う。 Decimalとintの混合は良い理由があるからOK。floatは精度が問題。boolとintの混合は良くない。Pythonが抱え続けるべきじゃない実装の悪い点。Pythonにはbool型がないってことだよ。TrueとFalseはただの名前付きの整数。マジで愚かでそうあるべきじゃないけど、昔からの理由でそうなってる。regexの例も意味不明。stringとr-stringは同じもの。インタープリタから見たら区別ないのに、どうやってregex関数がr-stringを強制できるの?違うべきかもしれないけど、Python 4.0がないと無理。 >そんな例を出すなんて信じられない。もしチームのプログラマーがそんなことしたらクビにするレベル。 >Pythonicなものって結構何でも受け入れるってこと 変数を使わないならf-stringは使わないな。 上で議論されてる理由から、t-stringもそうなるかは分からないな。frameworkやlibraryが対応するには時間がかかると思うし(後方互換性を維持しながら)、ベストプラクティスがlintingツールとかに浸透するのにも時間がかかると思う。 stringが使える場所ならt-stringも使えるなら、パラメータがないt-stringはコードのにおい(linting error)。専用のtemplate-string APIがあるなら、普通のstringを使うのをやめると後方互換性がなくなるってこと。 >stringが使える場所ならt-stringも使える libraryの作者はどっちの型を受け入れるか決めないといけない。database cursorなら、普通のstring + パラメータ引数とtemplate stringのどっちを受け入れる?それとも新しいAPIを作る? 既存の関数はt-stringに対応しないと思うよ。代わりにt-string専用の新しい関数が作られるんじゃないかな。 strcpyとかstrncpyとかstrlcpyみたいに、ドキュメントだけが生き残って、チュートリアルとか古いコードからコピペされまくって、新しいものが普及しないリスクがあるよね。AIがコード書く時代になるし、もっとひどくなるかも。でも、古いものを廃止すると言語自体が死んじゃうし、難しい問題だよね。静的チェックが簡単にできるようになったら、少しずつ変わっていくかもしれないね。 Pythonのtype checkerとかlinterって、特定の関数を呼んだ時に警告とかエラーを出せる機能あるのかな?それがあれば、新しいt-string専用の関数への移行を強制できるかもね。 ちょっと前に、知らないコード見てたら、 完全にテンプレート専用の新しいライブラリじゃない限り、今のコードは文字列を受け付けるし、後方互換性のために、これからも文字列を受け付け続けるんじゃないかな。 それは実装次第だね。既存のライブラリなら、こんな感じになるんじゃない? OPはtをfに置き換えるって言ってるんだよ。 それだとget()に文字列が渡されて、エラーになるよ。get()はテンプレートを処理する関数だから。もっとコメントを表示(1)
safe
を省略したら、動くかもしれないけど、インジェクションを許しちゃうかも。t”
を使うのを忘れて、代わりにf”
を使った場合も同じことが言えるよね。少なくともdb.safe
は何をするか言ってるけど、t”
は違う。f-strings
は値をサニタイズしないから、安全じゃないよ。この記事に書いてある。
そう、言語にこれがあることで、ライブラリの作者は、それが適切なユースケースのために実装を書くっていう考えだよ。db.execute
でt-string
を使ったからって、以前より安全になったって意味にはならないよ。
重要なのは、t-stringはstringじゃなくて、stringの構文を再利用してTemplateオブジェクトを作る新しいリテラルってこと。これがf-stringと根本的に違う点だね。新しい型だから、stringを受け入れるライブラリは明示的に処理するか、TypeErrorを出す必要があるんだ。
t-stringを実装して、値を保存したり、ログを改善したりするために使ってて、チェックやエスケープのことなんて考えてないかもしれないし。他の場所で忘れがちなのと同じようにね。execute
関数はどうやってフォーマットすべきものとパラメーター化された値を区別するのさ?
print(”hello”)
def f():
nonlocal foo
とすると:
SyntaxError: no binding for nonlocal ‘foo’ found
って出るでしょ?helloの前にエラーが出て、f()すら呼ばれてない。select * from {table}
みたいなのがあれば良かったのにね。変換される前のSQLが正しいSQLである保証もないし。tgive me {table} but only {columns}
が変換後にSQLになるかもしれないし。>元のコメント書いた人の意見に対する批判的な意見だねHello
”)みたいなパターンが出てくると思うよ。ハイライターとか静的解析ツールは、これを見て判断するんじゃない?JavaScriptのtagged template literalsも同じくらい柔軟だし。tag関数を動的に選べるから。Pythonのツールも同じようにできると思う。名前付きの処理関数の中にあるt-stringsだけをサポートすればいいんじゃないかな。Hello
”みたいな。もっとコメントを表示(2)
def query(sql_query: SQL):
# do stuff with Template to sanitize
query(t”SELECT * FROM foo WHERE bar={bar}”)
テンプレート文字列がこれを実現するために必要ってわけじゃないけどね!JetBrainsはずっと前からやってるし。でも、テンプレート文字列がこういう目的で十分に役立つなら、ツールが進化して、フォーマッターや他のエディターでサポートしてくれるかもしれない(PyCharmがJavaより良いサポートを受けられるかも)。Annotated
を使うってやつ)。でもね、リンターってPythonの型システムのこと全然知らないんだって。PyCon USとかEuroPythonとかで、この辺のコミュニティを盛り上げて、何とかしたいと思ってる。JSX/TSXの世界は本当にツールが充実してるから。それを欲しい人には提供できるし、場合によってはもっと良くできるかもね。
それだけじゃないよ。使われ方も見れるじゃん。エディターが有名なライブラリでのt-stringの使い方を知ってて、どのt-stringがライブラリの関数に渡されてるか追跡して、t-stringが従うべき文法を推測できる。ズルいかって?ある意味そうだけど、便利だし、プログラマーにとっては価値があると思うよ。query: SQL = t”SELECT ...”
って書くのは、DXが向上するなら安いもんだよ。
city = ‘London’
min_age = 21
# Find all users in London who are 21 or older:
users = db.get(t’
SELECT * FROM users
WHERE city={city} AND age>{min_age}
’)
db.get()関数がテンプレートを受け入れるなら、そうなるはずだよね?もしそうなら、今までで一番SQLを使いやすくする方法かも!
db1 eval {INSERT INTO t1 VALUES(5,$bigstring)}
https://sqlite.org/tclsqlite.html#the_eval_method
EF/EF Coreは何年も前からあるよ!https://learn.microsoft.com/en-us/ef/core/querying/sql-queries
問題はそこなんだよねー。ほとんどの場合、エラーにならない可能性が高いってこと。
SQLクエリにはパラメーターがないものも多いし。テーブルの行数とか取得するだけなら、生の文字列で全然OK。
sqlite3は本当に文字列を許可しないの?パラメーターがない場合でも、テンプレートを強制するの?
そうあるべきだって主張できるけど、それは入力に優しくないし、後方互換性を壊すよね。もしかしたら、モジュールに厳密な動作を有効にするフラグがあって、10年後にはデフォルトになるかも?
パラメーター化されてないクエリに、生の文字列の代わりにたった一文字追加するだけ。「強制」するってことね。t-string自体はパラメーターなくてもちゃんと動く。
テンプレート専用APIに切り替えるのは後方互換性の問題があるけど、テンプレート専用APIは、パラメーターの数に関係なく、すべての文字列の前にt
をつけるだけなら、そんなに「優しくない」ってわけでもない。
どんな場合でも特定の種類の文字列を使うように強制されるのは、Pythonの伝統的なやり方とは違うよね。安全なのはわかるけど、間違いなく優しくないから、モジュールのメンテナーがどう扱うか気になる。もっとコメントを表示(3)
それって、俺とは違う話をしてない?
俺はただPythonを説明してるだけ。擁護してるわけじゃない。Trueを足せる理由も知ってるから例に出したんだし。r-stringがただのstringなのも分かってる。Pythonはr-stringを別のオブジェクトにして、バックスラッシュエラーをなくせたのにそうしなかった。
俺の言いたいことは、Pythonicなものって結構何でも受け入れるってこと。type hintだって強制じゃないし。君はそうあるべきじゃないと思ってるみたいだけど、Pythonが厳しい言語だって言うのは違う。
stringをstringとして、intをintとして使うのは「何でも受け入れる」わけじゃない。プログラミング言語の基礎だよ!duck typingと「何でも受け入れる」を混同してるんじゃない?標準ライブラリでも互換性のあるインターフェースを使うべきって考えはあるよ。generatorをlistが必要な関数に渡して痛い目を見たことなんて何回もあるし。
Lintersも変数が無いf-stringには文句を言うし。t-stringも同じだと思う。
違うよ。型が違う。t-stringはstrじゃない。
それを活かすかどうかは良いframework/APIデザイン次第。
cursor.execute(“select * from x where foo=?”, {foo=1})
# 許可しつつ
cursor.execute(t“select * from x where foo={foo}”)
# または
cursor.executetemplate(“select * from x where foo={foo}”)
executeがstringとt-stringを受け入れるなら、パラメータがないt-stringを使うのは問題だと思う。t-string専用のAPIがあるなら、パラメータの渡し方が2つに分かれるから広範囲な破壊的変更を意味する。datetime.utcnow()
に打ち消し線が引かれてたんだよね。マウスオーバーしたら、deprecatedってメッセージが出てきて。editor(vscode)とtypechecker(pyright)が、deprecatedってマークされてるのを見つけて表示してたみたい。これで、utcnow()がdeprecatedだって知ったし、自分のコードでもdeprecatedマークして、新しいバージョンを使うように促せるって学んだよ。
>def get(self, query):
> if isinstance(query, template):
> self.get_template(query) ”テンプレートの場合”
> else:
> self.get_old(query) ”古いコードを壊さないように!”