「Python 3.9」登場、デコレーターに関する制約の緩和、新しいパーサーの採用とはPython最新情報キャッチアップ

Python 3.9で行われたデコレーター式の記述時の制約緩和、新しいパーサーが採用された理由などについて見ていこう。

» 2020年10月23日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「Python最新情報キャッチアップ」のインデックス

連載目次

 前回は、Python 3.9で辞書に追加された和集合演算子、文字列に追加されたremoveprefix/removesuffixメソッドなどを取り上げた。今回は、デコレーターに関する制約が緩くなったことや、新しいパーサーについて簡単に見ていこう。

デコレーターに関する制約の緩和

 ここでPython 3.8とPython 3.9におけるデコレーターの違いをPythonのドキュメントで比べてみよう。以下はPython 3.8のドキュメント「関数定義」の画面キャプチャーだ。ここにはデコレーターがどんなものかも定義されている(「decorator」という部分)。

Python 3.8のドキュメント「関数定義」の画面キャプチャー Python 3.8のドキュメント「関数定義」の画面キャプチャー

 そして、以下がPython 3.9のドキュメント「関数定義」の画面キャプチャーだ。

Python 3.9のドキュメント「関数定義」の画面キャプチャー Python 3.9のドキュメント「関数定義」の画面キャプチャー

 「decorator」の定義がPython 3.8では「"@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE」となっているのが、Python 3.9では「"@" assignment_expression NEWLINE」と変わっている点に注目しよう。

 日本語にすると、Python 3.8までのデコレーター式では、アットマーク「@」の後には「dotted_name」つまり「識別子.識別子.……(引数リスト)」のような記述しかできなかったということになる。これに対して、Python 3.9ではアットマーク「@」の後に「assignment_expression」を書けるようになっている。では、assignment_expression(代入式)がどうなっているかというと、以下のようになっている。

代入式 代入式

 代入式はPython 3.8で導入された言語構造であり、if文やwhile文の条件など「(代入)文を置けない場所」において「識別子 := 式」という形で代入を行うためのものだ。代入式の定義にある「expression」とは、Pythonにおける「式」と考えてよい(デコレーターに関する制約の緩和が行われる基となったPEP 614ではこれを「if/elif/whileブロックでテストとして有効なもの全て」としている)。

 つまり、これまでデコレーター式は「@decorator_name」「@some.decorator.name(args)」のようにしか書けなかったが、Python 3.9からは「有効な式は何でも」デコレーター式に記述できるようになるということだ。もちろん、デコレーターとは「関数を引数として受け取り、関数を返送する関数」なので、式の評価結果は関数でなければならない。

 なぜ、このような緩和が行われたかというとアットマーク「@」の後には「識別子」とドット「.」と引数リストしか書けないという制約が強すぎたからだ。例えば、PEP 614では、PEP 614を提案することになった大きな理由として、PyQt5(クロスプラットフォームなGUIツールキットであるQtをPythonから使用するためのライブラリ)を例に取り、従来のデコレーター式では次のような記述ができないことを挙げている。

buttons = [QPushButton(f'Button {i}') for i in range(10)]

# Do stuff with the list of buttons...

@buttons[0].clicked.connect
def spam():
    ...

@buttons[1].clicked.connect
def eggs():
    ...

# Do stuff with the list of buttons...

Python 3.8までは記述できないコード(PEP 614より引用)

 このコードでは、10個のQPushButtonオブジェクトを要素とするリストを生成し、それぞれのボタンに対して、それらとクリック時に呼び出される関数を結び付けるコードを、デコレーター式を使って記述している。しかし、既に述べたようにPython 3.8までは「@識別子.識別子.……」のような記述しか許されていなかった。つまり、リストに保存されたQPushButtonオブジェクトを取り出すためのインデックス指定をデコレーター式に含めることはできない。PEP 614を見ると、この制約を回避するコードもあるが、それらはスッキリとしたコードではなく、効率的ともいえない。そこで、デコレーター式の記述において任意の式を書けるようにしようという提案が行われ、Python 3.9ではこれが取り込まれたということだ。

 PyQt5をインストールして実際にコードを試すというのも大変なので、ここでは同様なことを簡単に試しておこう。

def make_decorators():
    def decorator0(func):
        def inner_func0(*args):
            print('decorator 0')
            return func(*args)
        return inner_func0
    def decorator1(func):
        def inner_func1(*args):
            print('decorator 1')
            return func(*args)
        return inner_func1
    return [decorator0, decorator1]

decorators = make_decorators()

@decorators[0]
def hello():
    print('hello')

@decorators[1]
def goodbye():
    print('goodbye')

hello()
goodbye()

2つのデコレーターを要素とするリストを作成する関数と、それらを使った関数定義

 この例では、make_decorators関数は2つのデコレーターを作成して、それらを要素とするリストを返送するようになっている。そして、デコレーターにリストのインデックス指定を含める形でhello関数とgoodbye関数を定義し、呼び出している。Python 3.9ではこのコードは問題なく実行できる。

Python 3.9では実行できる Python 3.9では実行できる

 しかし、Python 3.8では以下のようにエラーとなる。

Python 3.8ではエラーとなる Python 3.8ではエラーとなる

 Python 3.8でリストに格納されているデコレーターを利用するには例えば、次のようなコードを書くことになる(以下はPEP 614で紹介されているものを、上記コード例に合わせて修正したもの。実行結果は省略)。

# リストからデコレーターを取り出して、デコレーターにインデックス指定が
# 含まれないようにする
decorator0 = decorators[0]
decorator1 = decorators[1]

@decorator0
def hello():
    print('Hello')

@decorator1
def goodbye():
    print('Goodbye')

hello()
goodbye()

# 引数をそのまま返す関数を定義し、インデックス指定は、
# 関数の引数に含めるようにする。_x自体はdotted_nameなので問題ない
def _x(func):
    return func

@_x(decorators[0])
def hello():
    print('hello')

@_x(decorators[1])
def goodbye():
    print('goodbye')

hello()
goodbye()

Python 3.8での回避策

 ハックとしては面白いが、コードを書く上ではあまりうれしくないだろう。特に最初の方法はデコレーターが多数あるときには到底書く気にはならないことが容易に予想できる。

 なお、Python 3.9では「代入式」をデコレーターに書ける。ということは次のようなコードも書けるということだ。実用上は何の意味もないが念のため、示しておこう。

@mydec:=decorators[0]
def foo():
    print('foo')

「@」の後に代入式を書いてデコレーターを定義

 PyQt5でスッキリとデコレーターを記述したいというのがPEP 614の提案の大きな動機となっているが、デコレーターに識別子とドット以外のものを書けたら便利だということが認められたことから、Python 3.9ではこれが導入されたということだろう。

新しいパーサーの採用

 Python 3.8まではPythonコードの解析(parse。パース)には「LL(1)」と呼ばれる手法を採用したパーサー(parser)が使われていた。しかし、Python 3.9では、PEG(Parsing Expression Grammar)と呼ばれる手法を採用したパーサーが採用されるようになった。

 これはPEP 617で提案されている。以下では、PEP 617を基に簡単になぜ新しいパーサーが採用されたかをまとめる。なお、LL(1)とPEGについては前掲したWikipediaへのリンクを参照してほしい(英語版のページの方がより詳細な説明となっている)。

 Pythonはこれまでに大きな進化を遂げ、これからもその進化は続く。その際に、従来のLL(1)ベースのパーサーによる制約が問題となる。実際、現在でもLL(1)ベースのパーサーではうまく処理できないことから、Pythonの文法を定める規則にはハックが加えられている場所もあるとのことだ。この影響として、Pythonの構文としては望ましくないものが文法的にはハックした規則にマッチしてしまうこともある。そうした望ましくないものをエラーとするには、それらを許さないように個別に処理する必要がある。

 例えば、Python 3.8で導入された代入式は、今述べたようなLL(1)ベースのパーサーで扱えるように規則をハックしたおかげで使えるようになった。ただし、その引き換えとして、文法的には「[x for x in y] := [1, 2, 3]」という式も規則にはマッチしてしまうという。が、これはPythonでは意味をなさない。そこで、このような望ましくない構文は許さないようにする処理がパース時には行われているということだ(実際、このコードは例外となるようになっている)。

 これから先もLL(1)ベースのパーサーを使い続けるとなると、このようなハックとそれによって生まれる望ましくないコードを文法的に認めないようにするための処理が増えていくことになる。また、LL(1)ベースのパーサーでは回避できない問題も出てきた。その代表的なコードとして例に挙げられているのが以下だ。

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

従来のパーサーでは解析できないコード(PEP 617より引用)

 上のコードを参考にして、カレントディレクトリにa.txt、b.txt、c.txtという3つのファイルがあるとして、以下のようなコードを実行してみよう。

with (
    open('a.txt') as a,
    open('b.txt') as b,
    open('c.txt') as c
):
    print(a.read())
    print(b.read())
    print(c.read())

Python 3.9で記述できるようになったwith文

 これをPython 3.8で実行すると次のようになる。

Python 3.8ではエラーとなる Python 3.8ではエラーとなる

 一方、Python 3.9では以下のように問題なく実行できる。

Python 3.9では実行できる Python 3.9では実行できる

 ここではwith文を例としたが、新しいパーサーがその力を示すのは将来の話だ。つまり、これから先、新機能をPythonに追加しようというときに、従来のパーサーではうまく扱えないものでも、新しいパーサーならうまく扱える可能性が高い(その典型例が上のコードということだ)。

 こうしたことから、次バージョンであるPython 3.10では従来のパーサー(とそれに依存した機能)はPythonから取り除かれることになっている。Python 3.9には従来のパーサーもあり、pythonコマンド実行時のコマンドラインスイッチ「-X oldparser」もしくは環境変数「PYTHONOlDPARSER=1」を指定することで、従来のパーサーを使える。

パッケージのトップレベルを越えた相対インポートでImportErrorが発生するように

 これまでパッケージのトップレベルを越えて相対インポートを行おうとしたときには、ValueError例外が発生していたが、Python 3.9からはImportError例外が発生するようになった。ValueError例外よりもImportError例外の方が状況をよく表す例外であることから、この変更が行われた(これに関するやりとりはここから辿れる)。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。