🐍

Python3.10 時代のモダン Python

2021/12/08に公開

この記事は刺身たんぽぽ同好会 Advent Calendar 2021[1] 8日目 の記事です.

7日目はげんしくんの 刺身たんぽぽ同好会を支えるDiscord鯖について - 最近のRecent です.
9日目はおのだ氏の Live2D #1 下準備(予定) です.

はじめに

Python3.10 がリリースされてから数ヶ月が経ちました.そこで,Python3.10 から入った新機能や,あまり知られていないが[2],知ってると便利な機能を紹介します.モダン Python を書いていきましょう.

型アノテーション

型アノテーション自体は Python3.5 からある機能[3]ですが,バージョンアップのたびに高機能になっています.Python3.10 では,| 演算子が型アノテーションに対しても使用できるようになりました.

使用例はこのような感じ

def f(x: int) -> int | str:
    if x % 2 == 0:
        return x
    return str(x)

Optional(null許容)な変数を表すときには,以下のようにします.

from typing import Optional

def g(x: Optional[int]) -> int:
    if not x:
        return 0
    return x
def g(x: int | None) -> int:
    if not x:
        return 0
    return x

変数そのものにもアノテーションできて,(ライブラリ側の問題で)型推論上では Any だが,実際には型が確定しているものなどで効いてきます.

import numpy as np

x: np.ndarray = np.array(x**2 for x in range(10))

少し複雑な型アノテーションも紹介します.
まずは tuple です.

from typing import Tuple

def henon(x: float, y: float, a: float, b: float) -> tuple[float, float]:
    "エノン写像"
    return (y+1-a*x**2, b*x)

<!-- また,以下のような書き方もできます

from __future__ import annotations


def henon(x: float, y: float, a: float, b: float) -> tuple[float, float]:
    "エノン写像"
    return (y+1-a*x**2, b*x)
``` -->

<!-- `from __future__ import annotations` というのは,Pytho3.10 で標準化されるはずでしたが,諸般の事情で見送られました.[^4] -->

関数を型アノテーションをするときは,`Callable` を使います.`[[引数のリスト], 返り値]` という順番で型アノテーションします.

```py
from typing import Callable

def henon(x: float, y: float, a: float, b: float) -> tuple[float, float]:
    "エノン写像"
    return (y+1-a*x**2, b*x)

def henon_map(
    f: Callable[[float, float, float, float], tuple[float, float]],
    a: float, b: float,
    first_term: tuple[float, float],
    n: int) -> list[tuple[float, float]]:
    "エノン写像をn回反復する"

    term = first_term

    return [first_term] + [term := f(*term, a, b) for _ in range(n-1)]

result = henon_map(f=henon, a=1.4, b=0.3, first_term=(0, 0), n=100_000)
# print(result[:10])

見慣れない記号が出てきたかもしれません.:=*term です.:= (代入式) については後ほど説明します. *term は,Iterable(list, tuple, str) などを要素ごとに展開(unpack)するという意味です.

関数や__init__メソッドに型アノテーションすることで,何を受け取り,何を返すかが明瞭になります.これはとても嬉しいことなのでやっていきましょう.また,関数名と引数から返り値が予測できないような関数は設計が悪い可能性があります.関数に持たせる機能を限定するなどすると,より読みやすくなるかもしれません.

from __future__ import annotations というモジュールがあります.Pytho3.10 で標準化されるはずでしたが,諸般の事情で見送られましたが便利なので紹介します.[4]

このモジュールでは型アノテーションの方法が変わるので,クラス内で自身を型名として扱うことができます.

from __future__ import annotations

class C:
    def __init__(self: C, x: int) -> None:
        self.x = x

    def __add__(self: C, other: C) -> C:
        # + 演算子の実装
        # アトリビュートxを足し算して新しいインスタンスを返す
        return C(self.x + other.x)

また,typing-extensions · PyPIというパッケージをPyPIからインストールし,自身の型をSelfという型名を用いて表現する方法もあります.

なお,Python3.11からは,Self がtypingモジュールにはいるので,上記のパッケージは不要になります.[5]

from typing_extentions import Self

class C:
    def __init__(self: Self, x: int) -> None:
        self.x = x

    def __add__(self: Self, other: Self) -> Self:
        # + 演算子の実装
        # アトリビュートxを足し算して新しいインスタンスを返す
        return C(self.x + other.x)

match-case 文

今まで Python には,他の言語でいうswitchのようなものはありませんでした.なので,頑張ってis-elif-else で処理をするしかありませんでした.

match の使用例は以下のとおりです.公式サンプルがわかりやすいので,こちらも参照してください.[6]

def http_error(status: int) -> str:
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

条件の後ろにif文を置くことでパターンガードができます.

def fizz_bazz(n: int) -> str:
    match n:
        case n if n % 15 == 0:
            return "FizzBuzz"
        case n if n % 3 == 0:
            return "Fizz"
        case n if n % 5 == 0:
            return "Buzz"
        case _:
            return str(n)

代入式

先ほど紹介した代入式(:=) Python3.8 から入った機能です.ウォルラス演算子(セイウチ演算子)と呼ばれることもあります.

例えば,以下のようなケースですっきりします.

a = list(range(10))

if len(a) > 5:
    print(f'aの長さは{len(a)}')
a = list(range(10))

if (x := len(a)) > 5:
    print(f'aの長さは{x}')

len()関数なので処理は軽いですが,2 回呼ぶのは気持ち悪いですね.Python では,if の後ろに文を取ることはできません.そして,=を用いた代入は文です.もう一つ注意しなければいけないのは,ウォルラス演算子は演算子の優先度が低いことです.代入するときはカッコでくくらないと意図しない挙動になることがあります.

これは None チェックにも使えます.

if tmp := return_optional_func():
    print(tmp)

内包表記と組み合わせると sum と同じようなことができて,

a = list(range(1, 11))
total = 0
[total := total + item for item in a]

そしてこれは関数を反復するときにとても強力です

from typing import Callable

import japanize_matplotlib  # matplotlibで日本語を使用するライブラリ
import matplotlib.pyplot as plt

def henon(x: float, y: float, a: float, b: float) -> tuple[float, float]:
    "エノン写像"
    return (y+1-a*x**2, b*x)

def henon_map(
    f: Callable[[float, float, float, float], tuple[float, float]],
    a: float, b: float,
    first_term: tuple[float, float],
    n: int) -> list[tuple[float, float]]:
    "エノン写像をn回反復する"

    term = first_term
    return [first_term] + [term := f(*term, a, b) for _ in range(n-1)]

def plot():
    result = henon_map(f=henon, a=1.4, b=0.3, first_term=(0, 0), n=100_000)
    xn, yn = zip(*result) # [(float, float)] -> (float), (float)
    fig, ax = plt.subplots()
    ax.set_xlim(-1.5, 1.5)
    ax.set_ylim(-0.5, 0.5)

    ax.scatter(xn[1000:], yn[1000:], s=0.3) # 過渡部分を切り落とす
    ax.set_xlabel(r"$x_n$")
    ax.set_ylabel(r"$y_n$")
    fig.tight_layout()
    plt.savefig("atructer.png")

plot()

f-strings

Python3.6からの機能です.Python で文字列に変数を埋め込むとき,C 言語と同じようにするか,format メソッドを使用する必要がありました.

name = "John"

print("Hello, %s!" % name)
print("Hello, {}!".format(name))

文字列の先頭にfをつけて,以下のようにできます.

name = "John"

print(f"Hello, {name}!")

また,f"{ver=}" とすると,変数名と値をセットで出力することができます

name = "John"

print(f"{name  = }")
# name = 'John'

様々な書式指定子があるので,詳しくはこちら[7]をご覧下さい.

dataclass

Python3.7からの機能です[8].雑に言うと,__init__ を自動実装してくれるというものです.dataclassデコレーターと,アトリビュート(クラス変数)に型アノテーションをつけることで行われます.

from dataclasses import dataclass

@dataclass
class InventryItem:
    name: str
    price: int
    quantity: int: = 0

    def total_cost(self):
        return price * quantity

以下の__init__と同じ意味です.

def __init__(self, name: str, price: int, quantity: int = 0):
    self.name = name
    self.price = price
    self.quantity = quantity

pipenv

Pythonで複数のバージョンを用意したい.ということがあると思います.Pipenvという仮想環境がおすすめです

まずはインストール.pipでいれることができます.

pip install pipenv

できることは,

  • Pythonのバージョンの固定
  • パッケージのインストール
  • 開発用パッケージのインストール(dev dependance)
    などです.生成されるPipfileがnpmのpackage.jsonのような役割を果たしてくれます.
pipenv --python 3.10               # Python3.10で初期化
pipenv install numpy               # numpyをインストール
pipenv install -r requirements.txt # requirements.txtからインストール
pipenv install --dev autopep8      # autopep8(フォーマッター)を開発用にインストール

Pipfileがある場合,

pipenv install       # 必要ライブラリをインストール
pipenv install --dev # 開発用パッケージも含めてインストール

で簡単に環境を用意することができます.これで,グローバルのpipを汚すことなく,クリーンに運用することができます.

詳しい使い方はPipenvを使ったPython開発まとめ - Qiita[9]がわかりやすいと思います.

終わりに

モダンなPythonについての記事でした.Pythonの文法はわかるけど… というところからステップアップしていきましょう.

こんな機能もあるよ.というのがればコメントで教えていただけるとありがたいです.

脚注
  1. 刺身たんぽぽ同好会 Advent Calendar 2021 - Adventar ↩︎

  2. 要出典 ↩︎

  3. typing --- 型ヒントのサポート — Python 3.10.0b2 ドキュメント ↩︎

  4. from __future__ import annotations が Python 3.10 でデフォルトにならなくなりました - methane のブログ ↩︎

  5. Python3.10 時代のモダン Python ↩︎

  6. What’s New In Python 3.10 — Python 3.10.1 documentation ↩︎

  7. Pythonのf文字列(フォーマット済み文字列リテラル)の使い方 | note.nkmk.me ↩︎

  8. dataclasses --- データクラス — Python 3.10.0b2 ドキュメント ↩︎

  9. Pipenvを使ったPython開発まとめ - Qiita ↩︎

Discussion