【Python】f-stringとフォーマットの仕組みを特殊メソッド__format__から理解する。
前書き
先日、同僚の@JunyaFff、@takapdayon(それぞれtwitterアカウント)とPythonのfstringについての勉強会をしていた時の知見と、追加で調べたことを合わせてまとめました。
f-stringは知っているけど、パターンを覚えているだけで仕組みやFormat specificationsはよくわからない、という人向けの内容です。
参考リンク
- PEP 498 -- Literal String Interpolation | Python.org
- f-stringのPEPです。
- PEP 3101 -- Advanced String Formatting | Python.org
%
を文字列フォーマットに使っていた時代に、format関数を提案しているPEPです。- f-stringも、仕組みはformat関数を使っているだけなので参考になります。
- string — Common string operations — Python 3.9.2 documentation
- formatのドキュメントです。
help('FORMATTING')
とすることで、python-shellから閲覧できます。
- 2. 字句解析 — Python 3.9.2 ドキュメント
- f-stringのドキュメントです。
環境
バージョン | |
---|---|
MacOS Big Sur | 11.1 |
Python3 | 3.9.1 |
f-stringの例と用語
f-stringはf
を先頭につけた文字列リテラルです。文字列リテラル中では、波括弧{}
によって、変数を置換することができます。
波括弧の部分は、置換フィールド(replacement field) と呼ばれているようです(参考リンク参照)。
>>> a = '1' >>> print(f'-{a}-') -1-
replacement field中で、!r
、!s
または!a
を変数の後ろに指定することで後述の__format__()
をオーバーライドして変数の値を変換します。
この!
から始まる型変換を、公式ドキュメントではconversion フィールド(conversion) と呼んでいるようです。
>>> a = '1' >>> print(f'-{a!r}-') -'1'- # これと同じ。 >>> print(f'-{repr(a)}-') -'1'-
またreplacement field中では:
から始まるさまざまな記号を指定できます。下の例では「値を右寄せにしてスペース5つとなるよう0で埋める」という意味になります。
この:
による書式の指定は、書式指定、書式指定文字列(format_spec、Format specifications) などと呼ばれているようです。
>>> print(f'-{a:0>5}-') -00001-
f-stringで使う用語のまとめ
- 置換フィールド(replacement field):
{}
の中身。 - conversion フィールド(conversion):
!s
など。 - 書式指定、書式指定文字列(format_spec、Format specifications):
:0>5
など。
最強コマンド: help('FORMATTING')
string --- 一般的な文字列操作 — Python 3.9.2 ドキュメント
こちらのドキュメントは、pythonのシェルに入り、help('FORMATTING')
とすればいつでも見ることができます。
conversionやformat specificationsに指定できる値は、こちらからいつでも確認できて便利です。
特に、最後の方には例文がたくさん乗っていてそのまま使えます。
フォーマットの仕組みを考える
replacement fieldのフォーマットの仕組みを、例を挙げて考えてみます。
conversionフィールドは特殊メソッド__str__
などを呼ぶ.
以下のクラスを定義します。
class MyClass: def __str__(self): return ('__str__') def __repr__(self): return ('__repr__')
このクラスをインスタンス化し、str()
, repr()
関数の引数にインスタンスを渡すと、特殊メソッド__str__()
, __repr__()
が呼ばれていることがわかります。
myclass = MyClass() str(myclass) # '__str__' repr(myclass) # '__repr__'
conversionの!s
, !r
でも、全く同じことをやっています!
print(f'-{myclass!s}-') # -__str__- print(f'-{myclass!r}-') # -__repr__-
f-stringの置換フィールド中では、__format__()
メソッドが呼ばれる。
先程のクラスのインスタンスですが、conversionを指定せずに呼ぶと以下のように、__str__()
が呼ばれます この挙動がなぜ起こるのか、正確に考えてみます。
print(f'-{myclass}-') # -__str__-
まず、新しく__format__
メソッドを導入したクラスを定義します。
class MyClassModFormatting: def __str__(self): return ('__str__') def __repr__(self): return ('__repr__') def __format__(self, format_spec): return ('__format__')
先ほどと同様にインスタンス化し、conversionをやってみます。
すると、f-stingの置換フィールドにインスタンスをおくと、__format__
が呼ばれていることがわかります。
>>> myclass_mod = MyClassModFormatting() >>> print(f'-{myclass_mod}-') -__format__-
実は、f-sting中の置換フィールドでは、変数に対して組み込み関数format()
を呼んでいるだけなのです。
そして組み込み関数format()
は、対象のクラスの__format__()
に全ての処理を丸投げする関数です。
format関数の定義は以下のようなものです。 PEP 3101 に定義が乗っています
def format(value, format_spec): return value.__format__(format_spec)
書式指定文字列(format_spec)の解釈は対象のクラス次第である。
format()
関数、__fomrat__()
メソッドは第二引数に書式指定文字列(format_spec)をとります。f-stringでは:
から始まる記号です。
この書式指定文字列の解釈はクラスの__format__()
の定義に完全に依存しています。換言すると、同じformat_specを指定しても、解釈は対象のクラスによって異なります。
以下のクラスは、与えられたformat_specをアットマークつきで返します。
class MyClassPritingSpec: def __format__(self, format_spec): return '__format__' + '@' + str(format_spec)
以下のようになります。f-stringのformat_spec(:
以降の部分)をパースするのは、フォーマットされるクラスに定義された特殊メソッド__fomrat__()
だということがわかります。
spec_printer = MyClassPritingSpec() print(f'-{spec_printer:hoeeee}-') # -__format__@hoeeee-
datetime.datetime
オブジェクトは特殊な書式指定文字列を持つ
datetime.datetime
はstrftimeによってフォーマットされますが、これもformattingの仕組みを使っています。
以下の結果は全く同じです。dateimeクラスに定義された__format__()
が、日付型専用の書式指定文字列をパースできるように定義されているのです。
>>> import datetime >>> d = datetime.datetime(year=2000, month=3, day=3) >>> d.strftime('%Y-%m') '2000-03' >>> f'{d:%Y-%m}' '2000-03'
デフォルトの__format__()
はobjectに定義されている
ここで最初の疑問に戻ります。__format__()
を定義していないクラスMyClassのインスタンスは、f-stringにかけて出力すると以下のように__str__()
が呼ばれるのでした。
print(f'-{myclass}-') # -__str__-
これは、自作クラスが継承しているobject
に定義された__format__()
メソッドの挙動です。
PEP 3101 PEPとしてはここに記載されています。 引用すると、以下のようになっています。
class object: def __format__(self, format_spec): return format(str(self), format_spec)
引用終わり。
オブジェクトに対して、str()
を呼んでからformatを呼んでいることがわかります。
ですので、先の例では-__str__-
と出力されていたのでした。
再確認: conversion(!r)は__format__
をオーバーライドする
最初の説明で、!r
や!s
などのconversionは「formatをオーバーライドする」と述べました。その実例をみていきます。
先程のMyClassModFormattingを再び使います。インスタンス化したあと、conversionを行います。
print(f'-{myclass_mod!s}-') # -__str__- print(f'-{myclass_mod!r}-') # -__repr__-
結果、このクラスは自分で定義した__format__()
が呼ばれていないということがわかります。__format__()
の処理が、__str__()
や__repr__()
で上書きされたのです。
ただし、!sや!rで変換した後のstringに対してはformatが呼ばれています。 書式指定文字列を渡さない場合、strはそのまま出力されるのでわかりにくいため、以下のようにしてみます。
>>> print(f'-{myclass_mod!s:0>30}-') # -00000000000000000000000__str__-
このように、文字列に変換されたあとは「 str型に定義されている__format__
」が呼ばれていました。
まとめ
f-stringはpython3.6から導入された便利な構文です。ただし、その処理の実体はPEP3101に記載があるformatの仕組みがそのまま使われています。(pythonのバージョンとしては3.0〜。これ以前は%
によるフォーマットと、テンプレートシステムだけが存在していたようです。)
formatの仕組みは一見複雑に見えますが、基本的には3つの特殊メソッド__str__
, __repr__
, __format__
で成り立っています。また、intかstrのformatに使う書式指定文字列を覚えていればよく、その例文はhelp('FORMATTING')
でいつでもサッと確認できます。
科学計算の数値表現はもちろん、Web開発での文字列の組み立て、ログへの出力(!rを用いて型情報付きで出力するのが便利!)などなど、あらゆる場面で利用できます。仕組みを理解して上手に使っていきたいなと思います。