2018/07/23

Python Tips:パッケージの開発版をインストールしたい

Python でパッケージの開発版をインストールする方法についてご紹介します。

開発版のインストールは、バグ報告やテスト等でパッケージに貢献したいときや、自分でパッケージを開発したいとき等に必要になってきます。

(尚この記事では「パッケージ」ということばは、 pip でインストールできる「ディストリビューションパッケージ」という意味で使っています)

  • Git リポジトリにあるパッケージをインストールしたい
  • ローカルにあるパッケージをインストールしたい

Git リポジトリにあるパッケージをインストールしたい


Git で管理されたパッケージをインストールしたいときは次の形で pip コマンドを使えば OK です。

$ pip install git+[リポジトリ URL]

例えば GitHub にリポジトリがある Requests の場合だと次のようになります。

$ # パターン A/B のどちらでも OK
$ # パターン A
$ pip install git+https://github.com/requests/requests.git
$ # パターン B
$ pip install git+git://github.com/requests/requests.git

バージョンやブランチを指定するには末尾に @ をつけてその後に指定します。

$ # ブランチ master
$ pip install git+https://github.com/requests/requests.git@master

$ # タグ v2.18.4
$ pip install git+https://github.com/requests/requests.git@v2.18.4

$ # コミットハッシュ 4ea09e49f7d518d365e7c6f7ff6ed9ca70d6ec2e
$ pip install git+https://github.com/requests/requests.git@4ea09e49f7d518d365e7c6f7ff6ed9ca70d6ec2e

コミットハッシュで指定する場合はハッシュ値を省略せずすべて渡す必要があるようです。

URL 等から pip がプロジェクト名(≒パッケージ名)を特定できない場合等は末尾に #egg=[プロジェクト名] を付ける必要があります。例えば、 GitHub のリポジトリの URL の末尾に .git を付けない場合は #egg=[プロジェクト名] をつけないとインストールができません。

$ # プロジェクト名を指定しない → 失敗
$ pip install -e git+https://github.com/requests/requests
Could not detect requirement name for 'git+https://github.com/requests/requests', please specify one with #egg=your_package_name

$ # プロジェクト名を指定する → 成功
$ pip install -e git+https://github.com/requests/requests#egg=requests

ちなみに pip は Git の他にも有名 VCS をひととおりサポートしており、 URL の前の git+ を他のものに変更すればその他の VCS で管理されたパッケージもインストールすることができます。

  • git+: Git
  • hg+: Mercurial
  • bzr+: Bazaar
  • svn+: Subversion

ただし各 VCS のリポジトリを利用するには対応するコマンド( githg 等)がその環境で利用できる必要があります。

参考:

ローカルにあるパッケージをインストールしたい


続いて、ローカルにあるパッケージをインストールする場合についてです。ローカルにファイル一式が置いてあるパッケージをインストールしたいときは次の形で pip コマンドを使えば OK です。

$ pip install [パッケージのパス]

ここの パッケージのパス には setup.py ファイルが含まれたディレクトリを指定します(あるいは、 sdist / wheel フォーマットのアーカイブファイルを指定する形も可能なようです)。

シンプルな hello という名前のパッケージを作ってインストールしてみた場合のイメージは次のとおりです。

$ tree hello/
hello/
├── hello.py
└── setup.py

0 directories, 2 files

$ cat hello/hello.py
def say_hello(name=None):
    msg = 'Hi, {}!'.format(name) if name else 'Hi!'
    print(msg)

$ cat hello/setup.py
import setuptools

setuptools.setup(
    name='hello',
    version='0.1',
    packages=setuptools.find_packages(),
)

$ pip install hello/
Processing ./hello
Building wheels for collected packages: hello
  Running setup.py bdist_wheel for hello ... done
  Stored in directory: [var ディレクトリのどこか]
Successfully built hello
Installing collected packages: hello
Successfully installed hello-0.1

おまけ


おまけ A: editable mode でのインストール

pip でのインストールには editable mode というモードが用意されています。これはパッケージ開発時に便利な機能で、コードが編集可能な状態でパッケージをインストールできるものです。

editable mode でインストールされたパッケージのコードに変更を加えると、再インストールをしなくてもそのまま実行環境に反映されます。

editable mode を有効にするには -e--editable )オプションを使用します。

$ pip install -e hello/

editable mode は、ローカルにファイルのあるパッケージだけでなく、通常のディストリビューションパッケージや Git リポジトリ上のパッケージの場合でも利用することができます。その際は pip install 実行時に出力されるパッケージのダウンロード先パスを確認してそこのファイルを編集すると OK です。

参考:

おまけ B: Pipenv でのインストール

Pipenv を使った場合でも上のパターンを利用することができます。

$ # Git リポジトリのパッケージを editable mode を有効にしてインストール
$ pipenv install -e git+https://github.com/requests/requests#egg=requests

$ # ローカルのパッケージをインストール
$ pipenv install hello/

$ # Pipfile には Git リポジトリや editable mode であること等がきちんと記録されている
$ cat Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
hello = {path = "./hello"}
requests = {editable = true, git = "https://github.com/requests/requests"}

[dev-packages]

[requires]
python_version = "3.6"

おまけ C: インストールし直す

インストールし直したいときは pip install-U--upgrade )オプションを指定すると OK です。

...

ということで、 Python パッケージの開発版をインストールする方法についてでした。このあたりはいろいろと洗練されておりすばらしいですね。

参考:

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 でよく見られるような直感的でわかりやすいインタフェースですね。

...

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

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

参考