2019/10/21

:= (ウォルラス演算子)の使い方


skeeze による Pixabay からの画像

Python の := 演算子について説明します。

:= 演算子は Python のバージョン 3.8 ( 2019 年 10 月リリース)で導入されました。該当する PEP は PEP 572 です。

呼び方


呼び方は、 := 演算子のことは walrus operator:= を含む式全体のことは assignment expressions と呼ぶのが主流のようです。

日本語では、前者はカタカナで ウォルラス演算子 、あるいは訳して セイウチ演算子 ( walrus = セイウチ )でしょうか。 assignment expression は別の言語で一般的な 代入式 がよいですかね。

PEP 572 には named expressions と呼ばれることもあると書かれています。

ウォルラス演算子という呼び名は日本ではあまり流行らなさそうな感じもしますが、本記事内ではウォルラス演算子と代入式という表現で行きます。

ウォルラス演算子とは


Python のウォルラス演算子は既存の = 演算子で作れる代入文( assignment statements )と同じく、名前(≒変数)に値をひもづけるための演算子です。

既存の = ・代入文は、別の式の中に組み込んで使うことができませんでした。ウォルラス演算子を使うことで、式の途中結果を名前にひもづける(≒代入する)ことができます。

JavaScript ・ Ruby ・ PHP 等 Python に似たスクリプト言語には以前から代入式が存在していたため、それらの言語をメインで使い Python をサブで使うような方には Python の既存の代入文( = )は融通が利かない・非直感的な感じに感じられたことと思います。

使い方


ウォルラス演算子・代入式は次のように使うことができます。

import random


def getvalue():
    if random.random() > 0.2:
        return 'OK'

    return None


if value := getvalue():
    print(value)

while value := getvalue():
    print(value)

このプログラムを実行すると OK という文字がランダムに 2 〜 7 行ほど表示されて終了します。

ウォルラス演算子が登場するまでは、この if 文と while 文のところはそれぞれ次のように書く必要がありました。

value = getvalue()
if value:
    print(value)

while True:
    value = getvalue()
    if value:
        print(value)
        continue
    break

ウォルラス演算子をうまく使うことでコードが短くわかりやすくなります。

メリット・使いどころ


ウォルラス演算子・代入式のメリットは、既存の代入文( = )では短く書けなかったコードが短く書けてコードがコンパクトになることです。

ただし、他の言語における代入式と同様に、濫用するとかえってわかりづらくなったりバグの温床になったりするので、むやみやたらと使うのではなく、使えばコードがよくなるところでだけ使うのがよいと思います。

使いどころのひとつは、 callable (関数やメソッド等)が必ず truthy な値か falsy な値を返し、 truthy な値を返す場合にだけ特定の処理を行いたいような場合 です。

while stream := getstream():
    process(stream)

if match := re.match(pattern, text):
    print(match)

PEP 572 には、標準ライブラリの rere.match()re.search() の例が紹介されています:

if match := re.search(pat, text):
    print("Found:", match.group(0))
elif match := re.search(otherpat, text):
    print("Alternate found:", match.group(0))
elif match := re.search(third, text):
    print("Fallback found:", match.group(0))

ウォルラス演算子による代入式は別の式の途中に差し込めるので、 デバッグのためにメソッドチェーンの途中の値を確認したいとき なんかにも便利かもしれません。例えば次のようなクラスがあるとします。

class A:
    def f1(self):
        ...
        return self

    def f2(self):
        ...
        return self

    def f3(self):
        ...
        return self

これを次のように利用している場合を考えます。

A().f1().f2().f3()

この途中にウォルラス演算子を差し込むことで値を取り出せます。

(f2_result := A().f1().f2()).f3()
logger.debug(f2_result)

その他、 lambda 式や三項演算(インラインの ifelse )との組み合わせでも価値を発揮してくれそうです。

留意点


ウォルラス演算子・代入式にはいくつか注意点があります。

スコープを作らない

ウォルラス演算子は新たなスコープを作るわけではありません。ウォルラス演算子で値にひもづけられた名前は、(削除しないかぎり)既存のスコープの末尾まで存在し続けます。 JavaScript の let のような挙動を期待して利用すると思わぬバグを生む可能性があります。

単独で文にすることはできない

ウォルラス演算子・代入式を既存の = ・代入文の代わりに使うことはできません。

次のコードを実行すると SyntaxError があがります。

val := 5
# => SyntaxError: invalid syntax

しかし、次のコードは問題なく通ります。

(val := 5)

次のコードも大丈夫です。

val2 = (val1 := 5)

ただし、このような使い方(=本来通常の = を使うべきところで := を使うこと)は紛らわしいので原則使わない方がよいと思います。 PEP でも非推奨と書かれています。

= と同じ挙動ではない

ウォルラス演算子・代入式の挙動は既存の = ・代入文と必ずしも同じではありません。

例えば、 = の代入では右辺が tuple の場合にそのかっこを省略することができますが、ウォルラス演算子の場合は右辺の tuple のかっこを省略することができません。かっこ無しで , 区切りで複数の値を右辺に書くと思いも寄らない挙動になります。

val1 = 3, 4
print(val1)
# => (3, 4)

(val2 := 3, 4)
print(val2)
# => 3

(val3, val4 := 3, 4)
NameError: name 'val3' is not defined

所感


いち Python ユーザーとしての個人的な感想です。

:= 演算子が Python に導入されると初めて聞いたときは「えー」と思いましたが、時間が経つうちに慣れてきて、いまはそれほど違和感を感じません。

ただし、従来の代入文と新しい代入式とで書き方のパターンが増えてしまうので、 Python らしいかというとあまり Python らしくはないと思います。しかし、代入式を使うときれいに書けそうなパターンがいくつか思いつくので、便利かそうでないかというと便利だと思います。

もう少し引いた視点から見ると、 Python はいま「最も使われているプログラミング言語のひとつ」でかつ「最もシェアが伸びているスクリプト言語」です。そのあたりの状況を自覚して(?)このあたりの仕様をちょっと緩くするという判断はとてもよい気がします。

以上です。というわけで、 Python の := 演算子についてでした。

参考

2019/10/08

Python Tips: Python で BOM つき UTF-8 のファイルを探したい

Python で BOM 付き UTF-8 でエンコードされたファイルを特定のディレクトリの中から探す方法についてです。

UTF-8 の BOM とは何ぞやというお話は別記事で述べているので興味のある方はそちらをご覧ください:


ここでは特定のディレクトリの下に BOM 付きの UTF-8 と BOM なしの UTF-8 、そしてその他のエンコーディングのファイルも混在している状況を想定しています。その中から BOM 付き UTF-8 のファイルを全件リストアップしたいイメージです。

上の記事で述べたとおり、 BOM 付きの UTF-8 かどうかを見分けるには 'utf-8' で読み込んで置いて先頭の文字が BOM を表す '\ufeff' と一致するかどうかをチェックすれば OK です。そのため、あとは特定のディレクトリ以下のファイルを順に見ていくだけです。ファイルを見ていくには pathlib.Path.glob() あたりを使うのがシンプルでかんたんだと思います。

以下一連のコードです。

"""Locates files with utf-8 with BOM in a directory.
"""

import sys
from pathlib import Path

ENCODING = 'utf-8'
BOM = '\ufeff'


def main():
    current_dir = Path('.')
    for path in current_dir.glob('**/*.*'):
        if path.is_dir():
            continue

        try:
            if has_bom(path):
                print(path)
        except UnicodeDecodeError:
            print(f'{path} is not encoded with {ENCODING}.', file=sys.stderr)
            continue


def has_bom(path: Path) -> bool:
    try:
        with path.open(encoding=ENCODING) as f:
            line_first = f.readline()
            if line_first.startswith(BOM):
                return True
    except UnicodeDecodeError as e:
        raise

    return False


if __name__ == '__main__':
    main()

UTF-8 で読み込めないファイル(= UTF-8 以外のエンコーディングのファイル)があった場合は UnicodeDecodeError が上がるので適宜処理をします。ここでは対象のファイル名を含むエラー文を標準エラーに出力しています。

以上です。

2019/08/21

Python Tips:スーパークラスのメソッドをオーバーライドできているか確認したい

今回は スーパークラスのメソッドをオーバーライドをできているか確認する方法 についてです。

Python には言語そのものの機能として「特定のメソッド定義が祖先クラスのメソッドの正しいオーバーライドであること」を確約する方法が(私が知るかぎり)ありません。

そのため、他人が作ったクラス(ライブラリやフレームワークが提供するクラス)を継承したクラスを作成しメソッドをオーバーライドするときには、メソッド名のタイポに十分に注意する必要があります。オーバーライドしたつもりでできていないと思わぬバグに苦しめられ多くの時間を浪費してしまうこともあります。また、ライブラリのバージョンアップ等によって、継承元クラスのメソッドがいつの間にかなくなってしまい、オーバーライドがいつからかうまく行かなくなっていた、ということもあります。

そんなときに便利なのが、メソッド定義時に「スーパープラスのメソッドを正しくオーバーライドできていること」を確認する仕組みを入れておくことです。

具体的には、次のようなデコレータを定義します。

# f-string を使用しているため、動かすには Python 3.6 以上が必要です。

def overrides(klass):
    def check_super(method):
        method_name = method.__name__
        msg = f'`{method_name}()` is not defined in `{klass.__name__}`.'
        assert method_name in dir(klass), msg

    def wrapper(method):
        check_super(method)
        return method

    return wrapper

このデコレータ overrides は次のように使用することができます。

class A:
    def method1(self):
        pass


class B(A):
    @overrides(A)
    def method1(self):
        pass

クラス B はクラス A を継承しています。そして、クラス B のメソッド method1() はクラス Amethod1() をオーバーライドしています。

このコードはエラーなく動いてくれます。

一方、次のクラス C の場合は C の定義時( compile time )にエラーが発生します。

class A:
    def method1(self):
        pass


class C(A):
    @overrides(A)
    def method2(self):
        pass

クラス C はクラス A を継承しています。ただし、クラス C ではクラス A のメソッド method1() をオーバーライドしたつもりがタイポにより method2() という名前になってしまっています。

この C のコードを実行すると、次のようなエラーが発生し、オーバーライドの間違いを早期に教えてくれます。

Traceback (most recent call last):
  File "xxx.py", line n, in 
    class C(A):
  File "xxx.py", line n, in C
    @overrides(A)
  File "xxx.py", line n, in wrapper
    check_super(method)
  File "xxx.py", line 14, in check_super
    assert method_name in dir(klass), msg
AssertionError: `method2()` is not defined in `A`. 

以上です。

ちなみに、今回のものはデコレータを使ったアプローチでしたが、これと同じことを実現する他の方法としては「メタクラスを使った方法」や「 __init_subclass__() を使った方法」等があるようです。

参考: