Python 3.9の新機能や変更点の中から、辞書に対する和集合演算子の追加、文字列からプリフィックスとサフィックスを削除するメソッドの追加などを紹介する。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
2020年10月5日にPythonの最新バージョンであるPython 3.9がリリースされた。そこで、今回と次回の2回に分けて、Python 3.9で追加、変更された機能から幾つかをピックアップしてお知らせしていこう。
Python 3.9の新機能については「What's New In Python 3.9」にまとめられているので、そちらを参照されたい。大きな機能追加や変更点としては次のものが挙げられる。
今回はこれらのうちの幾つかについて見ていこう。ただ、その前に簡単にPython 2.7のサポート終了にまつわる変更点についても取り上げておく。
Python 2.7のサポートが終了したことで、Python 3からはPython 2に対する後方互換性レイヤーが取り除かれたか、近いうちに取り除かれる。これらのレイヤーにより、これまではDeprecationWarningが表示されていたが、このような後方互換性レイヤーを提供するのはPython 3.9が最後ということだ。DeprecationWarningを確認するには、pythonコマンドに「-W default」か「-W error」を付加して、Pythonコードを実行する。前者はDeprecationWarningまたはPendingDeprecationWarningを表示し、後者はエラーを発生させる。この機能を使って、早めにPython 3への移行に取りかかった方がよいだろう。
Python 3.9では和集合演算子「|」を使って、2つの辞書の要素を含んだ新しい辞書を作成できるようになった。辞書を「インプレース」で更新するにはupdateメソッドを使えたが、Python 3.9ではこれについても累算代入演算子の「|=」を使えるようになった。
2つの辞書の要素を1つにまとめる、という意味では今も述べたupdateメソッドが使える。ただし、updateメソッドはあくまでもインプレースでこれを行う。
d1 = {'k1': 'v1', 'k2': 'v2'}
d2 = {'k3': 'v3', 'k4': 'v4'}
d1.update(d2)
print(d1) # {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}
そうではなく、元の辞書には影響を及ぼすことなく、2つの辞書の要素をマージした「新しい」辞書が必要だという場合に新しい演算子が役に立つ。この演算子がない環境(つまり、Python 3.8以前)では、同じことをするには次のようなコードを書くことになる。
d1 = {'k1': 'v1', 'k2': 'v2'}
d2 = {'k3': 'v3', 'k4': 'v4'}
# 一方の辞書のコピーを作成して、もう一方の辞書の内容を使ってそれを更新
d3 = d1.copy()
d3.update(d2)
print(d3) # {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}
# 組み込みのdict(mapping, **kwarg)関数を使う
d4 = dict(d1, **d2)
print(d4) # {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}
# 辞書リテラルの要素としてd1、d2の要素を使用
d5 = {**d1, **d2}
print(d5) # print(d3) # {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}
直観的なものもあれば、そうでないものもあり、さらには一見すると何をしているか分からないものもある(最後の例)。これに対して、Python 3.9では次のようにシンプルに同じことを書ける。
# Python 3.9で有効
d1 = {'k1': 'v1', 'k2': 'v2'}
d2 = {'k3': 'v3', 'k4': 'v4'}
d3 = d1 | d2 # Python 3.8以前ではTypeError
print(d3) # {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}
また、updateメソッドを使わずに、次のように累算代入演算子の「|=」を使って、辞書の更新を行えるようにもなった。
# Python 3.9で有効
d1 = {'k1': 'v1', 'k2': 'v2'}
d2 = {'k3': 'v3', 'k4': 'v4'}
d1 |= d2 # Python 3.8以前ではTypeError
print(d1) # {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}
「|=」演算子では、右項の被演算子には辞書以外の反復可能オブジェクトも指定できる。ただし、辞書以外の反復可能オブジェクトでは、その要素が2つの要素を持つ必要がある。例えば、「2要素からなるタプルを格納するリスト」などがそうだ。これはdict関数や辞書のupdateメソッドに引数として指定可能な反復可能オブジェクトの形式と同様だ。以下に例を示す。
d1 = {'k1': 'v1', 'k2': 'v2'}
l = [('k3', 'v3'), ('k4', 'v4')]
d1 |= l # Python 3.8以前ではTypeError
print(d1) # {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}
d1 | l # TypeError
なお、上のコード例の最後に示したように、「|」演算子では辞書同士のマージしか許されていないことには注意しよう。
今見たように和集合演算子「|」では辞書同士の演算のみがサポートされる一方で、累算代入演算子「|=」(updateメソッド)では他のオブジェクトを被演算子(引数)に指定できるというのは、リストにおける連結演算子「+」ではリスト同士の演算のみがサポートされ、累算代入演算子「+=」(extendメソッド)では他の反復可能オブジェクトを指定可能であることと似た関係にある。以下にリストとタプルで連結、加算代入を行う例を示すので、上のコードと見比べてほしい。
# リストとタプル
l = [0, 1, 2]
t = (3, 4, 5)
l += t # [0, 1, 2, 3, 4, 5]:リストにタプルを加算代入することは可能
l + t # TypeError:リストとタプルの連結はできない
最後に、キーが重複した場合についても話しておく。といっても、右側にあるキー/値の組により、以前のキーの値が上書きされるというだけだ。
d1 = {'k1': 'v1', 'k2': 'v2'}
d2 = {'k2': 'foo', 'k3': 'v3'}
print(d1 | d2) # {'k1': 'v1', 'k2': 'foo', 'k3': 'v3'}
print(d2 | d1) # {'k2': 'v2', 'k3': 'v3', 'k1': 'v1'}
このコードでは変数d1とd2の順番を変えて、「|」演算子を適用している。1つ目の例では、キー「'k2'」の値が右側のd2に含まれている値となっていること、2つ目の例ではその逆になっていることに注目してほしい。
Python 3.9では文字列(strおよびbytes、bytearrayなど)に対して、その先頭または末尾から特定のプリフィックスまたはサフィックスを取り除くremoveprefixメソッドとremovesuffixメソッドが追加された。
簡単な例を以下に示す。
filename = 'test_document.txt'
print(filename.removeprefix('test_')) # document.txt
print(filename.removesuffix('.txt')) # test_document
print(filename.removeprefix('sample_')) # test_document.txt
print(filename.removesuffix('.doc')) # test_document.txt
見れば分かる通り、removeprefixメソッドは引数に指定したプリフィックスで文字列が始まっていれば、それを取り除いた新しい文字列を返送し、そうでなければ元の文字列を返送する。removesuffixメソッドはその末尾版だ。
似た機能を持つメソッドとしては、stripメソッド/lstripメソッド/rstripメソッドがあるが、これらがパラメーターに受け取るのは「削除したい文字の集合」である点に注意しよう。以下にrstripメソッドの使用例を示す。
filename = 'test_document.txt'
print(filename.rstrip('.txt')) # test_documen
上のコードは「test_document.txt」から末尾の「.txt」を取り除くことを意図したものだが、これを実行すると「test_documen」と1文字余計に取り除かれてしまう。これはrstripメソッドに渡した「.txt」があくまでも「削除したい文字の集合」であり、文字列の末尾から「.」「t」「x」「t」ではない文字に当たるまではマッチしたものが削除されてしまうからだ。その結果、「.」の左側にある「document」の「t」まで削除されてしまう。
このように、文字列が特定の文字列で始まっていれば(または、特定の文字列で終わっていれば)それを取り除くという処理を行うのに、strip/lstrip/rstripメソッドは都合が悪い。そこで、これを明示的に行うためにこれら2つのメソッドが追加された。
実際に行っているのは、おおむね、次のような処理となる(ここでは、メソッドではなく、関数として定義していることに注意)。
def removeprefix(s, prefix):
if s.startswith(prefix):
return s[len(prefix):]
else:
return s[:]
def removesuffix(s, suffix):
if suffix and s.endswith(suffix):
return s[:-len(suffix)]
else:
return s[:]
filename = 'test_document.txt'
print(removeprefix(filename, 'test_'))
print(removesuffix(filename, '.txt'))
このメソッドが有用な場面を考えてみると、プリフィックスやサフィックスが幾つかのパターンに分かれているときに、それらを簡潔なコードで削除したいといったときが例として挙げられる。
例えば、「some_func」「some_method」のような文字列から「_func」「_method」を削除したいとする。removesuffixメソッドがなければ、これは次のように書く必要があるだろう。
# removesuffixメソッドを使わないコード
name_list = ['some_func', 'some_method', 'some_name']
for name in name_list:
if name.endswith('_func'):
result = name[:-5] # len('_func') = 5
elif name.endswith('_method'):
result = name[:-7] # len('_method') = 7
else:
result = name
print(result) # 「some」「some」「some_name」
ここではendswithメソッドで末尾が特定の文字列で終わっているかを調べて、そうであれば、サフィックスの文字列長を調べてスライスを取り出している。しかし、サフィックスごとに文字列の長さは異なるので、if文での分岐が余計に必要になる。取り除きたいサフィックスを要素とするリストを作れば、if文でサフィックスの数に応じた条件分岐を行う必要はなくなるだろうが、今度は二重ループが必要になるだろう。
name_list = ['some_func', 'some_method', 'some_name']
suffix_list = ['_func', '_method']
for name in name_list:
for suffix in suffix_list:
if name.endswith(suffix):
result = name[:-len(suffix)]
break
else:
result = name
print(result)
これに対して、removesuffixメソッドを使うと、上記のコードは次のようにキレイに書けるようになる。
name_list = ['some_func', 'some_method', 'some_name']
for name in name_list:
result = name.removesuffix('_func').removesuffix('_method')
print(result)
removesuffixメソッドは削除後の文字列または元の文字列を返送するので、メソッドを上のようにチェーンさせることで、望みの結果が得られる。
Python 3.5以降では型ヒントを使って、プログラムコード中に型ヒントとして変数や関数などについてのデータ型情報を付加できるようになった。この副作用として、実はtypingモジュールの内部ではPythonのコレクション階層の複製を持つことになっている。例えば、Pythonに組み込みのリストに対応して、typingモジュールにtyping.Listクラスが存在するといった感じだ。
しかし、本来の型階層とは別個にtypingモジュールに型階層があるのはあまりよろしくないということで、Python 3.9では、現在利用可能な組み込みのコレクション型を型ヒントでも使えるようになった。
例えば、Python 3.8ではtyping.Listクラスを使って、リストの型ヒントを次のように付加していた。
import typing
def mysum(container: typing.List[int])->int:
return sum(container)
しかし、Python 3.9からはtypingモジュールで定義されているコレクション階層を表すクラスは基本的に非推奨となっている。その代わりに、通常の組み込み型であるlistやtupleなどを使って型ヒントを記述できるようになったということだ。
def mysum(container: list[int])->int:
return sum(container)
Python 3.9以降でもtypingモジュールで定義されているこれらの型は使用できるが、非推奨の警告は発せられない。また、Python 3.9のリリースから5年が経過した後に初めてリリースされるPythonでこれらの型は削除されるので、現在、これらの型を使っているのであれば、早めにPythonに既存の組み込みコレクション型を使った型ヒントの記述に切り替えるようにした方がよいだろう。
最後に小ネタも紹介しておこう。それは文字列のreplaceメソッドだ。ただし、空文字列を置換しようとした場合の挙動が変更になった。以下にコード例を示す。
emptystr = ''
s = 'foo'
n = 2
d = emptystr.replace('', s, n)
print(d)
このコードを実行すると、Python 3.8までは空文字列「''」が返送されていたが、Python 3.9では「'foo'」が返送される。replaceメソッドの第3パラメーターに非0値を指定した場合に、空文字列ではなく、置換後の文字列(第2パラメーター)の値が返されるということだ。なぜ、このような変更をしたかというと、第3パラメーターを指定しなかった場合と一貫性が取れるようにするためとのこと。
以下のコードはPython 3.9でもそれより古いバージョンでも「foo」と出力される。
d = emptystr.replace('', s)
print(d) # foo
今回はPython 3.9の新機能や変更点の中から、大きめのネタを紹介した。次回は今回取り上げられなかったものを幾つか紹介していくことにする。
Copyright© Digital Advantage Corp. All Rights Reserved.