68
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

中級者へのModern Python

Posted at

はじめに

本記事の読者対象

  • Pythonの開発環境・ツールをさらに覚えたい方
  • よりモダンに近いPython環境が欲しい方

想定していない方

  • Python自体がはじめての方
  • Python上級者

説明すること・しないこと

説明する

  • ツールのおおまかな説明
  • ツールを使用する理由・嬉しさ
  • 参考になるドキュメント・URL

説明しない

  • 具体的なコマンド
  • 細かい文法

Modern Python

大学院で研究をするようになってから、かなりの時間Pythonを書くようになりました。
なぜならば、研究で利用しやすいライブラリが豊富であり、かつ研究のようなイテレーションがはやいプロジェクトに対して非常に有効であるためです。

しかし、Pythonは短期的にコードを試して動作を変更できる分、安定した動作が難しくなってきます。
たとえば、C++などはコンパイルを通す必要があるため、設計をうまく考えて実装する必要があります。
一方、Pythonはスクリプト言語なので最悪闇の設計をすると設計が適当でもなんとかなってしまいます。
ただ、このような後先考えないコードは将来的に大きな負債になります。
実際、締め切りが近い論文に合わせて急ピッチで書いたコードが現在自分の前に立ちはだかり、また1から設計・実装し直す羽目になりました。

Pythonは良くも悪くも以下のような特徴があります。

  • 型を設定しなくていい
    • その分、変数名に思いを馳せることになる
  • モジュールが多い
    • バージョンがずれる
  • パッケージマネージャーが豊富
    • どれを使えばいいのかが分かりづらい
  • 設計が甘くて良い
    • 読み辛くなる

今回は、これらの問題を解決できる可能性があるツールについてまとめます。
ただし、実際の使い方や詳細については割愛しているため、他記事を参考ください。

自分もはやくPythonに慣れて中級者になっていきたいです。

パッケージマネージャー

Pythonのパッケージマネージャーには多くの種類の組み合わせがあります。
以前pythonの環境構築戦争にイラストで終止符をどうやら打てないという記事でPythonの環境の構築について説明しました。
そこから自分の理解もある程度進み、今のPythonの環境構築これでいいかな〜と感じるものがあったため、それについてまとめようと思います。

パターン1: pyenv + pipenv

pyenv複数のPythonのバージョンを管理できます。
具体的には、Python3.7、Python3.8、Python3.9をそれぞれインストールして好きに切り替えることができます。
このようにPythonのバージョンを切り替えることによって、複数のプロジェクトに対応できます。
たとえば、昔から続いているプロジェクトのコードはPython2.7でしか動かないんだ!みたいなこともあります。
そのため、pyenvでバージョンを切り替えられるとうれしいわけです。

しかし、pyenvはバージョンを切り替えることはできますが、仮想環境を作成することはできません。
この仮想環境とはプロジェクトごとに利用する環境のことを本記事では指します。

もし仮想環境を切り分けられない場合を考えてみます。
機械学習のプロジェクト開発を行った後、別のプロジェクトに割り当てられDjangoの開発になったとします。
このとき、機械学習のpytorchのライブラリはDjangoに直接必要はありません。
大量にライブラリが増えてくると動作も重くなり、必要なバージョンの整合性が取れなくなってきます。
そのため、プロジェクトごとにライブラリをインストールできるような仮想環境が必要なわけです。

この問題を解決すべく、pipenvあるバージョンのPythonにおいて複数の仮想環境を作成することができます。
venvをラップするような便利な機能がpipenvにたくさん備わっているのでオススメです。
また、ここでは詳しくは述べませんが、ライブラリのバージョン管理をより賢く行えます。

まとめると、pyenvでPython自体のバージョンを管理し、pipenvで特定のPythonのバージョンにおける仮想環境を管理します。
Macではbrewでpipenv, pyenv両方インストールできます。

パターン2: pyenv + poetry

pyenvはさきほど出てきました。
今回、新しく出てきたのはpoetryになります。

poetryはrustっぽくライブラリ管理ができるマネージャーのようです。
まだ触ったことがないため詳しくないのですが、tomlファイル1つですべてを管理するらしいです。

かなり新しいマネージャーのようなので、今度触ってみたいです。

参考URL

型の導入

Pythonは型付けをしなくても動くスクリプト言語です。
そのため、初期段階ではかなりのスピードで開発できますが、後半になってくると変数に思いを馳せる時間が長くなり、実行時エラーに悩まされることが多くなります。

そこで、Python3.5から導入されたtypingなどを利用して型安全にコーディングできます。
しかし、プログラム実行時、変数に異なる型の内容が入っていてもエラーを出さないことに注意が必要です。

実行時に異なる型が入っていても止まりません。
しかし、IDEの恩恵を受けやすくなったり、第三者がコードの意味を理解しやすくなるため、積極的に書いていきましょう。

typing

def greeting(name: str) -> str:
    return 'Hello ' + name

Python3.5から、上記のように変数や関数などに型を指定できるようになりました。
型を指定することで、IDEの恩恵を受けやすくなります。
また、長期的に見るとコーディングの効率化に繋がります。

Finalキーワードもあり、定数などをより安全に設定できます。

詳しくは参考URLを御覧ください。

data classes

データを格納するクラスを簡単に作成するデコレータなどを提供します。

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

namedtuple

名前をつけたタプルを宣言できます。
書き換えられないデータを保証できるので、変更しないものを管理するには便利そうです。

可能であればtyping.NamedTupleを継承したクラスでタプルを宣言したほうがわかりやすいです。

class Employee(NamedTuple):
    name: str
    id: int

ジェネリクス

pydantic

FastAPIで採用されているライブラリです。
実行時に型情報を提供してくれます。

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: Optional[datetime] = None
    friends: List[int] = []
    
external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3'],
}
user = User(**external_data)

型があっていないデータが入ると実行時でも例外を投げてくれます。
(一方typingなどは実行時エラーを投げません。そのため、型でおかしい箇所がよりわかりやすく、堅牢性が上がります。)

参考URL

docstring

docstringは、関数やクラスなどの情報を表す文字列です。

関数やクラスがどのような引数・属性を持つのか、どのような振る舞いをするのかを文字列で記述します。

def add(x:int, y:int) -> int:
    """add function.

    xとyの和を計算する。

    Attributes
    ----------
    x: int
        足される数
    y: int
        足す引数

    Returns
    -------
    int

    Notes
    -----
    こういう関数に普通はここまで書くことはない(わかるので)
    """
    return x + y

docstringのメリット

docstringを書くことで以下のようなメリットがあります。

  • 他のチームメンバーに関数やクラスの振る舞いを伝えることができる
  • ドキュメントとコードの距離が近い
  • IDEなどにおいて、型や補足情報がより詳細になる
  • 後述しますが、Sphinxというドキュメント自動作成ツールでdocstringを利用できる
  • なにより将来の自分のためになる

スタイルの種類

docstringの書き方にはいくつかの流儀があります。
主なものは以下の3つです。

  • reStructuredText
  • NumPy
  • Google

それぞれ書き方に特徴があります。
自分の好きな書き方を参照するのが良さそうです。
また、チームでdocstringの書き方が決まっていればその書き方にならいましょう。

docstringは書き方のリファレンスが少なく、何かのライブラリを参考にすると良さそうです。

参考URL

スタイルガイド

コードを複数人で書くと、それぞれ異なるコードの書き方の癖が現れます。
" or '、文字数、変数の付け方などさまざまです。

異なるコードスタイルを統一的にするべく、コードチェック・Lint・フォーマッターなどがあります。
これらを用いることでより統一的なPythonコードを書くことができます。

この節では、そのうち自分が使っているものについてのみ触れます。
そのため、これら以外にも他のLinterがあります。ぜひ調べてみてください。

pep

pepは、python enhancement proposalの略で、ドキュメント・コーディング規約を指します。
Pythonをより良くするための提案書であり、有名なのはpep8です。

pep8は標準ライブラリなどのコーディング規約となっており、多くのPythonのコードはこのpep8を基準にしています。

flake8

flake8はpipでインストールできるコードのフォーマットをチェックするツールです。
コーディング規約を守っているかをチェックしてくれます。

flake8は以下3つのライブラリのラッパーになっています。

  • PyFlakes
  • pycodestyle
  • mccabe

flake8は、一行の文字数など、細かい規約の設定を行うことができます。
また、以下のようなflake8プラグインをpipインストールすると、flake8のコマンドを実行したとき、自動でそれらのプラグインも実行してくれます。

  • flake8-docstrings
  • flake8-mypy
  • flake8-isort
  • flake8-print

black

blackはコードフォーマッターです。
flake8は規約違反の箇所を教えるものでしたが、blackは実際にコードをフォーマットします。

blackの特徴は比較的新しくできたものであり、そして変更できる設定がかなり少ないです。
そのため、blackを使っておけば多くのプロジェクトで似たような強制フォーマットになります。

blackは非常に使いやすいため、ぜひ使いたいですね。

mypy

mypyは、コードのアノテーション・型を静的解析して、間違っている型を教えてくれます。
mypyのおかげで、間違っている箇所の型を治すだけで良くなります。

しかし、利用しているライブラリなどに対してエラーを出すこともあるため、そのときはスタブを生成したり、すでにpipで配布されているスタブをインストールする必要があります。

isort

isortは、pythonのimportの順番を修正します。
flake8にisortプラグインがあるため、順番がおかしいと警告されたときにisortをすると良さそうです。

参考URL

設定ファイル

Pythonのプロジェクトを作る際はいくつかの設定ファイルが出てきます。
この節ではこれらの設定ファイルについて説明します。

setup.py

setup.pyはプロジェクトを第三者に向けて配布するために利用するファイルです。 setuptools`というモジュールを用いて、プロジェクトのファイルをpipでインストールできるようなパッケージを作成します。
パッケージの情報やインストールの方法、URLなどを記述します。

# https://packaging.python.org/tutorials/packaging-projects/?highlight=setup.py#creating-setup-py
import setuptools

with open("README.md", "r", encoding="utf-8") as fh:
    long_description = fh.read()

setuptools.setup(
    name="example-pkg-YOUR-USERNAME-HERE", # Replace with your own username
    version="0.0.1",
    author="Example Author",
    author_email="author@example.com",
    description="A small example package",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/pypa/sampleproject",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.6',
)

普段何気なく書いているpip install numpyというコマンドは、setup.pyで作成されたパッケージをオンライン(PyPI)から取得し、site-packagesというディレクトリに入れています。

プロジェクトをパッケージとしてオンラインで公開したい場合はsetup.pyを書きましょう。

MANIFEST.in

setup.pyでプロジェクトからパッケージを作成するときに、pythonファイル以外をパッケージに含みたいときがあります。
たとえば、画像ファイルや音声ファイルなどが考えられます。

このとき、MANIFEST.inというファイルを作成することによって、より簡単にさまざま々なファイルをパッケージに含めてビルドできます。

setup.cfg

setup.pyはパッケージとして公開、インストールするために必要です。
しかし、直接Authorやインクルードするファイルなどを指定してしまうと、後から変更しづらくなります。

そこて、設定ファイルであるsetup.cfgを追加で作成することによって、
パッケージで使う情報を独立して管理できます。
setup.pyを実行したときにsetup.cfgがあった場合は、情報を取り出して内容を上書きし、それからパッケージを作成します。

[metadata]
name = my_package
version = attr: src.VERSION
description = My package description
long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
keywords = one, two
license = BSD 3-Clause License
classifiers =
    Framework :: Django
    License :: OSI Approved :: BSD License
    Programming Language :: Python :: 3
    Programming Language :: Python :: 3.5

[options]
zip_safe = False
include_package_data = True
packages = find:
scripts =
    bin/first.py
    bin/second.py
install_requires =
    requests
    importlib; python_version == "2.6"

requirements.txt

pipでインストールしたパッケージのリストが表されたファイルです。
必ずしもrequirements.txtという名前でなくても構いませんが、慣習的にこの名前になっています。

このファイルを作成してGitHubなどに含めることによって、
pip install -r requirements.txtで簡単に第三者がパッケージをインストールできます。

しかし、依存性の解決に向いておらず、ライブラリのバージョンの更新をし辛いなどの問題がrequirements.txtには存在しています。
そのため、現在はpipenvではPipfile、poetryではpyproject.tomlという、別のパッケージ管理のファイルが利用されています。

Pipfile/Pipfile.lock

Pipfile, Pipfile.lockはrequirements.txtの問題を解決したpipenvの管理ファイルです。

Pipfileには、直接依存するライブラリが記入されます。
たとえば、requestsを使ってURLを叩くプロジェクトをつくる場合には以下のようなPipfileを作成します。

[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[packages]
requests = "*"

このとき、requestsは他のライブラリに依存しています。
たとえば、chardetと呼ばれるファイルの文字コードを判別するライブラリを内部で使用しています。

ここで、chardetのバージョンはPipfileには記入されません。
一方で、ライブラリすべてのバージョンがPipfile.lockに記入されます。

このとき、直接使用したいトップレベルのライブラリはPipfileで管理できるため、簡単にバージョンのアップグレードを検討できます。
また、依存するライブラリすべてのバージョンはPipfile.lockで管理されるため、第三者が同じ実行環境を簡単に用意できます。
これによって、依存性の問題をファイルを分けることで解決できました。

pyproject.toml

pyproject.tomlPEP 518で定義された、パッケージの設定を管理するファイルです。
最近ではpoetryというパッケージマネージャーがこのファイルを利用していますが、
poetryに限った設定ファイルではないらしいです。
対応している任意のパッケージマネージャーがpyproject.tomlを使えます。

これまでは、requirements.txt, setup.py, setup.cfg, MANIFEST.inなど多くのファイルがパッケージ公開に必要でしたが
pyproject.tomlはこのファイルのみでこれらをすべて補う機能を果たします。

現在pipenvを使っていますが、poetryならびにpyproject.tomlに興味が湧いたため使ってみようと思います。

tox.ini

toxは、Pythonのテストを自動化するライブラリです。
toxコマンドを打つことで、tox.iniに書かれたテストの内容をすべて自動実行してくれます。

1つのpytestぐらいであれば、毎回そのコマンドを打つだけで良いです。
しかし、配布するバージョンに合わせて、python2.7, python3.8, python3.9など複数バージョンでテストを行いたいことがあります。
また、flake8のようなコーディング規約を満たしているかのテストだけを行いたい場合もあります。

そこで、toxならびに設定ファイルtox.iniを使うことで、1コマンドのみですべてのテストを自動実行できます。
しかも、toxはtoxが扱う特別なディレクトリ内部に他のバージョンのPython環境を作成します。
そのため、テストごとに仮想環境が作成され、テスト間の依存がないという嬉しさもあります。

 [tox]
 # 使用する環境を指定する
 # 名前が一致しているflake8-py38は[testenv:flake8-py38]を実行する
 # py38は[testenv:py38]がないため[testenv]を実行する
 envlist =
     py38
     flake8-py38
     mypy-py38
 
 [testenv]
 deps = pipenv
 # テストで実行するコマンド
 # このコマンドはpipenv installするだけなので当然テストをパスする
 commands =
     pipenv install
 
 [testenv:flake8-py38]
 basepython = python3.8
 description = 'check flake8-style is ok?'
 commands=
     pipenv install
     pipenv run flake8 gym_md
 
 # 設定ファイル
 # https://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations
 [flake8-py38]
 max-line-length = 88
 
 
 [testenv:mypy-py38]
 basepython = python3.8
 description = 'check my-py is ok?'
 commands =
     pipenv install
     pipenv run mypy gym_md

参考URL

PyPI

PyPIは、pythonのライブラリをアップロードできるサイトです。
pip installをした場合などは、ここからダウンロードされています。

テスト

pythonには複数のテストツールがあります。

unittest

標準のテストライブラリはunittestです。
標準パッケージに含まれているため、インストールせずにテストを書けます。

TestCaseクラスを継承し、testから始まるメソッドを作ります。

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

pytest

pytestは、サードパーティ製のテストライブラリです。
pytestは関数をベースにテストを行い、unittestよりも詳細なエラーを出してくれます。

たとえば、以下のように出力値が間違っていた場合に、間違えた場所とその値を出力します。
pytestは簡単にかけて出力値もわかりやすいため、私はpytestを使っています。

# content of test_sample.py
def inc(x):
    return x + 1


def test_answer():
    assert inc(3) == 5
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item

test_sample.py F                                                     [100%]

================================= FAILURES =================================
_______________________________ test_answer ________________________________

    def test_answer():
>       assert inc(3) == 5
E       assert 4 == 5
E        +  where 4 = inc(3)

test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================

doctest

doctestは、さきほど出てきたdocstringでテストを実行することを可能とします。
doctestをインポートすると、もしdocstringに>>>で実行例が書いてあったらその通りの動作になるかをテストしてくれます。

pytestのように、複雑なテストを書くことはできません。
しかし、docstringの中にあるため、テストとして利用しながら実行例として第三者に提示できます。
そのため、コードがどのような動作をするのか理解しやすく、コードも変更しやすくなります。

def square(x):
    """Return the square of x.

    >>> square(2)
    4
    >>> square(-2)
    4
    """

    return x * x

if __name__ == '__main__':
    import doctest
    doctest.testmod()

tox

さきほど出て来ましたが、toxを書くことで複数のテスト・コマンドを自動化できます。

参考URL

ドキュメント

Sphinxは、きれいなドキュメントを簡単につくることができるツールです。
Pythonのライブラリのリファレンスの多くは、このSphinxで作成されています。

Sphinxは、reStructuredTextと呼ばれるマークアップ言語を用いてドキュメントを作成します。
このとき、自動でドキュメントを作成する機能であるsphinx-apidocが付属しており、docstringをPythonコードにきちんと書いていれば、コマンド一発でドキュメントを作成できます。
そのため、docstringを書くと型や情報などを将来のために残すことができ、しかもそれがそのままリファレンスになります。
これによって、ドキュメントがコードに比べて更新されなくなり、ドキュメントが形骸化してしまうという負債も発生しづらくなりますね。

ただし、shpinx-apidocによって作成されるbstファイルはデフォルトのため、立派なものにするためには自分で追加編集する必要があります。

参考URL

cookiecutter

cookiecutterは、Pythonのきれいなプロジェクトを簡単に作成することができるツールです。
利用できるテンプレートがGitHubで公開されており、このテンプレートを指定するとよく設計されたプロジェクトを簡単にローカルに作成できます。
boilerplateに自分の情報を設定しながら
ローカルに作成できるツールと考えると良さそうです。

たとえば、cookiecutter-pypackageを指定すると簡単に以下が設定されたプロジェクトを作成できます。

  • 洗練されたプロジェクト設計
  • TravisCIを用いたテスト自動化
  • Shpinxを用いたドキュメント作成
  • toxを用いた複数環境でのテスト
  • PyPIへの自動リリース
  • CLIインターフェイス(click)

これまで説明してきたものが一括で用意できるのでうれしいですね。

参考URL

さいごに

Pythonの開発ツールについてまとめてきました。
まとめる中で、自分もまだまだ理解が浅い、使いこなせていないと実感しました。

pythonicなコードが書けるように、毎日練習していきたいです。

68
84
0

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
68
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?