LoginSignup
332
199

More than 1 year has passed since last update.

Python3.9の新機能 (まとめ)

Last updated at Posted at 2020-01-18

はじめに

Python 3.5から What's Newの内容をまとめる記事を投稿してきました。

これからはリリースサイクルが早くなるので投稿の頻度が増えそうですが、Pythonを1.xの頃から追っかけていた古株のエンジニアとして出来るだけ続けていきたいなと思っています。

そして、こちらの記事で書いたようにPython2は今年の1月1日でEnd-of-lifeを迎えました。Python3がメンテナンスされている唯一のPythonという中で初めてリリースされる 3.9にどのような新機能が追加されていくのか楽しみです。

なお、前回から「小変更はこの記事で、大きめの変更は別記事で書いてここにリンクを張る」という風に前回からまとめ方を変えましたが、今回もそれを踏襲したいと思います。そして全部の変更を網羅するのではなく個人的に気になったものを中心に載せていきます。

まずは今回から1年周期になった開発ロードマップ(PEP-596)。

  • 3.9 開発開始: 2019-06-04 (完了)
  • 3.9.0 alpha 1: 2019-11-19 (完了)
  • 3.9.0 alpha 2: 2019-12-16 -> 2019-12-18 (完了)
  • 3.9.0 alpha 3: 2020-01-13 -> 2020-01-25 (完了)
  • 3.9.0 alpha 4: 2020-02-17 -> 2020-02-26 (完了)
  • 3.9.0 alpha 5: 2020-03-16 -> 2020-03-23 (完了)
  • 3.9.0 alpha 6: 2020-04-22 -> 2020-04-28 (完了)
  • 3.9.0 beta 1: 2020-05-18 -> 2020-05-19 (完了、これ以降は新機能の追加なし)
  • 3.9.0 beta 2: 2020-06-08 -> キャンセル(リリースのパッケージングでミスがあったため)
  • 3.9.0 beta 3: 2020-06-29 -> 2020-06-09 (完了)
  • 3.9.0 beta 4: 2020-07-20 -> 2020-07-03 (完了)
  • 3.9.0 beta 5: (not planned) -> 2020-07-20 (完了)
  • 3.9.0 candidate 1: 2020-08-10 -> 2020-08-11 (完了)
  • 3.9.0 candidate 2: 2020-09-14 -> 2020-09-17 (完了)
  • 3.9.0 final: 2020-10-05 -> 2020-10-05 (完了)

更新履歴

2022.08.21

TopologicalSorterが新設のgraphlibモジュールで提供されているよう修正しました。@gotta_dive_into_python さん、ありがとうございます。この変更で functoolsから新たなモジュールへ移動されているのですが、ベータ出た後なので見逃してました(汗)。

2020.10.07

beta2がurllib系の問題でキャンセルされるなどあってβ版が一つ増えましたが最後は辻褄を合わせて予定通り10/5に3.9.0が出ていました。

2020.05.31

2020-05-19にほぼ予定通り最初のベータ版がリリースされました。これで 3.9の新機能は揃い踏みということになります。

「新規に追加されたモジュール」に以下を追加しました。

  • zoneinfoモジュール

「モジュールの改善」に以下を追加しました。

  • datetimeモジュール(isocalendar()の仕様変更)
  • functoolsモジュール(TopologicalSorter の追加)
  • hashlibモジュール(コンパイル時オプションで組み込み実装を無効化できる)
  • ipaddressモジュール(ipv6のスコープ付きアドレスのサポート)
  • mathモジュール (gcdの拡張、lcmの追加)
  • osモジュール(os.pidfd_open()os.P_PIDFDの追加)
  • randomモジュール(Random.randbytes()の追加)

2020.05.04

2020-04-28に最後のアルファ版であるa6がリリースされました。依然として1週間遅れくらいで来ていますね。

「注目した新しい機能」に以下を追記しました。

  • 文字列に接頭語、接尾語を除去するメソッドを追加
  • 標準のコレクション型で型付けができるようになる
  • 新しいパーサーの導入

「モジュールの改善」に以下を追加しました。

  • typingモジュール(typing.Annotatedの追加など)

2020.04.06

2020-02-26にa4, 2020-03-23にa5がリリースされていました。1週間くらいの遅れでほぼ計画通りで来ていますね。新機能に関してはベータが出てから書こうかなと思っていましたが、結構溜まっていたのでアップデートしました。一度に全部かけないので少しずつ追加していきます。

  • リリースハイライト(「DeprecationWarningを確認しよう!」)を追加
  • 「辞書型で和集合演算子が使えるようになる」を追加

2020.01.27

2020-01-25にa3がリリースされました。ざっと見る限り、1/18以降大きなアップデートは無いようです。GC(ガベージコレクション)に関するバグの修正が取り上げられていました。なおスケジュール的にa3は少し送れましたが、次のa4の予定は変わっていません。

2020.01.18

最初のバージョン。a2が 2019-12-18にリリースされましたが、そのwhat's new をベースに書いています。a3が2020-01-13予定だったのでそれを待ってからと思っていましたが、遅れているみたいなのでとりあえずこれで出しちゃいます。

リリースハイライト

DeprecationWarningを確認しよう!

Python2.7のサポート期間が終了し、これまで残してあった過去互換の機能がバッサリと切られる、あるいは直ぐに切られることになります。それらは DeprecatedWarningをもう何年も出し続けているけど、そろそろ真面目にチェックして置かないとアップデートした途端に動かなくなるということが起きえます。
チェックするには実行時に -W defaultとして警告メッセージを出させるか、思い切って -W errorとエラーにしても良い。

注目した新しい機能

辞書型で和集合演算子が使えるようになる

→ 別記事にしました: Python3.9の新機能(1) - 辞書型で和集合演算子が使えるようになる

PEP 616 文字列に接頭語、接尾語を除去するメソッドを追加

strbytesbytearraycollections.UserStringremoveprefix(prefix)removesuffix(suffix)というメソッドが追加されました。これは文字列(あるいはバイト列)の先頭や末尾に付いている部分文字列を切り落とすメソッドです。例えば、

>>> 'test_sample.txt'.removeprefix('test_')
'sample.txt'
>>> 'abc.doc'.removesuffix('.doc')
'abc'

みたいなことができます。「え?こういうメソッドって無かったけ?」と思うのですが、無かったんですよね。実は似たようなメソッドで lstriprstripがあります。通常は

>>> '   spacey_head'.lstrip()
'spacey_head'
>>> 'spacey_tail   '.rstrip()
'spacey_tail'

という形で空白文字を消すために使うのですが、これに引数を与えたときは「その文字列に含まれる文字のどれかに合致しなくなるまで消す」というへんてこな(?)仕様になっていて間違いを起こしやすい。例えば上の例と同じことをしようとして、

>>> 'abc.doc'.rstrip('.doc')
'ab'
>>> 'test_sample.txt'.lstrip('test_')
'ample.txt'

とすると思わぬところまで消えてしまってびっくりということが起きます。実際、この挙動がPythonユーザを混乱させていることも、新たに接頭語、接尾語を消すためのメソッドを追加した理由の一つとのことです。

PEP 585 標準のコレクション型で型付けができるようになる

これまでリストやタプルなどコレクション型で型アノテーションしたい場合には typingモジュールの ListとかTupleを使ってこういう風にしていました。

import typing
def greet_all(names: typing.List[str]) -> None:
    for name in names:
        print("hello", name)

strとかintと同じ様にlistとかtupleという型名がそのまま使えたら良いのになと思った方も多いはず。それが3.9で実現します。つまり、

def greet_all(names: list[str]) -> None:
    for name in names:
        print("hello", name)

と書けるようになります!

PEP 617 新しいパーサーの導入

言語処理系でコードを読み込んで解析する部分をパーサーと言いますが、これまでPythonはLL(1)という種類のパーサーを使ってました。これは先読みを一つだけしながらトップダウンで構文解析するパーサーで、LR型というボトムアップ式のものと合わせてプログラミング言語の解析でよく使われるものです。効率良い解析が可能である一方で、扱える文法が限られているという課題があります。Pythonでは既にLL(1)で扱えない構文があり、そのためにパーサーの解析の後処理として幾つかの特別なロジックが組み込まれているとのこと。今回、新しいPEGパーサーを導入し、パーサーだけで色々な構文を処理できるようにします。

例えば、with文で複数の変数をバインドしたい場合、

with (
    open("a_really_long_foo") as foo,
    open("a_really_long_baz") as baz,
    open("a_really_long_bar") as bar
):
    ...

という風に書きたくなりますが、これは今のパーサーでは処理できません。こういうのを扱うために新たなパーサーを導入したいということです。

なおPython3.9には旧来のLL(1)パーサも搭載されています。デフォルトは新しいパーサーを使いますが、呼び出し時のオプション(-X oldparser) か環境変数(PYTHONOLDPARSER=1)で古いものも呼び出せます。ただしこれは過渡的な措置で、Python 3.10になった時には古いパーサーは引退させて新しいものだけにします。そして言語仕様もそれを前提にしたものになります。

その他の言語の変更

インポート時のエラーがValueErrorではなくImportErrorをあげるようになる

階層構造になっているパッケージ内では、上の階層のモジュールを

import ..module_1

というようにインポートできますが、これを例えばパッケージのトップレベルでやってしまうとエラーになります。その場合、これまでは

ValueError: attempted relative import beyond top-level package

というValueErrorがでてましたが、3.9からは

ImportError: attempted relative import beyond top-level package

というImportErrorになります。同様に、importlib.util.resove_name()のエラーもValueErrorからImportErrorになります。非互換の変更なので、この例外を捕まえて処理しているコードは変更の必要がありますが、まあ真っ当な変更ですね。

ローカルファイルを実行した時の実行時のパスが絶対パスになる

python script.pyとした時に__file__属性にその実行したスクリプトのファイル名が入りますが、これまではコマンドラインで書かれたままの相対パスでしたが、3.9からは絶対パスになります。なお、What's new には sys.path[0]の値も絶対パスになるって書かれているのですが、これは今のバージョンでもそうなのでドキュメント上のミスなんじゃないかなと思っています。 -> これはバグレポート挙げて直してもらいました。

それから、過去の議論を追うと、 sys.argv[0]も絶対パスに変えちゃおうとしていたみたいです。が、そこは影響範囲が大きすぎるといういうツッコミが入り、そちらは取りやめになったみたいです。確かにそれはちょっとやり過ぎだと思うので良かったです。ふー。

空文字列("")に対するreplaceの挙動の変更

空文字列("")に対して replaceメソッドを適用した時の挙動が変わります。これまでは、オプションのcount引数(最大何回変更を適用するかを指定する)が付いているとおかしな結果が出ていました。

"".replace("", "p") = "p"
"".replace("", "p", 1) = ""
"".replace("", "p", 2) = ""

これが、3.9になると、より一貫性を保った形でこうなります。

"".replace("", "p") = "p"
"".replace("", "p", 1) = "p"
"".replace("", "p", 2) = "p"

まあ、はっきり言ってバグ修正のレベルだと思いますが、「(たぶん無いと思うけど)万が一、この挙動に依存した実装をしている人が困るので以前のバージョンにはバックポートしない」とのこと。

新規に追加されたモジュール

zoneinfo

標準ライブラリに新たにzoneinfoモジュールが追加されました。これは IANAのタイムゾーンデータベースの機能を標準で提供するものです。datetimeモジュールが提供する関数にはタイムゾーンの情報を引数にとるものがいくつかありますが、標準のtimezoneクラスは最低限の機能しか提供しておらず、少し凝ったことをしようとすると python-dateutilなどの外部ライブラリに依存しなければなりませんでした。これが3.9で新たに追加される zoneinfoモジュールを使うとだいぶ楽になります。

例を示します。

>>> from zoneinfo import ZoneInfo
>>> from datetime import datetime

>>> tzinfo = ZoneInfo("Europe/Brussels")
>>> dt = datetime(2020, 10, 24, 12, tzinfo=tzinfo)
>>> print(dt)
2020-10-24 12:00:00+02:00
>>> dt.tzname()
'CEST'

>>> dt = datetime(2020, 10, 25, 12, tzinfo=tzinfo)
>>> print(dt)
2020-10-25 12:00:00+01:00
>>> dt.tzname()
'CET'

西ヨーロッパの多くの国が「中央ヨーロッパ時間」を採用しています。このタイムゾーンの国々は3月の最終日曜日から10月の最終日曜日まで夏時間となるのですが、それがzoneinfoを使うと、切り替えのタイミングを意識すること無く場所の情報だけから、問題なくタイムゾーンを扱えることがわかります。

graphlib

graphlibという新たなモジュールが追加され、TopologicalSorterという有向グラフのソートをするための機能がクラスとして提供されました。これについては Python3.9の新機能(2) - Pythonで有向非巡回グラフのソートをするで別記事にしました。

モジュールの改善

asyncio

asyncioには幾つか変更が加わっています。

一番面白そうだなと思ったのは PidfdChildWatcherの追加です。asyncioには子プロセスを複数作ってその結果を非同期に待ち受けるということができるのですが、「子プロセスの終了を検知する」というのが意外と難しい。これまで4つほどのやり方が実装されていて、子プロセスを作る毎に監視の為のスレッドを作成する方法、シグナル(SIGCHLD)を使う方法が2つ、そしてos.waitpid()を使う方法がありました。それぞれ一長一短があり、ユーザが必要に応じて変更することができます(デフォルトはスレッド作成のもの)。今回、ここに pidfsを使ったものが追加されます。

pidfsは私も今回はじめて知ったのですが、Linuxに新たに導入された仕掛けで、プロセスをファイルディスクリプタで指し示すことが出来るようになります。Unixでは通常、プロセスをPID (Process ID)で指し示しますが、システム全体で共有されていて32ビットの signed integer (符号付き整数)なので、システムが稼働し続けプロセスの生成と消去を繰り返すといずれ枯渇してしまいます。そのため、以前使っていた(既に消滅したプロセスの)PIDを再利用することになりますが、これがセキュリティ的な穴になり得ることがわかっています。その対策として、プロセスごとに個別の番号を割り振るファイルディスクリプタの考え方をプロセスにも適用し、子プロセスへのアクセスはそれを用いて出来るようになっています。

3.9で追加されたPidfdChildWatcherはスレッドやシグナルを必要とはせず、他のプロセスを邪魔することもないので、「ちょうどよい」子プロセス監視の実装となっています。課題はLinuxの比較的新しいバージョン(5.3以上)でしか使えないことですが、徐々に広がっていけばこれを使える機会も増えてくるのではないかと思います。

datetime

datetime.isocalendar()date.isocalendar()はこれまで以下のように (year, week, weekday) というタプルを返していました。

>>> from datetime import datetime
>>> dt = datetime(2020,5,30)
>>> isocal = dt.isocalendar()
>>> isocal
(2020, 22, 6)
>>> isocal[0]
2020

タプルだとその内部の情報にアクセスするのに数値インデックスを使わないといけないのでわかりにくいですね。これが 3.9から名前付きタプルを返すようになります。

>>> from datetime import datetime
>>> dt = datetime(2020,5,30)
>>> isocal = dt.isocalendar()
>>> isocal
datetime.IsoCalendarDate(year=2020, week=22, weekday=6)
>>> isocal.year
2020

オブジェクト属性のようにアクセスできるのでだいぶ楽になります。

hashlib

Python処理系コンパイル時のオプションとして、組み込みのハッシュ関数実装を(選択的に)無効化することができるようになりました。OpenSSL実装の利用を強制するためのものです。

ipaddress

RFC4007で定められたスコープ付きアドレスを <ipv6_address>%scope_idという形で指定できるようになります。また IPv6Addressクラスにscope_idという属性が追加され、スコープ付きアドレスの場合はそこで値を確認できます。

math

最大公約数を計算するgcdが3つ以上の引数を取れるようになりました。これまでは2つに限定されていました。また、最小公倍数を計算する lcmが追加されました。こちらは最初から3つ以上の引数対応しています。

>>> import math
>>> math.gcd(120, 156, 180)
12
>>> math.lcm(120, 156, 180)
4680

os

pidfd_open()P_PIDFDが追加されました。上記 asyncioのところで話題にしたpidfsのサポートの一環ですね。

pathlib

シンボリックリンクの先をたどる pathlib.Path.readlink()が追加されています。例えば、b -> aというリンクがあった場合、これまでも os.readlink()を使えば

>>> import os
>>> os.readlink('b')
'a'

とできましたが、ファイルパスを文字列で与えなければなりませんでした。3.9からはPathオブジェクトにメソッドとしてreadlink()が追加されたので、

>>> from pathlib import Path
>>> p = Path('b')
>>> p.readlink()
PosixPath('a')

こんな風にできます。この例だとos.readlink()よりもステップ数が多くなっちゃていますが、Pathオブジェクトを使って例えば /usr/local/binの下のシンボリックリンクを表示するツールはこんな風に書けます。

from pathlib import Path

p = Path('/usr/local/bin/')
for child in p.iterdir():
  if child.is_symlink():
    print(f'{child} -> {child.readlink()}')

random

乱数を任意の長さのバイト列で返す Random.randbytes()が追加されました。ただし、randomモジュールの生成する乱数は擬似乱数なので、モデリングとかシミュレーションとかには良いですが、セキュリティ応用とか暗号のためにはオススメされていません。その様な用途のためにはすでに secrets.token_bytes()という関数が用意されています。

typing

typingモジュールにAnnotatedというクラスが追加されて、型ヒント情報にメタデータを追加できるようになりました。

例えば、IntInRangeというクラスのvalueというメンバー変数が整数型(int)で値が -10 ~ 5の範囲に入ると仮定してみます。従来は、整数型であるという型情報は付けられましたが数値の範囲までは指定できませんでした。3.9からは以下のように Annotatedの2つ目以降の引数に任意のデータを置けるので、タプルで数値の範囲を指定してみました。

そしてそのヒント情報は get_type_hints関数に3.9から追加されたinclude_extrasという引数にTrueを渡すことで取り出すことができます。これを使って特別な型チェッカーを作れば値の範囲も含めた型チェックが可能になります。

from typing import Annotated, get_type_hints

class IntInRange:
    value: Annotated[int, (-10, 5)]

get_type_hints(IntInRange) ==  {'value': int}
get_type_hints(IntInRange, include_extras=True) == {'value': Annotated[int, (-10, 5)]}

最適化

廃止予定

  • math.factorial()が浮動小数点を入れると小数点以下0でも廃止予定の警告がでるようになりおます。将来的には TypeErrorになるようです。
  • randomモジュールで、今はハッシュ可能であればシードとしてどんな型のデータでも良かったのですが、結果が一意に決まることを保証するためにNoneintfloatstrbytesとbytearrayのみがシードして使えることになります。

機能削除

  • array.arraytostring()fromstring()メソッドが消去されました。3.2の頃から廃止予定になっていて tobytes()frombytes()のエイリアスになっていました。

まとめ

Python 3.9の変更点についてまとめてみました。3.9もベータが出たのでこの記事もほぼ完成かと思いますが、最適化の部分とかまだ書ききれていない部分もあるので適宜追加していきます。

332
199
11

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
332
199