Python 3.9の新機能

2020-09-28

Python 3.9 のリリース予定日である2020年10月05日が間近に迫ってきました。

https://docs.python.org/3.9/whatsnew/3.9.html

から、Python3.9の主要な新機能を紹介します。

辞書のマージ演算子

2つの辞書オブジェクトを| 演算子で併合して、 一つの新しい辞書オブジェクトを作成できるようになりました。

PEP 584 -- Add Union Operators To dict

2つの辞書オブジェクトの和から、新しい辞書オブジェクトを作成します。

In [13]:
print({1:'A'} | {2:'B'})
{1: 'A', 2: 'B'}

どうせ導入するなら、もっと昔から導入されていても良かった感じの機能ですね。

左項と右項の辞書に同じキー値があると、右項のキーと値が結果の辞書に追加されます。

次の例では、キー値 2 の要素が両方の項に存在しますが、右項の 2:'う' のみが登録されます。

In [12]:
print({1:'あ', 2: 'い'} | {2:'う'})
{1: 'あ', 2: 'う'}

リストの結合は + 演算子で、

In [7]:
[1,2,3] + [4,5,6]
Out[7]:
[1, 2, 3, 4, 5, 6]

と書くのに、辞書の場合は | なの? と不思議に思う人もいるかも知れません。辞書の場合は、リストと違って単純な結合ではなく、2つの集合の和集合をとるため、 | を利用しています。

同じようなパターンでは、set型同士の演算も、+ ではなく | で和集合を作ります。

In [11]:
print({1, 2, 3} | {2, 3, 4})
{1, 2, 3, 4}

|= 代入文

|= 演算子で、左辺の辞書に右辺の辞書を追加する事もできます。

In [5]:
d1 = {1: 'A'}
d1 |= {2: 'B'}
print(d1)
Out[5]:
{1: 'A', 2: 'B'}

|= 演算子で要素を追加する場合は、右辺にキーと値のシーケンスも指定できます。

In [15]:
d1 = {1: 'A'}
d1 |= [(2, 'B'), (3, 'C')]
print(d1)
{1: 'A', 2: 'B', 3: 'C'}

これは、d1.update([(2, 'B'), (3, 'C')]) を実行した場合と同じです。

| 演算子の場合は、辞書オブジェクト同士しかサポートしていません。

In [20]:
{1: 'A'} | [(2, 'B'), (3, 'C')]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-9954c56c8a42> in <module>
----> 1 {1: 'A'} | [(2, 'B'), (3, 'C')]

TypeError: unsupported operand type(s) for |: 'dict' and 'list'

リストオブジェクトの場合も、+ 演算子はリスト同士の加算しかできませんが、+= では右辺にタプルやイテレータを指定できます。

In [19]:
list1 = []
list1 += (i for i in range(5))
print(list1)
[0, 1, 2, 3, 4]

文字列のremoveprefix()メソッドとremovesuffix()メソッド

PEP 616 -- String methods to remove prefixes and suffixes

これまで、文字列メソッドには前後の空白文字を取り除く lstrip() メソッドと rstrip() メソッドはありました。

In [23]:
s = "   hello   "
print(repr(s))
print(repr(s.lstrip()))
'   hello   '
'hello   '

しかし、空白以外の、固定パターンの文字列を取り除くメソッドは存在しませんでした。

Githubでは、コミットログなどに GH-xxxxx のように決まったパターンを記述すると、自動的にIssueへのリンクに変換するようになっています。PythonでこのパターンからIssue番号だけを取り出す処理を書く場合、次のように書く必要がありました。

In [24]:
text = "GH-1234"
if text.startswith("GH-"):
    text = text[3:]
print(text)
1234

よくあるパターンですが、比較する文字列と文字列の長さが別々の場所に出現して、コードの修正時にバグを作りそうなコードです。

Python3.9 からは、この処理は次のように書けます。

In [31]:
text = "GH-1234"
text = text.removeprefix("GH-")
print(text)
1234

文字列の末尾から一致した文字列を削除するときは、 removesuffix() を使います。【大安売り!】 のような文字列から、前後の 【】 を取り除く場合は、次のように書けます。

In [34]:
print("【大安売り!】".removeprefix("【").removesuffix("】"))
大安売り!

組み込みGeneric型

PEP 585: Builtin Generic Types

Pythonの型ヒント では、整数型のみを要素とするリストオブジェクトは、typing.List を使って、

from typing import List
intlist: List[int] = []

のように記述します。

List[int] は、Listへの引数として、要素の型である int を指定しています。typing.List のように、[] で引数として別の型を指定できる型を、Generic型 といいます。

といっても、typing.List[int]list のような本来の ではなく、呼び出してもオブジェクトのインスタンスは作れませんし、isinstance() で型チェックにも使用できません。純粋に、mypy などの型チェッカーが利用する注釈でしかありません。

list型とtyping.List のような、本当の型と注釈用の型定義が別々に存在するのは、決して好ましいことではありません。Pythonのclass文で定義する型は Generic型 として定義できるようになっているのですが、listdefaultdict のような、PythonのAPIを使ってC言語で開発された、いわゆる 組み込み型 には、型アノテーションを使う方法がありません。型アノテーションはPython言語のソースコードをチェックするための記述で、Pythonの実行エンジンは、型アノテーションを使用しないのです。このため、list型の型定義は、typing.List で別に定義されています。

これまで、実際の型定義と型のアノテーションは別々のモジュールで定義されているため、型アノテーションを利用するソースコードでは、

In [75]:
from typing import (
    Any,
    Callable,
    DefaultDict,
    Dict,
    Iterator,
    List,
    NamedTuple,
    Optional,
    Sequence,
    Set,
    Tuple,
    Type,
)

のように、大量の import の記述が必要なっており、非常に不便です。特に、defaultdict などは、collection.defaultdicttyping.DefaultDict の両方に存在し、とてもわかりにくい状態になっていました。

そこで、Python3.9からは listDefaultDictなどの組み込み型を修正し、[] 演算子をサポートして、型を指定できるようになりました。これまで、

from typing import List

intlist: List[int] = [1,2,3]

と書いていたコードは、組み込み型 list を使って、

intlist: list[int] = [1,2,3]

と書けるようになりました。

PEP 585では、list などの型オブジェクトの特殊メソッド __class_getitem__ が、適切なGeneric型を返すように修正されました。

In [42]:
list[int]
Out[42]:
list[int]

list[int] は、types.GenericAlias 型のオブジェクトで、従来の typing.List 型の typing._GenericAlias とは異なっています。typing.List は、呼び出してリストオブジェクトのインスタンスを生成できませんが、list[int] は、Python言語で定義したジェネリック型のように、インスタンスを生成できます。

In [46]:
# list型の呼び出し -> リストオブジェクトが生成される
list([1,2,3])
Out[46]:
[1, 2, 3]
In [44]:
# typing.List型の呼び出し -> エラー

from typing import List
List([1,2,3])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-44-f0d88ba25a85> in <module>
      1 from typing import List
----> 2 List([1,2,3])

/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py in __call__(self, *args, **kwargs)
    620     def __call__(self, *args, **kwargs):
    621         if not self._inst:
--> 622             raise TypeError(f"Type {self._name} cannot be instantiated; "
    623                             f"use {self.__origin__.__name__}() instead")
    624         result = self.__origin__(*args, **kwargs)

TypeError: Type List cannot be instantiated; use list() instead
In [27]:
# list[int]型の呼び出し -> リストオブジェクトが生成される

list[int]([1,2,3])
Out[27]:
[1, 2, 3]

この機能追加により、typing モジュールの ListDict などは deprecateされました。Python3.9リリース後、5年後に削除される予定になっていますので、早めの移行を心がけるようにしましょう。

zoneinfoモジュール

PEP 615 -- Support for the IANA Time Zone Database in the Standard Library

これまで、Pythonの 日付型 では、タイムゾーンの指定方法として、Asia/TokyoJapanなどの、いわゆる タイムゾーン名 はサポートされていませんでした。タイムゾーン名を使用する場合には、サードパーティライブラリの dateutil などが使われています。

Python3.9では zoneinfo モジュールが追加され、標準ライブラリだけでタイムゾーン名を利用してタイムゾーンを指定できるようになりました。

In [74]:
from datetime import datetime
from zoneinfo import ZoneInfo

tokyo = ZoneInfo("Asia/Tokyo") # タイムゾーン情報を取得

now = datetime(2020, 10, 1, 0, 0, 0, tzinfo=tokyo) # Asia/Tokyoタイムゾーンでの現在時刻を取得
print(now.isoformat())
2020-10-01T00:00:00+09:00

tzdataパッケージ

タイムゾーン情報はしばしば更新されるため、Pythonにはタイムゾーンのデータベースを添付しておらず、通常はプラットフォームのOSが提供するデータベースを利用します。

しかし、Windowsなどの、タイムゾーンのデータベースを利用できない環境では、別途 PyPIから tzdata パッケージをインストールしておく必要があります。


Copyright © 2001-2023 python.jp Privacy Policy python_japan
Amazon.co.jpアソシエイト
Amazonで他のPython書籍を検索