2018/07/03

2018 年上半期に GitHub でスターの多かった Python リポジトリ

2018 年の前半があっという間に終わりましたね。

2018 年も後半に差しかかったので、 2018 年上半期に登場した GitHub リポジトリのうち多くのスターが付けられた Python 関連リポジトリを調べてまとめてみました。次の記事と同様のまとめです。


具体的には次の基準でリポジトリをピックアップしました。

  • GitHub に認識されているリポジトリの言語が Python
  • 2018 年 7 月時点でスター数が 2000 以上

ちなみに、 2018 年のまとめでは「スター数 2500 以上」を条件としましたが、今回は少し条件を緩めて 2000 以上にしました。理由は、 2018 年前半が終わってすぐに調べるとスターが付くのに十分な時間が経っておらずリポジトリ数が少なかったためです。

2018 年上半期にスター数の多かった GitHub リポジトリ


上の条件で検索した結果、ひっかかったリポジトリの数は 10 個です。

以下、スター数が多かったものから順にあげていきます。 description の翻訳とかんたんな説明をつけているので、興味のある方はご覧になってみてください。

FastPhotoStyle

画像の雰囲気を他の画像に写す機能を提供するライブラリ。「コンテンツ画像」と「スタイル画像」の 2 つの画像があったときに、スタイル画像のスタイル(色味)をコンテンツ画像に適用した画像を自動生成してくれるようです。

NVIDIA 社製で、ライセンスは CC BY-NC-SA 4.0 。

リポジトリ名FastPhotoStyle
説明Style transfer, deep learning, feature transform
説明(翻訳)スタイルの転写、ディープラーニング、特徴変換
URLhttps://github.com/NVIDIA/FastPhotoStyle
ホームページ-
スター8330

black

Python コードを整形する black コマンドを提供するライブラリ。設定値(≒自由度)を極力少なくすることで、シンプルに使えることを目指したフォーマッタです。

ライセンスは MIT 。

リポジトリ名black
説明The uncompromising Python code formatter
説明(翻訳)妥協なしの Python コードフォーマッタ
URLhttps://github.com/ambv/black
ホームページhttps://black.readthedocs.io/en/stable/
スター4538

Douyin-Bot

中国語で書かれておりよくわかりませんが、美人の画像を自動で探してくるボットのようです。

リポジトリ名Douyin-Bot
説明Python 抖音机器人,论如何在抖音上找到漂亮小姐姐?😍
説明(翻訳)-
URLhttps://github.com/wangshub/Douyin-Bot
ホームページhttps://zhuanlan.zhihu.com/p/37365182
スター4058

Python-100-Days

中国語で書かれておりよくわかりませんが、「 100 日で Python をマスターしよう」的な Python チュートリアルのようです。

リポジトリ名Python-100-Days
説明Python - 100天从新手到大师
説明(翻訳)-
URLhttps://github.com/jackfrued/Python-100-Days
ホームページ-
スター3856

AutoSploit

Metasploit のモジュールを使ってセキュリティ脆弱性を突いた攻撃を大量に行うためのライブラリ(悪用厳禁のもの)。

リポジトリ名AutoSploit
説明Automated Mass Exploiter
説明(翻訳)セキュリティ攻撃の自動化
URLhttps://github.com/NullArray/AutoSploit
ホームページ-
スター3096

vibora

Python 3.6+ の async 機能を利用した高速ウェブアプリケーションフレームワークです。

リポジトリ名vibora
説明Fast, asynchronous and elegant Python web framework.
説明(翻訳)高速・非同期のエレガントな Python ウェブフレームワーク。
URLhttps://github.com/vibora-io/vibora
ホームページhttps://vibora.io/
スター2599

minigo

説明のとおり、 AlphaGo を越える性能を発揮した AlphaGoZero を Python で実装したリポジトリとのことです。

リポジトリ名minigo
説明An open-source implementation of the AlphaGoZero algorithm
説明(翻訳)AlphaGoZero アルゴリズムのオープンソース実装。
URLhttps://github.com/tensorflow/minigo
ホームページ-
スター2188

Tensorflow-Project-Template

マシンラーニングのフレームワーク TensorFlow のプロジェクトの参考テンプレートです。 base model trainer data_loader utils 等のディレクトリを提案しています。

リポジトリ名Tensorflow-Project-Template
説明A best practice for tensorflow project template architecture.
説明(翻訳)TensorFlow プロジェクトのテンプレートアーキテクチャのベストプラクティス。
URLhttps://github.com/MrGemy95/Tensorflow-Project-Template
ホームページ-
スター2121

rebound

python コマンドの代わりに使用することで、 Python スクリプトの実行中のエラー発生時に Stack Overflow でエラーを検索した検索結果を表示してくれる rebound コマンドです。

リポジトリ名rebound
説明Command-line tool that instantly fetches Stack Overflow results when you get a compiler error
説明(翻訳)コンパイラエラーが出たときに Stack Overflow での検索結果をすぐに取得するコマンドラインツール
URLhttps://github.com/shobrook/rebound
ホームページ-
スター2116

gif-for-cli

アニメーション GIF 画像または Tenor ( tenor.com )というサイトの GIF 画像からアニメーションアスキーアートを生成するライブラリ。

Google 社製。

リポジトリ名gif-for-cli
説明-
説明(翻訳)-
URLhttps://github.com/google/gif-for-cli
ホームページhttps://opensource.googleblog.com/2018/06/tenor-gif-for-cli.html
スター2050

以上 10 つの Python リポジトリが 2018 年上半期では人気でした。

所感


2017 年に引き続き、マシンラーニング関連のリポジトリが多かった印象です。

Python にかぎらない話ですが、近年は中国語のリポジトリが目に見えて増えてきたのも印象的でした。いつか中国語リポジトリが上位の大半を占めるような日が来るのでしょうか。

個人的には、 vibora rebound あたりに興味があるので、チャンスがあれば試してみようと思います :)

2018/06/26

Python Tips:月の初日や最終日を取得したい

Python で、月の初日や最終日を取得する方法をご紹介します。

標準ライブラリを使った方法


月の初日を取得する

月の初日を取得するには、 datetime.datetimedatetime.datereplace() メソッドを使った方法が便利です。

import datetime

def get_first_day_of_month(date=None):
    '''指定された日付の月の最初の日を返す'''
    if not date:
        date = datetime.date.today()
    return date.replace(day=1)

テストを書いてみましょう。

import unittest

class TestGetFirstDayOfMonth(unittest.TestCase):
    def test_特定の日(self):
        date = datetime.date(2018, 12, 15)
        first_day = get_first_day_of_month(date)

        self.assertEqual(first_day, datetime.date(2018, 12, 1))

    def test_当日(self):
        first_day = get_first_day_of_month()
        today = datetime.datetime.today()

        self.assertEqual(first_day.day, 1)
        self.assertEqual(first_day.month, today.month)
        self.assertEqual(first_day.year, today.year)

上の 2 つのコード片をあわせたスクリプトを python -m unittest スクリプト名 で実行すると、テストがパスすることが確認できます。

月の最終日を取得する

月の最終日を取得する場合は初日の場合より少し複雑です。月によって最終日が変わるからです。

calendarmonthrange() 関数には月の最終日を返す機能が備わっているのでこれを使用するのがかんたんです。

import calendar
import datetime

def get_last_day_of_month(date=None):
    '''指定された日付の月の最終日を返す'''
    if not date:
        date = datetime.date.today()
    last_day = calendar.monthrange(date.year, date.month)[1]
    return date.replace(day=last_day)

calendar.monthrange() は、年と月を引数に取り、要素数 2 のタプルを返します。タプルの要素は、その月の初日の曜日を表す整数( 0 が月曜日)と、その月の日数です。

import calendar

calendar.monthrange(2020, 7)
# => (2, 31)
# ( 2020 年 7 月の 1 日は水曜日で、日数は 31 日)

calendar.monthrange(2020, 8)
# => (5, 31)
# ( 2020 年 8 月の 1 日は土曜日で、日数は 31 日)

こちらもテストしてみます。

import unittest

class TestGetLastDayOfMonth(unittest.TestCase):
    def test_4月(self):
        date = datetime.date(2020, 4, 7)
        last_day = get_last_day_of_month(date)

        self.assertEqual(last_day, datetime.date(2020, 4, 30))

    def test_8月(self):
        date = datetime.date(2020, 8, 10)
        last_day = get_last_day_of_month(date)

        self.assertEqual(last_day, datetime.date(2020, 8, 31))

    def test_うるう年の2月(self):
        date = datetime.date(2020, 2, 5)
        last_day = get_last_day_of_month(date)

        self.assertEqual(last_day, datetime.date(2020, 2, 29))

こちらも実行するとすべてパスすることが確認できます。

前月の最終日を取得する

前月の最終日を取得する方法についてはアプローチがいくつか考えられますが、最もシンプルでかんたんなのは「前月の最終日 = 今月の初日の前日」と考える形ではないかと思います。

import datetime

def get_first_day_of_month(date=None):
    '''指定された日付の月の最初の日を返す'''
    if not date:
        date = datetime.date.today()
    return date.replace(day=1)

def get_last_day_of_prev_month(date=None):
    '''指定された日付の前月の最終日を取得する'''
    if not date:
        date = datetime.date.today()
    first_date = get_first_day_of_month(date)
    return first_date - datetime.timedelta(days=1)

こちらもテストを書いてみます。

import unittest

class TestGetLastDayOfPrevMonth(unittest.TestCase):
    def test_4月(self):
        date = datetime.date(2020, 4, 7)
        last_day = get_last_day_of_prev_month(date)

        self.assertEqual(last_day, datetime.date(2020, 3, 31))

    def test_1月(self):
        date = datetime.date(2020, 1, 20)
        last_day = get_last_day_of_prev_month(date)

        self.assertEqual(last_day, datetime.date(2019, 12, 31))

こちらも python -m unittest で実行すると、すべてパスすることが確認できます。

以上は Python に同梱の標準ライブラリを使った方法でした。続いて標準ライブラリ以外のパッケージを使った方法を見てみましょう。

標準ライブラリ以外のパッケージを使った方法


さまざまなパッケージがあるかと思うのですが、ここでは名前とインタフェースがイケている Delorean (デロリアン)というパッケージを使った方法をご紹介します。 Delorean を使うとかんたんにタイムトラベル(時間の変更)を行うことができます。




from delorean import Delorean

月の初日を取得する

from delorean import Delorean

def get_first_day_of_month2(delorean):
    '''指定された日付の月の初日を返す'''
    return delorean.truncate('month')

Delorean オブジェクトは datetime.datetime をラップしたオブジェクトです。アトリビュート datedatetime で日付や日時を返してくれます。

テストすると次のようになります。

import unittest

class TestGetFirstDayOfMonth2(unittest.TestCase):
    def test_4月(self):
        d = Delorean(datetime.datetime(2020, 4, 10), timezone='Asia/Tokyo')
        d_first = get_first_day_of_month2(d)

        self.assertEqual(d_first.date.day, 1)

        tz_tokyo = pytz.timezone('Asia/Tokyo')

        # *1
        self.assertEqual(
            d_first.datetime,
            tz_tokyo.localize(datetime.datetime(2020, 4, 1)),
        )

        # *2: pytz.timezone を datetime.datetime() に渡して
        # 使うと時間がズレることがあり次は等しくならない
        self.assertNotEqual(
            d_first.datetime,
            datetime.datetime(2020, 4, 1, tzinfo=tz_tokyo),
        )

以下少し本題から外れた余談です。

pytz のタイムゾーンが謎の LMT 9:19 になる問題

*2 のコメントに書いているとおり、 pytz.timezonedatetime.datetime() に渡して使うと思わぬ挙動をすることがあるので、この方法で日時オブジェクトを生成してはいけません。代わりに *1 のように pytz.timezone.localize() を使う必要があります。

というのは、 pytz.timezonedatetime.datetime() に渡して使うと、 pytz.timezone('Asia/Tokyo') が指すタイムゾーンが JST+9:00 ではなく LMT+9:19 になることがあるためです。

import pytz

# JST じゃない何か変なのが出てきた
pytz.timezone('Asia/Tokyo')
# => <DstTzInfo 'Asia/Tokyo' LMT+9:19:00 STD>

# localize() 後のものは正しい JST になっている
pytz.timezone('Asia/Tokyo').localize(2018, 5, 27).tzinfo
# => <DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>

この原因はどうも、 pytz のタイムゾーンオブジェクトは実はタイムゾーンではなく場所を表していて、時代によって指し示すタイムゾーンが自動的に切り替わるように作られているらしいのですが、日時をまったく指定しなかったときのデフォルト値の問題のようです。このデフォルト値は pytz が使用している IANA のタイムゾーンデータベースの次の部分から来ているそうです。

# Zone  NAME    GMTOFF  RULES FORMAT  [UNTIL]
Zone  Asia/Tokyo  9:18:59 - LMT 1887 Dec 31 15:00u
      9:00  Japan J%sT}
# Since 1938, all Japanese possessions have been like Asia/Tokyo.

確かに LMT でほぼ 9:19 ですね。

ちなみに、この 1887 年というのは、日本の標準時が明石に定められた 1888 年の前年です。 pytz.timezone('Asia/Tokyo') は、自動的に、 1887 年 12 月 31 日までは LMT 、 1888 年 1 月 1 日以降は JST になります。

import datetime
import pytz

pytz.timezone('Asia/Tokyo').localize(datetime.datetime(1880, 5, 27))
# => datetime.datetime(1880, 5, 27, 0, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' LMT+9:19:00 STD>)

pytz.timezone('Asia/Tokyo').localize(datetime.datetime(2030, 5, 27))
# => datetime.datetime(2030, 5, 27, 0, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>)

このあたりは IANA のタイムゾーンデータベースや pytz のバージョンが更新されると変わる可能性があるようです。私がこの挙動を確認したときの pytz のバージョンは 2018.4 です。

このあたりの詳細に興味のある方は次のページ等をご覧になってみてください。


LMT 9:19 の意味や日本標準時について知りたい方には次のページ等が参考になります。



余談終わり。

続いて今月と前月の最終日です。これらはシンプルなのでテスト無しで関数だけ書いておきます。

月の最終日を取得する

from delorean import Delorean

def get_last_day_of_month2(delorean):
    '''指定された日付の月の最終日を返す'''
    return delorean.truncate('month').next_month().last_day()

前月の最終日を取得する

from delorean import Delorean

def get_last_day_of_prev_month2(delorean):
    '''指定された日付の前月の最終日を返す'''
    return delorean.truncate('month').last_day()

Ruby の gem でよく見られるような直感的でわかりやすいインタフェースですね。

...

以上、月の初日や最終日を取得する方法についてでした。

このあたりは使うときにはよく使うパターンなので、必要なときにサッと書けるようにひきだしとして持っておくとよいかと思います。

参考

2018/06/19

Python Tips: Chrome のブックマークを Python で確認したい

Python で Google Chrome のブックマークを確認する方法をご紹介します。

この記事を書いている時点では、私の Mac では Chrome のブックマークのデータは次の場所の Bookmarks というファイルに格納されています。
/Users/{ユーザ名}/Library/Application Support/Google/Chrome/Default/Bookmarks
Chrome のバージョンは 67.0.3396.87 です。

拡張子は付いていませんが中身は JSON テキストなので、そのまま普通の JSON ファイルとして取り扱うことができます。

データを読み込む


import json
import getpass

# `getpass.getuser()` でカレントユーザの名前を取得する
CHROME_BOOKMARK_PATH = (
    '/Users/{username}/Library/Application Support/'
    'Google/Chrome/Default/Bookmarks'
).format(username=getpass.getuser())


def get_chrome_bookmark_data() -> dict:
    '''Get the json of user's Chrome bookmark.'''
    with open(CHROME_BOOKMARK_PATH) as f:
        return json.load(f)


# JSON 内のデータを取得する
bookmark_data = get_chrome_bookmark_data()

print(type(bookmark_data))
# => <class 'dict'>

# ルートには 3 つの要素が入っている
print(bookmark_data.keys())
# => dict_keys(['checksum', 'roots', 'version'])

# checksum と version はメタ情報なので使わない
print(bookmark_data['checksum'])
# => 2f4a0ccbaba3f63a811870efbeff5dbb
print(bookmark_data['version'])
# => 1

# 実際のブックマークデータは `roots` の下に分かれて入っている
print(bookmark_data['roots'].keys())
# => dict_keys(['bookmark_bar', 'other', 'sync_transaction_version', 'synced'])

# 試しにブックマークバーのデータを表示する
bookmark_bar = bookmark_data['roots']['bookmark_bar']
print(bookmark_bar.keys())
print(bookmark_bar['name'])
for entry in bookmark_bar['children']:
    if entry['type'] == 'folder':
        print('{type}: {name}'.format(**entry))
    else:
        print('{type}: {name} - {url}'.format(**entry))
# =>
# url: ブックマーク1 - http://example1.com
# url: ブックマーク2 - http://example2.com
# folder: フォルダA
# folder: フォルダB
# folder: フォルダC
# ...

ルートにある辞書の下に roots というキーがあり、その下に bookmark_bar / other 等のグループに分かれて実際のブックマークデータが格納されています。 bookmark_bar は名前のとおりそのままブックマークバーのことで、 other はその他のブックマークを表すようです(公式のドキュメントが無いので、中身を見て判断しています)。

ブックマークの要素は type というキーを必ず持ち、これが folderurl のどちらかの値を持ちます。 folderurl のどちらかによって存在するその他のキーは異なります。私が見たかぎりそれぞれが持つキーは次のとおりになっていました。

folder:
  • id ブックマーク ID
  • type タイプ( folder
  • name ブックマークの名前
  • children 含まれるブックマークのリスト
  • date_added 作成日時を表す独自のタイムスタンプ?
  • date_modified 更新日時を表す独自のタイムスタンプ?

url:
  • id ブックマーク ID
  • type タイプ( url
  • name ブックマークの名前
  • url URL
  • date_added 作成日時を表す独自のタイムスタンプ?

id / type / name / date_added の 4 つについては共通しているようです。

私自身は大量のブックマークを手作業で確認したくなかったので、この方法で確認しました。同じようにブックマークをデータとして確認したい方の参考になれば幸いです。

CSV その他の形式に出力するもう少し本格的なスクリプトを GitHub に置いたので、興味のある方はよろしければ参考にしてください。


注意点として、間違って Bookmarks ファイルを上書きしてしまうと大変なことになる可能性があるので、参考にする際は 1) バックアップを取ってから触る、 2) 書き込みモードでは絶対にファイルを開かない、等の対策をするようにしてください。

2018/06/06

Stack Overflow Developer Survey 2018 の Python 関連データまとめ



開発者向け Q&A サイトの Stack Overflow が 2011 年以降毎年開発者向けの調査を行いその結果を公表しています。 公式のコメントによると、 2018 年の調査ではなんと 10 万人を越える開発者が調査に協力したそうです。


こういう調査の結果は、驚きの情報が得られるわけではありませんが、世間のトレンドや自分の立ち位置を確認するのによいですよね。

今回は、この 2018 年の調査結果データのうち Python 開発者に関係する部分を抽出してグラフ化してみました。周りに Python 開発者があまりいない方等にとってはとてもおもしろい調査結果なのではないかと思います。興味のある方はぜひご覧ください :)

  • 集計方法
  • 注意点
  • 調査結果グラフ
  • 終わりに

集計方法


Stack Overflow が公開している結果データ csv のうち、次の質問の選択肢で Python にチェックが入った人の回答のみを集計しました。

Which of the following programming, scripting, and markup languages have you done extensive development work in over the past year, and which do you want to work in over the next year?

意訳:

過去 1 年に主に使用した言語と、先 1 年に使用したい言語はどれですか?

数としては、 csv ファイルに含まれる回答全 98,855 件のうちおよそ 30% の 30,359 件が該当しました。

グラフ作成において、回答の選択肢が多い質問では、すべての回答をグラフにすると細かい部分が見づらくなるので、比率の少ない選択は「 others 」という項目に集約しています。

注意点


グラフを見る上での注意点をいくつかあげます。

  • 2018 年の調査結果( csv )が使用されています。
  • この調査の回答が Pythonista の母集団をどれだけ代表しているかは不明なので、この結果を Pythonista 全体にそのまま一般化することはできません(とはいえ、ヒントにはなるかと思います)。
  • Python ユーザと他の言語のユーザの比較は行っていないため、 Python だからこの結果なのか Stack Overflow の調査だからこの結果だからなのかは不明です。
  • グラフ化しているのは調査結果のうちごく一部です。
  • 単一選択の質問は円グラフ、複数選択可の質問は棒グラフでそれぞれ表しています。

では順に見ていきましょう。

調査結果グラフ


DevType (開発者のタイプ)



Which of the following describe you? Please select all that apply.

「開発者としての属性を教えてください」という質問です(複数選択可)。

バックエンド開発者が 58% 、フルスタック開発者が 45% と、回答者の大半はバックエンドの開発を行っている開発者です。学生が 22% 、デザイナー 11% と、本職の開発者以外の人の割合が高いのは Stack Overflow のサービスが Q&A サイトだからでしょうか。

データサイエンティストとマシンラーニングスペシャリストが多いのは Python ならではな気がします。ただ、 Python Software Foundation が 2017 年に行った別の調査ではデータ解析やマシンラーニングをやっている人の割合がこれよりもずっと多かったので、この調査の回答者は「ウェブ開発者が多めに答えている」と考えるのがよさそうです( Stack Overflow のサービスの性質上、それは当然な気もします)。

FormalEducation (最終学歴)



Which of the following best describes the highest level of formal education that you’ve completed?

「最終学歴を教えてください」という質問です(単一選択)。

学部( Bachelor )卒が 43% 、修士( Master )卒が 24% とのことです。修士の割合を他の似た言語と比較すると、 JavaScript は 21% 、 PHP は 18% でした。 Python でよく行われるマシンラーニングを原理を理解して行うには最低でも学部レベルの数学は必要ですし、 Python の利用者に修士が多いのは Python らしいと言えるかもしれません。

ちなみに、国内に関して言うと、文部科学省の資料によると 2012 年時点で学部生 256 万人に対して大学院生は 26 万人だそうです。ざっくり平均で学部生は 4 年、大学院は 3 年在籍すると考えると、学部卒と院卒の割合は学部卒 10 人に対して院卒 1.0〜1.6 人程度になるでしょうか。アメリカやヨーロッパの院卒比率が一般に日本よりも高いことを考慮しても、この調査の回答者は修士卒比率が非常に高いことがわかります(というわけで、この回答者群は全 Pythonista の母集団をよく代表していないと言えます(笑))。


UndergradMajor (学生時代の専攻)



You previously indicated that you went to a college or university. Which of the following best describes your main field of study (aka 'major')

「専攻を教えてください」という質問です(単一選択)。

コンピュータ・サイエンスやソフトウェア・エンジニアリングといった「ソフトウェア関連」が専攻の人たちが最も多く全体の 62% を占めます。 3 位の情報システム・情報技術とあわせると 68% にもなります。 Pythonista にかぎらない全体の回答でも傾向はほぼ同じですが、 Pythonista の場合はその他の工学系・生物学・物理学・数学等の専攻の割合が回答全体よりも少し高いのが特徴です。これは、 Python が広い分野で「プログラミングの非専門家」にもよく使われているという事実を裏付けているといえるでしょう。

世界的には(日本以外では)、最低でも工学系の学士を持っていることが開発者になる最低条件となっている国が多く、文系でも開発者になれる日本の状況は世界ではむしろ例外とも聞きます。この結果からはそのあたりのところも読み取れます。

個人的には、日本の、努力次第で誰にでも職業プログラマへの道が開かれている(=学歴軽視な)状況は素晴らしいと思う反面、基礎知識や適正が無い人もかんたんにプログラマになれてしまって(=実戦投入されてしまって)ろくなキャリアが築けなくても自己責任なのは社会全体で見て非効率でよくないなぁと思います。とはいえ、弁護士や博士のように、政府が無理やり開発者を増やしてもうまく行かなさそうなので、なかなか難しいですね。

Age (年齢)



What is your age? If you prefer not to answer, you may leave this question blank.

「何歳ですか」という質問です(単一選択)。

25 - 34 歳が最も多くほぼ半数の 47% 、続いて 18 - 24 歳が 28% となっています。

Stack Overflow のような Q&A サイトは熟練者よりも経験の少ない人の方が比較的よく使うと思うので、これはそれを反映しているものと思います。とはいえ、 18 - 34 歳が大多数を占めるというこのデータを見ると、高齢国日本と世界の大きなギャップを感じます。

LanguageWorkedWith (使用言語)



Which of the following programming, scripting, and markup languages have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the language and want to continue to do so, please check both boxes in that row.)

「過去 1 年に使用した主な言語は何ですか」という質問です(複数選択可)。

この質問で Python にチェックが入った回答のみを使っているので Python 使用率は当然 100% です。続く言語は次のとおりとなっています。

  • JavaScript 69%
  • HTML 68%
  • CSS 65%
  • SQL 58%
  • Bash/Shell 57%
  • Java 50%
  • C++ 35%
  • C 33%
  • PHP 31%
  • C# 29%

JavaScript / HTML / CSS / SQL / Shell あたりはウェブ開発で欠かせないので、このあたりの使用率が高いのは当然といえば当然かもしれません。個人的には、 C の 33% が意外に高くて驚きです。約 10% が R や Matlab を併用しているのは、データサイエンスその他の科学でよく使われる Python ならではのような気がします。

LanguageDesireNextYear (希望言語)



Which of the following programming, scripting, and markup languages have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the language and want to continue to do so, please check both boxes in that row.)

「次の 1 年に使いたい言語は何ですか」という質問です(複数選択可)。

上位はおおむね上の「使用言語」と同様の顔ぶれです。「使用言語」との対比でいうと、 Go / Kotlin / Rust あたりが「現在使っていないけれど新たに使いたい言語」として人気なようです。逆に人気が無い言語は PHP あたりでしょうか。

DatabaseWorkedWith (使用データベース)



Which of the following database environments have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the database and want to continue to do so, please check both boxes in that row.)

「過去 1 年に使用した主なデータベースは何ですか」という質問です(複数選択可)。

MySQL / PostgreSQL あたりが一番人気なのは統計を取らなくてもなんとなくわかりますね。個人的には、 SQL Server / MongoDB / Elasticsearch あたりが意外と高くて驚きです。世間ではよく使われているんですねぇ。

DatabaseDesireNextYear (希望データベース)



Which of the following database environments have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the database and want to continue to do so, please check both boxes in that row.)

「次の 1 年に使いたいデータベースは何ですか」という質問です(複数選択可)。

上位は「使用データベース」とあまり変わらないのでコメントがしづらい感じです。クラウドのデータベースについては一様に新たに使ってみたいと考えている人が多いようですね。

PlatformWorkedWith (使用プラットフォーム)



Which of the following platforms have you done extensive development work for over the past year? (If you both developed for the platform and want to continue to do so, please check both boxes in that row.)

「過去 1 年に使用した主なプラットフォームは何ですか」という質問です(複数選択可)。

作業マシンに加えてウェブサーバとして使うマシンの OS も含まれているようで、 Linux が Windows を上回っています。また、 AWS が 26% と、クラウドの中では AWS が抜群によく使われているようです。

WordPress が選択肢に含まれていることが少し不思議な感じがしますが、 WordPress は 13% と非常に高くなっています。これは、 WordPress が iOS 等に並ぶほどよく使われているということでしょうか。 CMS の中では一人勝ちに近い WordPress 恐るべしです。

PlatformDesireNextYear (希望プラットフォーム)



Which of the following platforms have you done extensive development work for over the past year? (If you both developed for the platform and want to continue to do so, please check both boxes in that row.)

「次の 1 年に使いたいプラットフォームは何ですか」という質問です(複数選択可)。

「使用プラットフォーム」との比較で言うと、 Windows の人気が低いのが目立ちます。一方、人気が高いのは Raspberry Pi 、 Serverless 、 Amazon Echo あたりです。 Raspberry Pi の人気が高いのは「 Python だから」というのもありそうですが、日本国内に比べて海外の Raspberry Pi の人気が高そうなことが伺えます。

FrameworkWorkedWith (使用フレームワーク)



Which of the following libraries, frameworks, and tools have you done extensive development work in over the past year, and which do you want to work in over the next year?

「過去 1 年に使用した主なフレームワークは何ですか」という質問です(複数選択可)。

Python を使っている開発者であれば Django 使用率が最も高いのかと思いきや、 JavaScript ・ Node 関連のツールが Django と同等かそれ以上の人気のようです。回答者全体の割合でも Node や Angular ・ React の割合が非常に高いので、 JavaScript は今や開発者の必修言語と言ってもよいかもしれません。

ちなみに、 Vue.js や Flask は回答の選択肢に入っていないためこれらの使用状況はわかりません。

FrameworkDesireNextYear (希望フレームワーク)



Which of the following libraries, frameworks, and tools have you done extensive development work in over the past year, and which do you want to work in over the next year?

「次の 1 年に使いたいフレームワークは何ですか」という質問です(複数選択可)。

「使用フレームワーク」との比較で言うと、マシンラーニング用の TensorFlow の人気がとにかく高いことがわかります。 Torch/Pytorch についても 10% の人が「使いたい」と考えており、マシンラーニング用のツールが全般的に注目を集めているようです。

IDE (統合開発環境)



Which development environment(s) do you use regularly? Please check all that apply.

「開発環境として何を使っていますか」という質問です(複数選択可)。

回答者全体の Vim の使用率は 25.8% なので、 Vim の使用率が高いのは Python 開発者ならではです。また、 VS Code や Visual Studio よりも Sublime Text の使用率が高いのも Python 開発者ならではと言えそうです。

個人的には、(私は Notepad++ を使わないので) Notepad++ が Sublime Text や VS Code に並ぶぐらい多く使われていることに驚きです。本格的な IDE のカテゴリでは JetBrains 社の IDE が非常に強いこともわかります。 IPython / Jupyter が 17% も使われているのも Python ならではですね。

OperatingSystem ( OS )



What is the primary operating system in which you work?

「メインで使う OS は何ですか」という質問です(単一選択)。

Windows / Linux / その他がそれぞれ 3 分の 1 ずつとなっています。 Linux が結構よく使われているんですね。 Vagrant 等の影響でしょうか。

CommunicationTools (コミュニケーションツール)



Which of the following tools do you use to communicate, coordinate, or share knowledge with your coworkers? Please select all that apply.

「一緒に働く人たちとのコミュニケーションツールとして何を使っていますか」という質問です(複数選択可)。

個人的には、 Slack と Office が多いのは納得ですが、 Jira や Confluence が多いのは驚きです。私の印象では日本ではあまり使われていないのですが、海外では(なのか日本でもそうなのかわかりませんが)結構使われているのですね。

また、チャットといえば Skype が Google Hangouts と同じくらいは使われているのではないかと思っていましたが、回答の選択肢にそもそも無かったので、開発者にはあまり使われていないのかもしれませんね。

NumberMonitors (モニタの数)



How many monitors are set up at your workstation?

「モニタは何枚使っていますか」という質問です(単一選択)。

51% という約半数の人たちが 2 枚使っています。 1 枚・ 2 枚以外の選択肢は 3 枚以上です。回答者全体の割合もこれとほぼ同じなので、使えるモニタが 1 枚しか無い職場で働いている人は「海外も含めて世界の 7 割以上の開発者は自分よりも恵まれた環境でコーディングしているんだ」と考えてよいでしょう。

モニタのサイズ等にもよるので一概には言えませんが、個人的にはモニタは 3 枚ぐらいまでなら増やせば増やすほど生産性が上がるので、モニタの追加は費用対効果がとてもよい設備投資だと思います。逆に、モニタ 1 枚だけで開発をするのはシューズを片方しか履かずにマラソンを走るようなものなので、当然のようにモニタ 1 枚しか用意しないような会社には開発者は関わってはいけません(個人の見解です)。

Methodology (開発手法)



Which of the following methodologies do you have experience working in?

「どのような開発手法の経験がありますか」という質問です(複数選択可)。

アジャイル( Agile )とスクラム( Scrum )がそれぞれ約半数で、カンバン( Kanban )とペアプログラミング( Pair programming )が 4 分の 1 程度でそれに続きます。日本はアジャイル手法の広まりが遅れていると言われることがありますが、それを実感するような結果ですね。「約半数がアジャイルやスクラムの経験がある」と言われると、日本とは完全に別世界の話に思えます。

とはいえ、回答全体で見ると、アジャイル 85.4% 、 Scrum 62.7% と Python 開発者に限定した使用率よりも高いので、(私の集計が間違っていなければ) Python 開発者はアジャイル手法をあまり使っていないと言えそうです。

このあたりの国内外ギャップにはさまざまな原因があるかと思いますが、「日本では請負が主流で、欧米は(にかぎらず海外全般も?)インハウスの開発が主流」と聞くので、そのあたりの商習慣や業界構造が大きく影響していそうです。また、日本ではおそらくコンピュータ・サイエンスやソフトウェア工学を専門的に学んだ人の割合が少なく、各手法を正しく理解して実践できる人が単純に不足しているというのも要因のひとつではないかと思います。

VersionControl (バージョン管理)



What version control systems do you use regularly? Please select all that apply.

「ふだんバージョン管理システムとして何を使っていますか」という質問です(複数選択可)。

Git が 88% とほぼ 9 割です。全体の回答でもほぼ同じ割合なので、これは Python 開発者にかぎりません。 Git も今や開発者の必須スキルと言えるでしょう。

以上です。

終わりに


今回は私が興味のあるところに絞って Python 限定でデータを抽出してみましたが、他の項目や、全体の傾向、他の言語の傾向を見てみるのもおもしろいと思います。興味がある方はオリジナルの方もご覧になってみてください。


例えば、 「 Most Loved, Dreaded, and Wanted ... 」(最も好きな・嫌いな・やりたい○○は何ですか?)という質問 はとてもおもしろいです。 dreaded なものには「確かに!」と声に出して言いたくなります。

ちなみに、 csv のデータはカラム数が非常に多く行数も約 10 万行あるので Excel 等でそのまま集計するのはつらいかもしれません。

また、今回集計とグラフ作成に使用した Jupyter notebook を GitHub に置きました。興味のある方はぜひ自由に見たり再利用したりしてみてください。


ちなみに使用したライブラリは次のとおりです。

  • jupyter
  • matplotlib
  • numpy
  • pandas

参考


Stack Overflow の他に Python Software Foundation と JetBrains が行った Python 公式の調査もあります。 Pythonista の傾向等に興味のある方はそちらもご覧になるとよいかもしれません。

2018/05/29

yield の使い方

Python のキーワード yield の使い方について説明します。


目次


  • yield とは
  • yield でジェネレータを作る
  • yield でコンテキストマネージャを作る
  • yield from でジェネレータを入れ子にする
  • その他の使い方


yield とは


yield は英語で「生み出す」「生む」「起こす」といった意味の単語とのことで、 Python における「 yield 」は、 コードを構成する構成要素(「キーワード」)のひとつで、 yield 式を作るためのもの です。

def get_abc():
    yield 'a'
    yield 'b'
    yield 'c'

print(list(get_abc()))  # => ['a', 'b', 'c']
print(list(get_abc()))  # => ['a', 'b', 'c']

yield 」は、関数(あるいはメソッド)の中にのみ記述することができるもので、 yield が記述された関数をジェネレータ関数化するものです。

ジェネレータ関数 」とは、関数の中でも少し特殊な関数で、通常の関数が 1 つの値を return で返すのに対して、ジェネレータ関数はジェネレータオブジェクトを返します。

ジェネレータオブジェクト 」(もしくは「ジェネレータイテレータ」(ジェネレータイテレータは『ゼニヤッタ・モンダッタ』みたいで語感がいいですね))とは、ジェネレータによって生成されたイテレータオブジェクトです。

イテレータオブジェクト 」とは、 for ループや組み込みの next() 関数に渡せば値を 1 つずつ返してくれるオブジェクトのことです(厳密には、 __iter__() __next__() の 2 つのメソッドを持っていて、 __iter__() メソッドの戻り値が self (自分自身)のオブジェクトです。参考: Iterator Types )。

for item in イテレータ:
    print(item)

まとめます。

  • yield キーワード: 関数の中で yield 式として使われ、関数をジェネレータ関数にするもの。
  • ジェネレータ関数: 「ジェネレータオブジェクト」を返すもの。
  • ジェネレータオブジェクト: ジェネレータ関数の呼び出しによって生成されたイテレータ。


少し余談ですが、このあたりの用語に関してややこしいと私が思うのは、 Python のプログラムの中では「ジェネレータオブジェクト」に「 generator (ジェネレータ)」という表記がされていて、その一方で、 Python.org の公式ドキュメントでは「ジェネレータ関数」のことを「ジェネレータ」と呼んでいるところです。

ジェネレータオブジェクトのことを「ジェネレータ」と呼ぶのであればそれを生成する関数の方は「ジェネレータ関数」、ジェネレータ関数のことを「ジェネレータ」と呼ぶのであればそれが生成するものは「ジェネレータオブジェクト」と呼ぶ、というように、人と話をするときにはどちらかに統一してから話をしないと混乱しそうです。

この節の最後に、公式のドキュメントからジェネレータの説明に関する部分を抜粋してご紹介します。

Generators are a special class of functions that simplify the task of writing iterators. Regular functions compute a value and return it, but generators return an iterator that returns a stream of values.

翻訳: ジェネレータ(訳注: このジェネレータは「ジェネレータ関数」の意味です)はイテレータをシンプルに書けるようにしてくれる特殊な関数です。 通常の関数はある値を計算して返しますが、ジェネレータは値のストリームを返すイテレータを返します

When you call a generator function, it doesn’t return a single value; instead it returns a generator object that supports the iterator protocol.

翻訳: ジェネレータ関数を呼ぶと、単一の値を返す代わりに、イテレータプロトコルをサポートするジェネレータオブジェクトを返します(訳注: これは上の「値のストリームを返すイテレータ」と同じ意味です)。

The big difference between yield and a return statement is that on reaching a yield the generator’s state of execution is suspended and local variables are preserved. On the next call to the generator’s __next__() method, the function will resume executing.

翻訳: yieldreturn 文の大きな違いは、 yield の場合は処理が yield の行に到達したときにジェネレータの実行状態が一時停止されてローカル変数が保持される点です。ジェネレータの __next__() メソッドが次回呼ばれたときにジェネレータ関数は実行を再開します。

参考

概念的な説明だけではわかりづらいので、つづいてサンプルコードを見ながら説明していきます。


yield でジェネレータを作る


yield 式は上述のとおり、関数の中で使われ、関数をジェネレータ関数に変える働きをします。

def gen_etos():
    ETOS = '子丑寅卯辰巳午未申酉戌亥'
    for eto in ETOS:
        yield eto

この関数を呼び出すと、「ジェネレータオブジェクト」(あるいは「ジェネレータイテレータ」)と呼ばれるオブジェクトが返されます。

g1 = gen_etos()

# ジェネレータ関数の戻り値はジェネレータオブジェクト
type(g1)
# => generator

# ジェネレータかどうかのチェックは types の GeneratorType でできる
import types
isinstance(g1, types.GeneratorType)
# => True

ジェネレータオブジェクトは、 for 文または next() 関数で回すことができます。

g1 = gen_etos()
for value in g1:
    print(value)
# =>
# 子
# 丑
# 寅
# 卯
# 辰
# 巳
# 午
# 未
# 申
# 酉
# 戌
# 亥

g1 = gen_etos()
try:
    while True:
        print(next(g1)) 
except StopIteration:
    pass
# =>
# 子
# 丑
# 寅
# 卯
# 辰
# 巳
# 午
# 未
# 申
# 酉
# 戌
# 亥

ジェネレータ関数は同時に複数のジェネレータオブジェクトを返すことができます。それら複数のジェネレータオブジェクトの内部状態は通常(あえて nonlocal 変数を共有するようなことをしなければ)互いに独立です。

g1 = gen_etos()
g2 = gen_etos()
 
for _ in range(3):
    print(next(g1))
# =>
# 子
# 丑
# 寅

# g2 の内部状態は g1 とは独立
for _ in range(2):
    print(next(g2))
# =>
# 子
# 丑

# g1 の内部状態は g2 とは独立
for _ in range(2):
    print(next(g1))
# =>
# 卯
# 辰

説明をシンプルにするために上の gen_etos() は引数を持たないようにしましたが、ジェネレータ関数も通常の関数と同様に引数を受け取ることができます。実用上は引数を渡すケースの方が多いかと思います。

def get_nonblank_lines(path):
    with open(path) as f:
        for line in f:
            if line.strip():
                yield line

g1 = get_nonblank_lines('sample.dat')
for line in g1:
    print(line, end='')

また上のコードではいずれもわかりやすくするためにジェネレータオブジェクトにあえて g1 という名前を付けましたが、実践ではジェネレータオブジェクトには名前を付けずに、次のように for ループの右側で直接ジェネレータ関数を呼び出す形が多いのではないかと思います。

for line in get_nonblank_lines('sample.dat'):
    print(line, end='')

混乱しやすいのですが、ジェネレータ関数そのものがイテレータなのではなく、ジェネレータ関数の戻り値がイテレータであるという点を覚えておく必要があります。

上の説明には「イテレータ」「 for 文」「 next() 関数」ということばが出てきました。これらの意味を押さえておかないと上の説明を完全に理解するのは難しいので、これらの意味がちょっと怪しいぞという方は一度これらのワードを検索するなり書籍にあたるなりしてからこの記事に戻ってきていただくと意味がスッキリとわかるのではないかと思います。

以上で yield 式とジェネレータの基本の説明は終わりです。続いて、 yield の少し応用的なお話に進みます。

yield でコンテキストマネージャを作る


コンテキストマネージャとは with 式に渡して使えるオブジェクトで、コードブロックに一時的なコンテキストを提供するものです。

with open('sample.log', 'a') as f:
    print('これはファイルに書き込まれます。', file=f)

例えば、組み込み関数の open()with 文に渡すとコンテキストマネージャとしてふるまうオブジェクトを返す関数です。

このコンテキストマネージャの仕組みそのものは yield とは独立のものですが、 yield で作るジェネレータ関数を使って、独自のコンテキストマネージャをかんたんに作ることができます。具体的には、コードブロックを挿入したい位置に yield 文を書いたジェネレータ関数を書き、それを @contextlib.contextmanager でデコレートします。

次のコードは print() の出力先を一時的に指定されたファイルに切り替えるコンテキストマネージャを yield を使って定義した例です。

from contextlib import contextmanager
import sys


@contextmanager
def switch_stdout(path):
    try:
        # print() の出力先を指定されたファイルに切り替える
        sys.stdout = open(path, 'a')
        # with のコードブロックの処理がここで実行される
        yield
    finally:
        # print() の出力先を標準出力に戻す
        # sys.__stdout__ はオリジナルの標準出力を格納したもの
        sys.stdout = sys.__stdout__


print('これは標準出力に出ます。')
with switch_stdout('/tmp/sample.log'):
    print('これは標準出力には出ません。')
    print('これも標準出力には出ません。')
print('これも標準出力に出ます。')

このコードを実行すると、 with のコードブロック内の 2 つの print() 関数の文字列は /tmp/sample.log に出力されます。一方、 with のコードブロックの外(=前後)にある print() 関数の文字列は通常どおり標準出力に出力されます。

ちなみに、この switch_stdout() と同等の機能を持つ @contextlib.redirect_stdout が標準ライブラリにすでに用意されています。


上の例のコンテキストマネージャでは yield 式に値を渡していませんが、 yield に値を渡すこともできます。コンテキストマネージャとなるジェネレータ関数の yield に渡された値は、利用時に with 文の as で受け取ることができます。

例えば、次のような中身の bookmarks テーブルを持つ SQLite のデータベースファイルがあるものとします。

url,title 
"https://www.google.co.jp","Google"
"https://www.instagram.com","Instagram"

$ sqlite3 db.sqlite3 >> EOS
CREATE TABLE bookmarks (url text, title text);
INSERT INTO bookmarks VALUES ("https://www.google.co.jp", "Google");
INSERT INTO bookmarks VALUES ("https://www.instagram.com", "Instagram");
EOS

このデータベースのアクセスするためのコンテキストマネージャとして次のようなものを作ることができます。ここで yield に渡されたコネクションオブジェクトは withas で取得・利用することが可能です。

from contextlib import contextmanager
import sqlite3


@contextmanager
def sqlite_conn(path):
    try:
        conn = sqlite3.connect(path)
        # コネクションオブジェクトをコンテキストに渡す
        yield conn
    finally:
        if 'conn' in locals():
            conn.close()

# コネクションオブジェクトを as で受け取る
with sqlite_conn('db.sqlite3') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM bookmarks')
    for row in cursor:
        print(row)

このように、通常のジェネレータ関数の yield とコンテキストマネージャの yield は意味合い・使い方が大きく異なるので注意が必要です。ジェネレータ関数の yield 式は「何らかの値を受け取ること」や「繰り返し呼ばれること」が前提で使われてますが、コンテキストマネージャの yield 式は値を返すとはかぎらず、原則一度しか呼ばれません。

他のスクリプト言語に馴染みのある方には、 Python のコンテキストマネージャは、 Ruby であれば「ブロックを受け取るメソッド」、 JavaScript であれば「無名のコールバック関数」に近いものと考えるとわかりやすいかと思います。


yield from でジェネレータを入れ子にする


Python 3.3 以降では yield from 文で、ジェネレータ関数の入れ子がかんたんに実現できるようになっています。

次のジェネレータ関数 gen_numbers_all() は、ジェネレータ関数 gen_numbers_1()gen_numbers_2() のジェネレータオブジェクトをそのまま結合したようなふるまいをします。

def gen_numbers_1():
    for n in 'AKQJ':
        yield n

def gen_numbers_2():
    for n in '98765432':
        yield n

def gen_numbers_all():
    # 他のジェネレータ関数で作られたジェネレータオブジェクトが 
    # yield した値をそのまま yield する
    yield from gen_numbers_1()
    yield from gen_numbers_2()

print(list(gen_numbers_1()))
# =>
# ['A', 'K', 'Q', 'J']

print(list(gen_numbers_2()))
# =>
# ['9', '8', '7', '6', '5', '4', '3', '2']

print(list(gen_numbers_all()))
# =>
# ['A', 'K', 'Q', 'J', '9', '8', '7', '6', '5', '4', '3', '2']

この yield from 式については、ジェネレータオブジェクトの内部状態を変更する send() メソッド等を使った使い方等もう少し複雑な使い方の例も Python.org の公式ドキュメントでは紹介されています。 yield from についてもっと詳しく知りたい方は公式のドキュメントにも目を通してみてください。



その他の使い方


上で見た使い方の他に、 asyncio で利用するコルーチンを定義するためにも yield 式は利用することができます。ただ、この使い方は用途が限られる&私は asyncio を使う必要に迫られず経験が不足しているので、ここで語るのはやめておきます。

気になる方は公式のページ等をご覧ください。


以上、 Python の yield の使い方についてでした。


参考


他サイト

ブログ内

2018/05/23

Python Tips: Python の例外システムを活用したい

Python 3 の例外システムを活用する上で押さえておきたいポイントをまとめてみます。

例外システムに関しては過去に「 Python の例外処理」という記事も書いています。この記事と内容が重複しますが、例外に興味のある方はよろしければそちらもご覧ください。


目次


本記事の目次です。

  • 基本形を押さえる
  • finally を押さえる
  • else を押さえる
  • 組み込みの例外クラスのツリーを押さえる
  • except のパターンを押さえる
  • 例外オブジェクトのアトリビュートを利用する
  • コンテキストマネージャを使う

順に見ていきましょう。

基本形を押さえる


Python 3 の例外処理の基本形は次のとおりです。

import sys

FILE_IN = 'sample.dat'

try:
    # 例外が起こる可能性のある処理
    file = open(FILE_IN)
except Exception:
    # 例外が起こったときの処理
    print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
    sys.exit(1)

キーワード tryexcept を組み合わせて使います。

他の言語を知る方だと try catch のパターンに馴染みのある方が多いでしょうか。 Python の場合は catch の代わりに except を使用します。

except 句は一般に次のいずれかのパターンで書きます。

except [キャッチしたい例外のクラス]:  
except [キャッチしたい例外のクラス] as [キャッチされた例外オブジェクトにつける名前]: 

finally を押さえる


tryexcept の後には finally という句を繋げることができます(厳密にいうと except がなくても大丈夫です)。

import sys

FILE_IN = 'sample.dat'

try:
    # 例外が起こる可能性のある処理
    file = open(FILE_IN)
    file.close()
except Exception:
    # 例外が起こったときの処理
    print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
    sys.exit(1)
finally:
    # 例外の発生有無にかかわらず最後に実行したい処理
    print('finally です。')
# =>
# ファイル sample.dat を開くことができません。
# finally です。

finally 句は例外が発生してもしなくても必ず実行されます。

finally はよくできていて、処理がどの経路を通っても必ず実行され、かつ、なるだけ遅いタイミングで実行されるようになっています。異なる経路のパターン a) - e) をあげて、どのタイミングで finally 句が実行されるのかを以下に見ていきましょう。

a) try の中で例外が発生しなかった場合 → try を抜けるときに実行される:
try:
    print('try です。')
finally:
    print('finally です。')
# =>
# try です。
# finally です。

b) try の中で例外が発生し、 except 句のひとつでキャッチされ、その中で例外があげられなかった場合 → except 句の後に実行される:
try:
    value = 1 / 0
except ZeroDivisionError:
    print('ZeroDivisionError です。')
finally: 
    print('finally です。')
# =>
# ZeroDivisionError です。
# finally です。

c) try の中で例外が発生し、 except 句のひとつでキャッチされ、その中で例外があげられた場合 → except 句内で例外があげられる直前で実行される:
try:
    value = 1 / 0
except ZeroDivisionError as e:
    print('ZeroDivisionError です。')
    raise e
finally: 
    print('finally です。')
# =>
# ZeroDivisionError です。
# finally です。
# Traceback (most recent call last):
#   ...
# ZeroDivisionError: division by zero

d) try の中で例外が発生し、どの except 句でもキャッチされなかった場合 → finally 句が実行された後に例外があがる(公式ドキュメントでは「 re-raised 」という表現が使われています):
try:
    value = 1 / 0
except StopIteration:
    print('StopIteration です。')
finally: 
    print('finally です。')
# =>
# finally です。
# Traceback (most recent call last):
#   ...
# ZeroDivisionError: division by zero

e) try 内で return 文や break 文が来て try 句を抜ける場合 → returnbreak の後に実行される:
def sample_exception():
    try:
        print('try です。')
        return True
    except:
        return False
    finally:
        print('finally です。')

result = sample_exception()
print(result)
# =>
# try です。
# finally です。
# True

このように finally 句はどんな場合でも必ず実行されるので、 try 句の中の処理の成否によらずに必ず実行したい処理――例えばデータベース等の外部リソースの開放等をするのに有用です。

else を押さえる


try except 句の後には else という句を繋げることができます。 else 句は try 句の中で 例外が起こらなかったときにのみ 実行されます。

import sys

FILE_IN = 'sample.dat'

try:
    # 例外が起こる可能性のある処理
    file = open(FILE_IN)
    file.close()
except Exception:
    # 例外が起こったときの処理
    print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
    sys.exit(1)
else:
    # 例外が起こらなかったときの処理
    print('file.closed: {}'.format(file.closed))
# =>
# file.closed: True

例外が起こらなかったときにのみ実行されると聞くと、「 else 句の中身を try の中に書けば else 句は要らないんじゃないか」と思えますが(私は思いました)、 elsetry の中身を最小限に留め思わぬ例外処理が起こってしまうのを防ぐのに有効です。 else 句が便利なパターンとして例えば次のような使い方が考えられます。

try:
    # データベースに変更を加える処理
except:
    # データベースの処理が失敗したのでトランザクションをロールバックする
else:
    # 処理が成功した場合にログを残す

個人的には、この「 try の処理が成功した場合に実行される句」に else という名前を使うのは違和感があります。 then 等の名前の方が直感的でわかりやすい気がしますが、このあたりは「予約語をなるべく少なくする」ことを優先した判断の結果なのですかね。

組み込みの例外クラスのツリーを押さえる


Python 3 には組み込みの例外クラスが多数用意されています。子クラスの例外は親クラスの except 句でキャッチできるので、適切な粒度で例外をキャッチして正しく例外処理を行うために、このツリー構造を押さえて必要があります。

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

# ZeroDivisionError は親クラスの ArithmeticError でキャッチできる
try:
    value = 1 / 0
except ArithmeticError as e:
    print('例外が起こりました。')
    print('{}: {}'.format(type(e).__name__, str(e)))
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero

except のパターンを押さえる


try 句の後に書く except 句はさまざまなパターンで記述することができます。

ひとつの except 句で複数の例外をキャッチする:
try:
    value = 1 / 0
except (OverflowError, ZeroDivisionError) as e:
    print('例外が起こりました。')
    print('{}: {}'.format(type(e).__name__, str(e)))
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero

複数の except 句をひとつの try 句につける:
try:
    value = 1 / 0
except OverflowError as e:
    print('OverflowError です。')
except ZeroDivisionError as e:
    print('ZeroDivisionError です。')
except Exception as e:
    print('Exception です。')
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero

except 句を複数連ねるときの注意点は、例外の場合分けを適切にするためには「 小さい例外を前に、大きな例外を後に書かなくてはいけない 」ことです。

上のサンプルではこのとおりに「小さい例外を先、大きな例外を後」にして書いていますが、これを変えて Exceptionexcept 句を先頭に持ってくると、そこですべての例外がキャッチされてしまうので OverflowErrorZeroDivisionErrorexcept 句には到達することがありません。これでは複数の except 句を書いた意味がなくなるので、 except 句は必ず、小さい例外(例外クラスの継承ツリーで枝葉の方にある例外)から先に書く必要があります。

例外オブジェクトのアトリビュートを利用する


例外もひとつのオブジェクトなので、自由にアトリビュートを付けることができます。

例えば、 Requests ライブラリ の例外には、リクエストオブジェクトを格納した request という名前のアトリビュートが付けられています。利用者は except 句の中でそのアトリビュートに自由にアクセスすることができます。

import requests


try:
    requests.get('http://www.yahoo.co.jp', timeout=0.001)
except requests.exceptions.ConnectTimeout as e:
    # ライブラリ側で用意してくれている e.requst アトリビュートが利用できる
    print('URL "{}" へのリクエストがタイムアウトしました。'.format(e.request.url))

規模の大きめのライブラリでは、独自の例外クラスを用意していて、アトリビュートにさまざまな情報を提供していることがあります。ライブラリの独自の例外クラスを取り扱う場合にはどんなアトリビュートがあるのかをチェックしておくとよいでしょう。

コンテキストマネージャを使う


with 文で利用できる Python のコンテキストマネージャは、特定のコードブロックに対して「コンテキスト」を提供できる仕組みです。さまざまな利用方法がありますが、代表的な使い方のひとつに「特定の例外処理パターンを再利用しやすくする」というものがあります。

サンプルとして、 CSV ファイルを読み込んで各行を返すリーダーオブジェクトを提供するコンテキストマネージャの例を見てみましょう。

from contextlib import contextmanager
import csv


@contextmanager
def read_csv(path):
    '''CSV ファイルの読み込みを行うコンテキストマネージャ'''
    try:
        f = open(path)
        reader = csv.reader(f)
        yield reader
    finally:
        if 'f' in locals():
            f.close()


# コンテキストマネージャを使って CSV ファイルを読み込む
with read_csv('sample1.csv') as reader:
    print([row[0] for row in reader])
# 出力例:
# => 
# ['アレックス', 'マーティ', 'メルマン', 'グロリア']

with read_csv('sample2.csv') as reader:
    print([row[0] for row in reader])
# 出力例:
# => 
# ['ポー', 'タイガー', 'ヘビ', 'カマキリ', 'ツル', 'モンキー']

read_csv() を定義することで、 tryfinally を使った例外処理のパターンをシンプルな with 文で再利用できるようになります。

コードの中でよく似た例外処理のパターンが繰り返し出てきた場合は、共通の例外処理パターンをコンテキストマネージャ化することによって、無用なコードの重複を減らすことができます。

...

以上、 Python の例外システムを活用する上で押さえておきたいポイント集でした。

以上のことがひととおりきちんと押さえられれば、 Python の例外処理については「正しく使えている」と自信を持ってよいのではないでしょうか。


参考

2018/05/15

Python にまつわるアイデア: Python のパッケージとモジュールの違い

Python の「パッケージ」と「モジュール」の違いについて説明してみます。

本題に入る前に数点お断りです。

  • この記事は長文です。
  • 記事作成時の Python の最新バージョンは Python 3.6 です。 Python 3.6 の頃の認識にもとづいて書かれています。
  • この記事はある程度調査・確認をした上で書いていますが、私は Python の仕様や Python そのものの開発のプロではありません。あくまでも Python のいちユーザの認識であり間違っている可能性があります(とはいえ、なるべく正確に書こうというモチベーションで書いているので、詳しい方で間違いに気づいた方はご指摘いただけますと幸いです)。

Python の「パッケージ」と「モジュール」の違い


Python のパッケージとモジュールの概念は少し複雑なので、ひとことでかんたんに説明することができません。

次の 2 通りの方法をするのがよいのかなと思います。

  • a) 正確ではないがシンプルな説明
  • b) シンプルではないが(わりと)正確な説明

2 つの説明を持ち出す理由は、数学で「円周率 = 3.14 」と考えることに似ています。

円周率は本来無理数なので「円周率 = 3.14 」という説明は厳密には間違いですが、 3.14 としておいても実用上は問題のない状況がほとんどです。また、より厳密な定義を理解するには無理数等の概念を先に理解するなどの準備が必要です。そのため、円周率については次の 2 通りの説明ができます。

  • e-a) 円周率は 3.14 である
  • e-b) 円周率は 3.14159265358979... と続く無理数である

今回 2 通りの説明をする理由はこれと同じで、 a) の説明は「正確ではないがほとんどの場合はその理解で問題ない」、 b) の説明は「わかりづらいがより正確」、といった違いがあります。

順番に説明していきます。

a) 正確ではないがシンプルな説明

先に、正確ではないがシンプルな説明をしてみます。

Python におけるモジュールとパッケージの説明はそれぞれ次のとおりです。

  • モジュール = ファイル。拡張子が .py (あるいは .pyc 等)の Python ファイルのこと。
  • パッケージ = ディレクトリ__init__.py というファイルを格納したディレクトリのこと。

次のように import 文で読み込むことができる点はパッケージもモジュールも共通です。

sample_module.py:
print('これはモジュールです。')

sample_package/__init__.py:
print('これはパッケージです。')

$ python -c 'import sample_module'
これはモジュールです。
$ python -c 'import sample_package'
これはパッケージです。

この a) の説明において、パッケージとモジュールの違いは「 モジュールは単一のファイルで、パッケージはディレクトリである 」ということになります。

パッケージはディレクトリなので、他のモジュール(=ファイル)やパッケージ(=ディレクトリ)を中に格納することができます。一方のモジュールはファイルなので、他のモジュールを格納することができません。

$ # パッケージは他のパッケージを格納することができる
$ tree .
└── songohan
    ├── __init__.py
    └── songoku
        ├── __init__.py
        ├── songoten.py
        └── songohan.py
# パッケージの中のパッケージを import することができる
import songohan.songoku
import songohan.songoku.songoten
import songohan.songoku.songohan

a) の説明は以上です。 b) の説明に入る前に、 a) について少し補足をします。

a) の補足: __init__.py を中に持たないディレクトリの場合

Python 3.3 以降では __init__.py ファイルを中に持たないディレクトリも Python パッケージとして認識されるようになりました。

$ # trunks.py は `import vegeta.trunks` でインポートできる
$ tree .    
└── vegeta
    └── trunks.py

__init__.py の無いディレクトリは、パッケージはパッケージだけど少し特殊な「 ネームスペースパッケージ 」(名前空間パッケージ)として扱われます。

ネームスペースパッケージは、異なるパスに置かれた複数のモジュールやパッケージを共通の親パッケージでまとめることができるものです 。例えば、次のように 2 つの異なる場所に chuoku というディレクトリがあった場合、それぞれの親ディレクトリをモジュールの検索パスに追加すれば、これらを共通のネームスペースパッケージとして利用できるようになります。

$ tree /tmp/tokyo/ /tmp/osaka/osaka/
/tmp/tokyo/
└── chuoku
    └── sample1.py
/tmp/osaka/osaka/
└── chuoku
    └── sample2.py

2 directories, 2 files

このことは、次の Python コードで検証することができます。

import sys

# モジュール検索パスに 2 つの chuoku ディレクトリの親ディレクトリを追加する
sys.path.append('/tmp/tokyo')
sys.path.append('/tmp/osaka/osaka')

# ネームスペースパッケージ `chuoku` の下にあるモジュール `sample1` `sample2` を 
# どちらも問題なく import することができる
import chuoku.sample1
import chuoku.sample2

逆に、上の chuoku ディレクトリのうちどちらか一方でも __init__.py ファイルを中に持っていれば、それはネームスペースパッケージではなく通常のパッケージとなります。例えば、 /tmp/tokyo/chuoku の方に __init__.py があればこれは通常のパッケージとなるので、もう一方の /tmp/osaka/osaka/chuoku はパッケージとして読み込めなくなってしまいます。逆もまた然りです。

ちなみにですが、ネームスペースパッケージではない通常のパッケージをネームスペースパッケージと区別するために、 Python の公式ドキュメントでは「レギュラーパッケージ」あるいは「トラディショナルパッケージ」ということばで通常のパッケージを呼んでいます。

この __init__.py を持たないディレクトリが自動的にネームスペースパッケージになると何がうれしいのかと言うと、 規模の大きなパッケージのサブパッケージを別々のディストリビューションパッケージとして配布するのがやりやすくなります

もうひとつちなみに、ここで「ディストリビューションパッケージ」というのは配布用にまとめられた単位のパッケージのことを指していて、通常は pip install コマンドの引数として指定するものです。

具体例をあげると、 Django REST framework というライブラリは、 pip でインストールするときには djangorestframework 、 Python コード内で利用するときは rest_framework という名前でそれぞれ参照・指定します。

pip コマンド:
$ pip install djangorestframework

Python コード内:
from django.contrib.auth.models import User
from rest_framework import serializers

class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('url', 'username', 'email', 'is_staff')

Django REST framework の場合、パッケージ名が rest_framework で、ディストリビューションパッケージ名が djangorestframework です。

さらにちなみに、この「 __init__.py が無いディレクトリが自動的にネームスペースパッケージになる」という仕組みが Python 3.3 で導入される前は、ネームスペースパッケージの仕組みそのものは存在していたのですが、ネームスペースパッケージ化するには __init__.py を用意してその中に次のようなコードを書く必要があり、これがなかなか面倒臭かったようです。

__path__ = __import__('pkgutil').extend_path(__path__, __name__)
__import__('pkg_resources').declare_namespace(__name__)

このあたりが Python 3.3 で簡単化されて「 __init__.py が無いディレクトリは自動的にネームスペースパッケージになる」という仕組みが導入されました。該当する PEP は PEP 420: Implicit Namespace Packages なので、細部に興味のある方は PEP 420 のページを読んでみてください(私は読んでもよくわかりませんが・・・)。


a) の補足は以上です。続いて b) の説明を見ていきましょう。

b) シンプルではないが(わりと)正確な説明

b) における Python のモジュールとパッケージの説明は次のとおりです。

  • モジュール = import 文でインポートすることができるもの
  • パッケージ = モジュールのうち他のモジュールをサブモジュールとして格納したもの

もう少し長めに言い換えます。

  • つまり、 すべてのパッケージはモジュールである 。数学的な感じで書けば「 Gp ⊂ Gm 」。
  • パッケージとモジュールの共通点は「 import 文でインポートできる 」こと。
  • パッケージとモジュールの違いは「 パッケージには __path__ アトリビュートがありそれを通じてサブモジュールを提供している一方で、モジュールには __path__ が無くサブモジュールを提供していない 」こと。

つまり、パッケージとモジュールは基本的には同じもので、異なるのは「パッケージには __path__ アトリビュートがありそれを通じてサブモジュール(この「サブモジュール」には「サブパッケージ」を含む)を提供していて、モジュールは __path__ アトリビュートが無くサブモジュールを提供していない点」だけです。

この「 __path__ というアトリビュートが重要な仕事をしている」という点は、次のような検証用コードで確認することができます。

2 つのパターンを試してみましょう。ひとつめは「パッケージとして動作する Python ファイル(≒モジュール)」のパターンです。

weird_module.py:
import os

__path__ = (os.path.dirname(__file__), )

weird_module.py ファイルがあるディレクトリに移動して、 Python インタラクティブシェルを起動して次のコードを試してみます。

# どの import 文も問題なく実行できる
import weird_module
import weird_module.weird_module
import weird_module.weird_module.weird_module

weird_module.py は単一の Python ファイルですが、 __path__ アトリビュートを持っており、その値のタプルの要素のひとつとして自分自身の親ディレクトリのパスを持っています。結果として、 weird_module は実体は単一のファイルでありながら、自分自身をサブモジュールとして再帰的に提供できるパッケージとして動作します(あくまでも検証用なので、実用性はありません)。

もう 1 つのパターンを見てみましょう。これは先ほどとは逆で、「 __init__.py ファイルを格納したディレクトリで、本来パッケージとして扱われるはずなのに、中にある .py ファイルをサブモジュールとして提供しないパッケージ」のパターンです。

次のファイル構造を用意しましょう。
$ tree weird_package
weird_package
├── __init__.py
└── child.py

0 directories, 2 files

続いて、 weird_package ディレクトリの下の __init__.py ファイルに次の 1 行のコードを書き込みます。

weird_package/__init__.py:
del __path__

準備ができたら、 weird_package の親ディレクトリに移動し、 Python インタラクティブシェルを起動して次のコードを試してみます。
# weird_package 自体は問題なく import できる
import weird_package

# weird_package/child.py は import できない
import weird_package.child
# =>
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# ModuleNotFoundError: No module named 'weird_package.child'; 'weird_package' is not a package

weird_package のサブモジュールであるはずの child が import できません。 weird_package ディレクトリは __init__.py ファイルを持っているにもかかわらず、 __path__ が適切に定義されていないので、サブモジュール child を提供していません。

この検証用サンプルでは他にもいろんなことを確認することができますが、これ以上踏み込むことはよしておきます。

b) の説明は以上です。続いて、 a) b) 2 つの説明を比較して、ポイントを見ていきましょう。

a) b) 2 つの説明を比較して

繰り返しになりますが、 b) の説明を読むと a) の「モジュール = ファイル、パッケージ = ディレクトリ」という説明は一部正しいところがあるものの厳密には正しくないということがわかります。

ただ、 a) の説明が間違いかというとそんなことはなくて、実用上は a) の形で理解していても何ら問題がないことがほとんどではないかと思います。

ちなみに、(私はこのあたりに詳しくはないのですが) Python のインポートシステムはデフォルトで次の 3 つのインポート用クラスを提供しています。

  • BuiltinImporter: 組み込みモジュールをインポートするためのもの。
  • FrozenImporter: コンパイルされた frozen モジュールをインポートするためのもの。
  • PathFinder: モジュール検索パス上にあるモジュールをインポートするためのもの。

a) の説明は、このうちの PathFinder の挙動の一部を説明したものです。 a) の説明は PathFinder 以外のインポート用クラスにはあてはまりませんし、さらに PathFinder の場合でも __path__ に手が加えられた特殊なモジュールにはあてはまりません。

他方の b) の説明は、 PathFinder を実際の挙動も含めてより厳密に説明したものです。ただ、こちらもあくまでも PathFinder の説明であり、 BuiltinImporterFrozenImporter にはそのままあてはまりません。

(私の経験上)標準ライブラリ以外のモジュールを利用する場合、そのほとんどは PathFinder を利用する形になるので、実用上は b) のところまで押さえられていれば十二分ですが、 b) は他のインポートシステムのパターンはカバーできていないため、より厳密に言うならこれも不正確ということになるでしょう。

さらに、 Python のこのインポートシステム自体拡張することができるようなので、開発者が独自に「パッケージとモジュール」の形を決めて実装することもできるようです。独自に拡張されたインポートシステムのことも含めて考えると、 Python の「パッケージとモジュール」の定義・違いをわかりやすく説明するのはさらに困難になります。

まとめ


最後に、ここまでの内容をかんたんにまとめてみます。おおよそ次のような感じになるでしょうか。

  • シンプルに Python を利用する場合だけなら、 a) の理解で十分
  • 規模の大きなパッケージを開発・管理するような人は、 b) までは理解しておいた方がよさそう
  • Python そのものの開発に関わりたいような人にとっては、 b) の理解でも(おそらく)不十分

世の中のほとんどの人にとって「 円周率 = 3.14 」以上の説明が必要ないのと同じように(本人が知らなくてもその恩恵を受けているという点については無視するとして)、この「 Python のパッケージとモジュールの定義・違い」についても b) 以上の理解を必要とする人はどちらかというと少数派でしょう。

ですので、多くの Python ユーザにとっては、「 厳密に言うなら a) は間違いだけど、ふだんは a) で覚えておいて差し支えない 」程度に認識しておいて、 b) 以上の理解が必要になったときにはその都度調べて確認できるようにしておく、ぐらいがちょうどよいのではないかと思います(もちろん「とにかく使えさえしたら原理はどうだっていい」という人の場合は、理解する必要はありません)。

ちなみに最後にもうひとつ余談ですが、今回 Wikipedia で「円周率」を引いて驚いたのですが、 Wikipedia によると円周率は 2016 年現在で 22 兆桁(!)まで計算されているそうです。


以上、「 Python のパッケージとモジュールの違い」についての説明でした。「わかりやすかったよー」「役に立ったよー」「間違っているよー」という方はコメント等でお知らせいただけるとうれしいです。

以下、参考ページです。いずれも英語のページですが、 Python のパッケージとモジュールについての理解を深めたい方には一読の価値があります。


参考

2018/04/30

Python Tips: JSON を整形して表示したい

今回は JSON 形式の文字列を Python で整形して表示する方法をご紹介します。

今回は次の 2 つのパターンを取り上げてみます。

  • シンタックスハイライトなし
  • シンタックスハイライトあり

早速見ていきましょう。

シンタックスハイライトなし


シンタックスハイライトが特に要らない場合は、標準ライブラリの json だけを使えば OK です。

JSON が Python コード内で文字列として取得できている場合

JSON 形式の文字列が Python コードの中で取得できている場合は、いったん loads() で読み込んだ後に dumps() で再度ダンプし直すとよいでしょう。その際に dumps() のオプション引数 indent を指定することでインデントの大きさを決めることができます。

import json

JSON_SAMPLE = '{"_meta": {"hash": {"sha256": "hash"}, "pipfile-spec": 6, "requires": {"python_version": "3.6"}, "sources": [{"name": "pypi", "url": "https://pypi.python.org/simple", "verify_ssl": true } ] } }'
 
data = json.loads(JSON_SAMPLE)  
print(json.dumps(data, indent=2))

このコードの出力結果は次のとおりになります。

{
  "_meta": {
    "hash": {
      "sha256": "hash"
    },
    "pipfile-spec": 6,
    "requires": {
      "python_version": "3.6"
    },
    "sources": [
      {
        "name": "pypi",
        "url": "https://pypi.python.org/simple",
        "verify_ssl": true
      }
    ]
  }
}

JSON がファイルに格納されている場合

JSON 文字列がファイルに格納されている場合は、上の loads() の部分を load() に変えて、さらに、引数を文字列からファイルオブジェクトに変更すれば OK です。

また、 Python プログラムの中ではなくターミナル上で処理できればそれで十分な場合は、 json.tool モジュールを使う方法がお手軽でおすすめです。 json.tool は次の形で利用することができます。

$ python -m json.tool < data.json
{
  "_meta": {
    "hash": {
      "sha256": "hash"
    },
    "pipfile-spec": 6,
    "requires": {
      "python_version": "3.6"
    },
    "sources": [
      {
        "name": "pypi",
        "url": "https://pypi.python.org/simple",
        "verify_ssl": true
      }
    ]
  }
}

python -m json.tool に JSON 文字列を渡す方法には、上の「標準入力で渡す方法」の他に「ファイル名を引数として渡す方法」もあります。

$ python -m json.tool data.json

出力結果はどちらも同じです。

ちなみに、 python -m json.tool のヘルプは次のとおりになっています。

python -m json.tool -h
usage: python -m json.tool [-h] [--sort-keys] [infile] [outfile]

A simple command line interface for json module to validate and pretty-print JSON objects.

positional arguments:
  infile       a JSON file to be validated or pretty-printed
  outfile      write the output of infile to outfile

optional arguments:
  -h, --help   show this help message and exit
  --sort-keys  sort the output of dictionaries alphabetically by key

私は使ったことはありませんが、 dict 型のキーをアルファベット順(辞書順)に並べられる --sort-keys というオプションがあるようです。

標準ライブラリの json を使った方法については以上です。続いて シンタックスハイライトありの方法を見ていきましょう。

シンタックスハイライトあり

シンタックスハイライトを施して JSON を表示する機能は(私が知るかぎり) Python の標準ライブラリには無いので、何らかの非標準のライブラリを使用するか自分で書くかのどちらかになります。おすすめなのは pygments というライブラリを使用する方法です。

シンタックスハイライトなしの場合と同様に JSON が Python コード内で文字列として取得できている場合から見ていきましょう。

JSON が Python コード内で文字列として取得できている場合
pygments.highlight() 関数にコードの文字列を渡すと、シンタックスハイライトを施した形で文字列を返してくれるので、それを利用します。

import json

from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter

JSON_SAMPLE = '{"_meta": {"hash": {"sha256": "hash"}, "pipfile-spec": 6, "requires": {"python_version": "3.6"}, "sources": [{"name": "pypi", "url": "https://pypi.python.org/simple", "verify_ssl": true } ] } }'

data = json.loads(JSON_SAMPLE)
formatted_data = json.dumps(data, indent=2)
print(highlight(formatted_data, JsonLexer(), TerminalFormatter()))

このコードの出力は次のとおりとなります。ターミナル上ではきれいにハイライトが行われて表示されます。

{
  "_meta": {
    "hash": {
      "sha256": "hash"
    },
    "pipfile-spec": 6,
    "requires": {
      "python_version": "3.6"
    },
    "sources": [
      {
        "name": "pypi",
        "url": "https://pypi.python.org/simple",
        "verify_ssl": true
      }
    ]
  }
}

出力をブラウザで利用したい場合等は、 TerminalFormatter の代わりに HtmlFormatter を利用することが利用できます。

import json

from pygments import highlight
from pygments.lexers import JsonLexer
from pygments.formatters import TerminalFormatter

JSON_SAMPLE = '{"_meta": {"hash": {"sha256": "hash"}, "pipfile-spec": 6, "requires": {"python_version": "3.6"}, "sources": [{"name": "pypi", "url": "https://pypi.python.org/simple", "verify_ssl": true } ] } }'

data = json.loads(JSON_SAMPLE)
formatted_data = json.dumps(data, indent=2)
print(highlight(formatted_data, JsonLexer(), TerminalFormatter()))

JSON がファイルに格納されている場合
JSON がファイルに格納されている場合は、もしターミナル上で扱えればよいだけであれば、 pygments が提供するコマンドラインツール pygmentize を使う方法がシンプルでおすすめです。

改行やインデントを保ったままハイライトだけができればよいのであれば、 pygmentize コマンドをそのまま使用すれば OK です。

$ pygmentize data.json
{"_meta": {"hash": {"sha256": "hash"}, "pipfile-spec": 6, "requires": {"python_version": "3.6"}, "sources": [{"name": "pypi", "url": "https://pypi.python.org/simple", "verify_ssl": true } ] } }

ファイルの拡張子が json の場合は自動で JSON と認識してくれるようです。拡張子が json 以外の場合は -l (--lexer) オプションでフォーマットが JSON であることを伝えれば OK です。

$ pygmentize -l json Pipfile.lock

改行やインデントをよきように調整してなおかつハイライトしてほしい場合は、標準ライブラリ json と組み合わせて使うとよいかと思います。 pygmentize コマンドも、対象のファイルが指定されなければ代わりに標準入力の文字列を処理してくれます。

$ python -m json.tool json_data.json | pygmentize -l json 
{
    "_meta": {
        "hash": {
            "sha256": "hash"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.6"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.python.org/simple",
                "verify_ssl": true
            }
        ]
    }
}

ヘルプドキュメントの分量が多いのでここには掲載しませんが、 pygmentize には豊富なオプションがあるので、使ってみたい方は一度確認してから使ってみることをおすすめします。

$ pygmentize --help

以上です。

参考

2018/04/02

Python Tips:組み込みの名前を上書きしてしまったのを元に戻したい

Python で組み込み関数等の名前を上書きしてしまったときに元に戻す方法をご紹介します。

Python には、モジュールを import しなくても利用できる組み込みの関数があります。
例えば、 Python 3.6 の場合だと、その数は 60 以上にもなります。

abs() all() any() ascii() bin() bool() bytearray() bytes() callable() chr() classmethod() compile() complex() delattr()
dict() dir() divmod() enumerate() eval() exec() filter() float() format() frozenset() getattr() globals() hasattr() hash()
help() hex() id() input() int() isinstance() issubclass() iter() len() list() locals() map() max() memoryview()
min() next() object() oct() open() ord() pow() print() property() range() repr() reversed() round() set()
setattr() slice() sorted() staticmethod() str() sum() super() tuple() type() vars() zip() __import__()


これらの関数の名前を誤って上書きしてしまうと、元々あった関数の機能を使うことができません。多くの場合は変数名を変えればそれで済むのですが、 REPL や Jupyter を使っていると、 Python プロセスを起動し直す手間が大きく、稀に困ることがあります。

私の場合は、 idmapmax 等のシンプルな名前を変数名として使いたくなって、うっかり上書きしてしまうことがよくあります(他の言語のコードと Python コードを並行で触っているときによくやります)。

max = 10
max(10, 20, 5)
# => TypeError: 'int' object is not callable

結論としては、キーワード del を使って変数を削除すれば OK です。 del した後は元々の名前をまたそのまま利用できるようになります。

max = 10

del max

max(10, 20, 5)
# => 20

組み込みの名前は上書きしないように気をつけることが第一ですが、万が一上書きしてしまったときにはこういう方法があるということを覚えておくと便利です。

参考


2018/03/16

お知らせ:「 Life with Python Books 」を公開しました

サブドメイン books に Life with Python Books という名前でサイトを公開しました。


サイトは、日本語の Python 関連書籍の情報を集めたシンプルなサイトです。

一昔前 Python 本といえば、ひとりですべての本を把握できるぐらいの数だったような気がしますが、近年は Python 需要・ Python 人気の高まりを受けてか Python 関連書籍が多く出版されるようになりました。

最近の状況を見ると、 2017 年は Python 関連書籍が毎月少なくとも 1 冊はコンスタントに出版され、年間通してざっと 40 冊以上出版されたようです。そして 2018 年は 1 〜 3 月の 3 ヶ月間に私が把握しているだけでも 15 冊以上出版される(された)ので、この調子が続くと 2018 年は年間 60 冊以上、去年の 1.5 倍となる見込みです。ここ数年の勢いはすごいものがありますが、海外の Python の人気ぶりなども見るとこの流れは今後少なくとも数年は続きそうな感じがします。

(お断り: ここで「 Python 関連書籍」とは、タイトルに「 Python 」が入っているものだけでなく、何らかの形で Python に関係している「広い意味での Python 関連本」を指しています)

どんどん出版される Python 関連書籍について情報を一箇所にまとめたサイトを作りたいと思い今回作ってみました。

既存の EC サイトや出版社サイトに対する私の印象ですが、 EC サイトは Python 本だけを自分のニーズに合った切り口で検索するということがやりづらく、出版社のサイトは当然その出版社の書籍の情報しか見れず不十分、という印象です。いずれも「 Python 縛りで本を探したい」ときにいまいち使い勝手がよくないイメージでした。

このサイトを使って、私と同じように Python 本に興味がある方が、よりかんたん・スムーズに書籍を探せるようになればいいなと思います。

ただ、サイトの状態としては、まだ、 Python 関連書籍がようやく一通り登録でき(日本で出版されている主要な Python 本は 90% 以上網羅できているはずです)、各書籍の目次がひとまず揃った、というぐらいの状態です。改善の余地はまだまだあるので、タイトルの末尾に「 β 」と付けています。最初から完璧を求めるといつまでも公開できずにそのままお蔵入りになる可能性があったので、 β 版レベルで公開してしまおう、ということになりました。今後継続的に少しずつ改善をはかっていくつもりです。

Python 本に興味のある方はよろしければ使ってみてください。


永遠に β 版のままになる可能性もなきにしもあらず・・・ですが、今の時点では継続的に改善していきたいと思っているので、ご要望やお気づきの点がありましたらコメントや次のフォーム等でお気軽にご連絡ください :)

2018/03/06

Python Tips:関数全体をコンテキストマネージャでかんたんにラップしたい

Python には with 構文で使える「コンテキストマネージャ」という種類のオブジェクトがあります。

with open('log.txt', 'w') as f:
    f.write('これはログです。')

この open() の戻り値の f がコンテキストマネージャです。

このコンテキストマネージャを使って関数をまるごとラップする方法を今回はご紹介してみたいと思います。

結論は「デコレータを使う」です。以下に具体例をあげてご説明していきます。

例として、 Fabric3 ( Python 3 に対応した Fabric のフォーク版)を使った、次のようなケースを考えてみましょう。

fabfile.py:

from fabric.api import cd, run, task

PROJECT_DIR = '/path/to/the/project/root'


@task
def git_status():
    '''`git status` を実行する'''
    with cd(PROJECT_DIR):
        run('git status')


@task
def git_pull():
    '''`git pull` を `master` ブランチに対して実行する'''
    with cd(PROJECT_DIR):
        run('git pull origin master')

ここでは git_status()git_pull() という 2 つの Fabric タスクが定義されています。いずれのタスクも、 PROJECT_DIR で表されたプロジェクトのルートディレクトリで処理を実行する前提となっています。そのために、 fabric.api.cd() を使って with cd(PROJECT_DIR) でコンテキストを作ってその中で実際の処理を実行する形になっています。

この with cd(PROJECT_DIR) の部分は共通です。タスクが 2 つの場合はそれほど気になりませんが、同じ with を使うタスクが増えてくるとこれを共通化したくなることでしょう。後々タスクが増えてきたときのためにコンテキストマネージャを共通化することを考えてみましょう。

結論としては上述のとおり、「関数全体をコンテキストマネージャでラップするデコレータを作る」形がよいかと思います。

実際にデコレータを導入してみた場合のコードは次のとおりとなります。

from functools import wraps
from fabric.api import cd, run, task

PROJECT_DIR = '/path/to/the/project/root'


def decorate_cd(directory):
    '''`fabric.api.cd()` で関数をラップするデコレータを生成する'''
    def decorator(func):
        '''関数をラップするデコレータ'''
        @wraps(func)
        def wrapper(*args, **kwargs):
            with cd(directory):
                func(*args, **kwargs)

        return wrapper

    return decorator


@task
@decorate_cd(PROJECT_DIR)
def git_status():
    '''`git status` を実行する'''
    run('git status')


@task
@decorate_cd(PROJECT_DIR)
def git_pull():
    '''`git pull` を `master` ブランチに対して実行する'''
    run('git pull origin master')

ポイントはもちろん、デコレータを生成する decorate_cd() 関数です。関数が 3 重に入れ子になっていてなんだかややこしそうに見えるので、外側の関数から順にご説明します。

decorate_cd() これは、ディレクトリのパスを受け取ってデコレータとなる関数を返す関数です。この関数自体はデコレータではなく、あくまでも「デコレータ関数を生成する関数」です。


decorator() decorate_cd() によって生成され、実際に関数に対してデコレート処理を行う関数です。関数に対するデコレータは「ただひとつの引数として関数を受け取って戻り値として関数を返す」ものである必要がありますが、この関数は func を引数として受け取り、 wrapper を戻りとして返しています。

wrapper() これは、実際に fabric.api.cd() のコンテキストマネージャで関数をラップする実際にラッパー関数です。

この構造で作られた decorate_cd() を使うことで、関数をかんたんにコンテキストマネージャでラップすることができるようになります。

デコレータで装飾された関数

@decorate_cd(PROJECT_DIR)
def git_status():
    '''`git status` を実行する'''
    run('git status')

は Python では

def git_status():
    '''`git status` を実行する'''
    run('git status')

git_status = decorate_cd(PROJECT_DIR)(git_status)

と同じ意味になるので、 git_status() 関数が無事に fabric.api.cd() でラップできていることになります。

うまく使えると便利かと思います。以上、関数全体をコンテキストマネージャでかんたんにラップする方法についてでした。


参考


2018/02/20

ライブラリ: Pipenv



Python のパッケージ管理用のライブラリ「 Pipenv 」をご紹介します。

$ pipenv


Pipenv とは


Pipenv (読み方は「ピップエンブ」)は Python のパッケージ(≒ライブラリ)をプロジェクト単位で管理するためのライブラリです。 Requests の作者として有名な Kenneth Reitz 氏 によるプロジェクトで、 2017 年にスタートしたようです。

pip や virtualenv を裏で組み合わせて、よりよい DX (Developer eXperience) を提供するライブラリです。 Pipenv を導入すれば、プロジェクトのパッケージ管理において pip や virtualenv のコマンドを打つ必要がなくなります。

公式のドキュメントでは次のような説明がされています。

Pipenv — the officially recommended Python packaging tool from Python.org, free (as in freedom).

Pipenv is a tool that aims to bring the best of all packaging worlds (bundler, composer, npm, cargo, yarn, etc.) to the Python world. Windows is a first–class citizen, in our world.

It automatically creates and manages a virtualenv for your projects, as well as adds/removes packages from your Pipfile as you install/uninstall packages. It also generates the ever–important Pipfile.lock, which is used to produce deterministic builds.

少し長いですが、以下、意訳です。

Pipenv — Python.org で公式に推奨されている Python パッケージングツール。フリー(「自由」という意味で)。

Pipenv はパッケージングの世界( bundler / composer / npm / cargo / yarn 等)のいいとこ取りをすることを目的としたツールです。 Windows は、 Pipenv の世界では第一級市民です。

Pipenv は、あなたのプロジェクトの virtualenv を自動的に作成・管理し、あなたがパッケージをインストールまたはアンインストールしたときに Pipfile へのパッケージの追加や削除も行います。まったく共通のビルドを行うために使われる、非常に重要な Pipfile.lock の生成も行います。

イメージとしては、公式の説明のとおり、他のプログラミング言語で定番となっている各種パッケージ管理ツールと近い使用感で使えるパッケージマネージャーです。 npmyarnbundlecomposer 等のコマンドに馴染みのある方であれば抵抗なくすぐに使い始められるのではないかと思います。

Pipenv の特徴のまとめ


Pipenv の主な特徴をかんたんにまとめると次のとおりです。

  • プロジェクト単位でパッケージのインストール・アンインストールが行える
  • プロジェクト単位のパッケージは、ユーザ毎に virtualenv をまとめたディレクトリの下にインストールされ一元管理される
  • インストールされたパッケージの情報は Pipenv ファイルと Pipenv.lock ファイルに記録される
  • Pipenv にはインストールを実行した人が指定したパッケージのバージョンが記録される
  • Pipenv.lock には Pipenv が実際にインストールをしたパッケージのバージョンが記録される
  • PipenvPipenv.lock を共有すると、同じバージョンのパッケージをインストールしたプロジェクト環境がかんたんに再現できる
  • 環境変数を定義した .env ファイルが存在する場合はその中身が virtualenv 上で読み込まれる

私は個人的には「 Python のプロジェクト毎のライブラリを npm や yarn 風に管理できる、 pip と virtualenv の統合ラッパー」といったイメージを持っています。

Pipenv の基本的な使い方


すべての使い方をここで網羅するのは難しいので、基本的な使い方のみをご紹介します。

その他の基本的な使い方は pipenv--help オプションで、より詳細の使い方は公式のドキュメントで確認ができるので、詳しく知りたい方は 公式のドキュメント 等をご参照ください。

Pipenv のインストール

まずはインストール方法から見ていきましょう。前提として、 Python 3 と pip がインストールされているものとします。

$ pip install pipenv

pipenv コマンドが使えるようになれば、インストールは成功です。

$ pipenv --version
pipenv, version 9.0.3

Pipenv によるプロジェクト単位のパッケージ環境( virtualenv )の作成

Pipenv はプロジェクト単位で利用するものなので、続いて、プロジェクトのルートディレクトリを作りましょう。

$ mkdir myproject
$ cd myproject/

プロジェクトの中で Pipenv を初期化します。

$ pipenv --python 3.6
Creating a virtualenv for this project…
Using /usr/local/bin/python3.6m to create virtualenv…
⠋Running virtualenv with interpreter /usr/local/bin/python3.6m
Using base prefix '/usr/local/Cellar/python3/3.6.4_2/Frameworks/Python.framework/Versions/3.6'
New python executable in /Users/hayato/.local/share/virtualenvs/myproject-abX3VXqy/bin/python3.6
Also creating executable in /Users/hayato/.local/share/virtualenvs/myproject-abX3VXqy/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location: /Users/hayato/.local/share/virtualenvs/myproject-abX3VXqy
Creating a Pipfile for this project…

補足ですが、このコマンドを実行したときの私の環境では、 Homebrew を使ってインストールした Python 3.6 が /usr/local/bin/python3.6m に、そこから作った virtualenv が /Users/hayato/.local/bin にいて、それらを使って Pipenv を実行しています。

上の出力からわかるとおり、最初に myproject の後に - を付けてランダムな文字列(?)をつなげた virtualenv ディレクトリの下に virtualenv が作成され、 setuptoolspipwheel の 3 つのライブラリがインストールされた virtualenv が作成されました。そして最後に Pipfile (読み方は「ピップファイル」)が作成されました。

ちなみに、このプロジェクトのルートディレクトリ myproject はデスクトップにあり、上の ~/.local/share/virtialenvs ディレクトリとは無関係の場所にいます。

上のコマンドが成功したら、カレントディレクトリに Pipfile ファイルが作られているはずです。

$ cat Pipfile

中身は次のとおりで、最小限のプロジェクト情報を格納したファイルとなっています(このあたりは Pipenv のバージョンが上がると変わる可能性があります。参考にされる際はご注意ください)。

[[source]]

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


[packages]



[dev-packages]



[requires]

python_version = "3.6"

virtualenv の場所はコマンドで確認したりすることができます。

pipenv --venv
/Users/hayato/.local/share/virtualenvs/myproject-abX3VXqy

パッケージのインストール

プロジェクトのパッケージ環境を格納する virtualenv が用意できたら、自由にパッケージをインストールできます。方法は pip の場合と同様です。

pipenv コマンドにサブコマンド install を指定してパッケージ名を指定します。

$ pipenv install Django
Installing Django…
Collecting Django
  Using cached Django-2.0.2-py3-none-any.whl
Collecting pytz (from Django)
  Using cached pytz-2018.3-py2.py3-none-any.whl
Installing collected packages: pytz, Django
Successfully installed Django-2.0.2 pytz-2018.3

Adding Django to Pipfile's [packages]…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Updated Pipfile.lock (374a8f)!

処理が無事に完了すると、プロジェクトのルートディレクトリにある Pipfile ファイルが更新され、同じ場所に Pipfile.lock ファイルが生成されます。

中身を確認してみましょう。 Pipfile ファイルの中の [packages] の下に django の行が追加されました。

[packages]

django = "*"

Pipfile.lock の方には、実行した環境のさまざまな情報と、実際にインストールされたパッケージの情報が記述されています。

同じ要領で他のパッケージも続けてインストールしていくとよいでしょう。

$ pipenv install requests
$ pipenv install mysqlclient
$ pipenv install uwsgi

もちろん、バージョンを指定してインストールすることも可能です。

$ pipenv install django==1.11

完全一致は == 、互換性維持は ~= といった感じで、バージョン指定の方法には pip と同じものが使えるようです(こちらもバージョンが上がると変わる可能性があるので、参考にされる際はご注意ください)。

実際のプロジェクトでは、 PipfilePipfile.lock を Git (等の VCS )の管理下に置くことで、複数人や複数の環境でコードを共用するときに同一のパッケージ環境をかんたんに共有できるようになります。

ここまで読んだときに、 Python 歴が長い方や pip に詳しい方であれば「あれ、 pip + requirements.txt と何がちがうんだろう・・・」とお思いのことかと思います。このあたりについては、説明したいのですが・・・すみません、私には正確な説明ができないので、「 pip + requirements.txt 」の課題について Kenneth Reitz 氏が語った次の記事を参照していただければと思います。


パッケージのアップデート

すでにインストールされたパッケージをアップデートしたい場合は、サブコマンド update を使用します。

$ pipenv update django

パッケージのアンインストール

インストールされたパッケージをアンインストールするには、サブコマンド uninstall を使用します。

$ pipenv uninstall django

Pipfile 入りのプロジェクトの依存関係のインストール

誰か他の人が作った、 Pipfile ファイルが含まれたプロジェクトにおいて、依存関係をインストールするには pipenv install コマンドを実行します。

$ pipenv install

virtualenv の有効化

Pipenv でインストールされたパッケージは virtualenv の中にいるので、そのままでは利用できません。利用するには virtualenv を有効化する必要があります。

Pipenv が作成した virtualenv を有効にするには pipenv shell コマンドを利用します。

$ pipenv shell
Spawning environment shell (/usr/local/bin/bash). Use 'exit' to leave.
Loading init files ........... done!
source /Users/hayato/.local/share/virtualenvs/myproject-abX3VXqy/bin/activate

無事に virtualenv が有効化できたら、 Python のパスが virtualenv のものになり、インストールしたパッケージが利用できることが確認できるでしょう。

(myproject-abX3VXqy) $ which python
/Users/hayato/.local/share/virtualenvs/myproject-abX3VXqy/bin/python

(myproject-abX3VXqy) $ pip freeze | grep requests
requests==2.18.4

(myproject-abX3VXqy) $ django-admin startproject projectX .

virtualenv を有効化しているうちはコマンドプロンプトの前に ( virtualenv 名) が付くので、 virtualenv が有効になっているかどうかはそこで確認することができます。

有効になった virtualenv を無効にして元に戻るには、 exit コマンドを使います。もちろん、 exit のショートカット、たとえば MacOS なら ctrl + d を使うこともできます。

(myproject-abX3VXqy) $ exit
$

virtualenv を有効にしてコマンドを実行

特定のコマンドを実行するときだけ virtualenv を利用したい場合はサブコマンド run を使います。

$ # Pipenv で管理されている virtualenv を利用して `django-admin` コマンドを実行する
$ pipenv run django-admin startproject projectX .

$ # Pipenv で管理されている virtualenv を利用して `manage.py` ファイルを実行する
$ pipenv run app/manage.py runserver

他の言語のパッケージマネージャではそれぞれ次のコマンドに相当するイメージです(間違えていたらすみません・・・)。

Bundler (Ruby):

$ bundle exec rails ...

Npm (Node):

$ ./node_modules/.bin/ava

Composer (PHP):

$ composer run phpunit ...

virtualenv の削除

Pipenv で作成した virtualenv が不要になったら、 virtualenv を削除しましょう。

削除は他のサブコマンドとは少しちがって、 pipenv コマンドに --rm オプションを渡して実行する形になっています。

$ pipenv --rm
Removing virtualenv (/Users/hayato/.local/share/virtualenvs/myproject-abX3VXqy)…

個人的には、 virtualenv は削除し忘れることがよくあるので、一元管理されているディレクトリをときどきチェックして不要になったものがないか確認するようにしています。

Pipenv でのオートコンプリートの有効化

Pipenv にはサブコマンドのオートコンプリート(自動補完)機能が付いています。

pipenv --completion を実行するとオートコンプリートを有効にするスクリプトが出力されるので、もしオートコンプリートを利用したいなら .bashrc などで用意するようにしておくとよいでしょう。

ちなみに、私の Bash で実行すると次のように出力されました。

$ pipenv --completion

_pipenv_completion() {
    local IFS=$'\t'
    COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
                   COMP_CWORD=$COMP_CWORD \
                   _PIPENV_COMPLETE=complete-bash $1 ) )
    return 0
}

complete -F _pipenv_completion -o default pipenv

以上です。

Pipenv には、今回は取り上げなかった機能として次のようなもの等もあります。

  • 既知の脆弱性のチェック( safety を利用)
  • Flake8 のスタイル違反のチェック
  • 使われていない可能性のあるパッケージのチェック

より詳しく知りたい方は公式ドキュメントをご覧になってみてください。

参考


日本語

こちらの記事はとてもわかりやすいです。

英語

いずれも Kenneth Reitz 氏本人の公式ドキュメント / 公式ページですが、これがいちばんわかりやすい気がします。

追記 2018/05/17


Python コミュニティのイベント PyCon 2018 で Kennth 氏本人が Pipenv についての発表をしていました。 Python のパッケージ管理の歴史を絡めてなぜ Pipenv を開発するに至ったのかの話や実演デモを行っています。とてもわかりやすくておすすめです。