2017/12/01

Python のタプルとリストの違い、タプルの使いどころ

今回は基本に立ち返って、 Python の組み込み型である「タプル」と「リスト」の違い、それと「タプルの使いどころ」について説明してみたいと思います。

「 Python タプル リスト 違い 」といったキーワードで Google 検索すると、多くのページで「リストとタプルは記法が異なります」「タプルはイミュータブルです」という説明だけがなされていて、それだけだとなぜタプルが用意されているのか、タプルはどんなときに便利なのかがわからないのではないかと思い、今回このテーマで書いてみようと思いました。

前提として、 Python におけるリストが何なのかというのは読者の方はご存知だという前提でお話ししていきます。

そもそもタプルとは何ぞやというところからまずは見ていきましょう。

タプルとは


Python における「タプル」とは、複数のデータを一直線に並べた「コレクション」タイプのデータ型です。

tuple1 = ('東京都', '神奈川県', '大阪府')
type(tuple1) == tuple  # => True

tuple1[0]  # => '東京都'
tuple1[1:]  # => ('東京都', '大阪府')

見た目上はリストのかっこ []() に変わっただけです。定義の仕方や要素へのアクセス方法もリストとよく似ています。

ただし、タプルはリストとは異なり、いったんオブジェクトを生成した後に変更ができないオブジェクトとなっています。「変更ができない」というのは、厳密には「オブジェクト id を変えずに、要素を追加・変更・削除することができない」という意味です。この性質のことを「イミュータブル」( immutable )と言ったりします。ちなみに、イミュータブルの逆(変更ができるという意味の用語)は「ミュータブル」( mutable )です。

タプルとリストの違い


では、タプルはリストと何が異なるのでしょうか。

上で述べたばかりですが、「リストは変更ができるが、タプルは変更ができない」、これが両者の違いです。上の用語を使うなら、「リストはミュータブルであり、タプルはイミュータブルである」といいます。


「イミュータブル」が意味することとしないこと


タプルを利用する場合には、「タプルはイミュータブルである」ということばの意味を正しく理解しておく必要があります。

「イミュータブル」というのはあくまでも「オブジェクト id を変えずに要素を追加・変更・削除をすることができないこと」のみを表します。逆に言うと、

  • a. タプルを参照している変数に再代入すること
  • b. タプルの中の要素がミュータブルな場合にその要素そのものを変更すること

は特に問題なくできるので、この部分を混同しないように注意が必要です。ことばでの説明だけだと意味がわかりづらいので、以下、サンプルを使ってご説明してみます。

a. タプルを参照している変数に再代入すること

次の例を見てください。

t1 = ('ハート', '7')
t1 = ('ダイヤ', '10')

タプルは変更不可ですが、このコードは問題なく動作します。変数がタプルを参照していても、そこに別のオブジェクトを再代入することは可能です。

これは、ミュータブルかイミュータブルかというポイントというよりもむしろ「 Python には定数(再代入ができないもの)がない」ことと関係していると言えるでしょう。

紛らわしいのは、 += 演算を利用した場合の挙動です。次のコードを見るとタプルが変更可能なように見えるかと思うのですが、いかがでしょうか。

t1 = ('みかん', 'りんご')
t1 += ('バナナ', 'パイナップル')
print(t1)
# => ('みかん', 'りんご', 'バナナ', 'パイナップル')

実はここにはトリックがあって、 += の行では既存のタプルが変更されているのではなく、新しいタプルが生成されています。
左辺の t1 に再代入されているのは新しく作られたタプルオブジェクトです。

同じ += の演算でも、リストの場合はここのところがまったく異なるので注意が必要です。リストの場合は、 += 演算を行っても元のオブジェクトが維持されます。

# タプルは += の結果新しいタプルが生成される
t2 = t1 = ('みかん', 'りんご')
print(id(t1))    # => 4464365064

t1 += ('バナナ', 'パイナップル')
print(id(t1))    # => 4461411048
print(t1 is t2)  # => False

# 一方、リストは += で元のリストが維持される
l2 = l1 = ['みかん', 'りんご']
print(id(l1))    # => 4464250312

l1 += ['バナナ', 'パイナップル']
print(id(l1))    # => 4464250312
print(l1 is l2)  # => True

続いて b の方を見ていきましょう。

b. タプルの中の要素がミュータブルな場合にその要素そのものを変更すること

次のサンプルを見てください(要素数が 1 のタプルを定義するときは () 内の末尾に , を入れる必要があります)。

# 第 1 要素に dict を持つタプルを定義する
t1 = ({'mark': 'ハート', 'number': '7'}, )

# dict を変更する
t1[0]['mark'] = 'スペード'

print(t1)
# => ({'mark': 'スペード', 'number': '7'}, )

この例では、 dict を要素に持つタプル t1 を定義した後に、その dict を変更しています。 dict 型はミュータブルなので、このコードは特に問題なく動作します。

再代入によって「タプルが持つ参照」そのものを変更することと、「タプルが参照するオブジェクトの中身」を変更することは別であって、タプルが許容していないのは前者の変更のみです。参照されるオブジェクトがミュータブルであれば当然そのオブジェクトの中身は更新できるので、このあたりを混同しないよう注意してください。


タプルの使いどころ


では、なぜ Python にはリストとタプルというよく似た 2 つのコレクション型が用意されているのでしょうか。
リストは他のプログラミング言語で「配列」と呼ばれるものに近く、多くのプログラマーにとってより直観的です。リストだけあれば十分じゃないかとも思えます。

このあたりの疑問点はタプルの具体的な使いどころを見るとすっきり理解できるので、タプルを使うべき場面を具体的に見てましょう。

タプルの使いどころ 1: 変更を許可しない変数を定義する

変更を許可したくない変数を定義したい場合にタプルは便利です。

API_KEYS = ('xxx', 'yyy', 'zzz')

例えば、上のコードのように、外部サービスの API キーを格納したタプルを用意すると、再代入さえしなければ、中身が変わっていないことが保証されているという前提の下でその変数を使いまわすことができます。

関数型プログラミングに見られるような「思わぬ変更が生まれる可能性を最小化してバグの発生源を減らす」という考え方でプログラムを組みたい場合なんかにもタプルは便利です。

タプルの使いどころ 2: dict のキーに使う

dict のキーには hashable なオブジェクトのみが利用できます。

リストは hashable ではないので、 dict のキーに使うことはできません。

例えば、 Python で地図アプリのようなものを考えるとします。この場合、処理の途中で、緯度経度の情報をキーに、その建物の名前を値に入れた辞書を作りたくことがあるかもしれませんが、リストは hashable ではないので緯度経度の情報を格納するためには使えません。しかし、タプルは hashable なので、 dict のキーとして使用することができます。

# リストとタプルが hashable かどうかを確認する
from collections import Hashable

print(isinstance([], Hashable))  # => False
print(isinstance((), Hashable))  # => True

# 緯度経度の情報をキーに、建物名を値に入れた dict を用意する
locations = {
    (35.676, 139.744): '国会議事堂',
    (34.669, 135.431): 'ホグワーツ城',
    (35.039, 135.729): '鹿苑寺金閣',
}

dict 的なデータ型のキーに文字列しか受け付けないような言語の場合は、このような数字のペアに特別な意味がある場合でもそれをキーとすることはできませんが、 Python は組み込み型のタプルを使うことで比較的かんたんにこのようなロジックを実現することができます。

タプルの使いどころ 3: パフォーマンスをよりよくする

パフォーマンスの観点で、タプルはリストよりもよいようです。

ただし、よっぽどパフォーマンスが重要となるシビアな場面で無いかぎり、「リストじゃだめだ!やっぱりタプルじゃないと!」となるようなケースは稀なのではないかと思います。パフォーマンスの観点でデータ型を気にするのであれば、「リストかタプルか」の選択ではなく、もっと他の選択肢を選んだ方がよいような気がします。

・・・こんなところでしょうか。

ポイントは「変更ができないという特徴を活かす」「リストではできないことをやる」といったところになるかと思います。

Python のタプルについて理解を深めたいという方の参考になれば幸いです。

最後に、タプルの挙動を確認するユニットテストのサンプルコードを載せておきます。興味のある方は python3 -m unittest などで走らせてみてください。

# coding: utf-8

import unittest

class TupleBasicTest(unittest.TestCase):
    '''タプルの基本的な使い方を確認する
    '''

    def test_creation(self):
        # タプルを生成する
        t1 = ('ギン', 'ギラ', 'ギン')
        self.assertIsInstance(t1, tuple)

        # 実践的な場面で使うことはまずないが空のタプルを生成する
        t2 = ()
        self.assertIsInstance(t2, tuple)

        # 要素数が 1 のタプルについては `,` が必須となるため注意が必要
        t3 = ('さりげなし', )
        self.assertIsInstance(t3, tuple)
        s1 = ('さりげなし')
        self.assertIsInstance(s1, str)

    def test_access(self):
        # タプルの要素にアクセスする
        t1 = ('炎', '天', '下')

        self.assertEqual(t1[0], '炎')
        self.assertEqual(t1[1], '天')

        self.assertEqual(t1[-1], '下')

        self.assertEqual(t1[0:2], ('炎', '天'))

    def test_conversion(self):
        l1 = ('岡山', 15)
        self.assertIsInstance(tuple(l1), tuple)

        t1 = ('香川', 18)
        self.assertEqual(list(t1), ['香川', 18])

    def test_methods(self):
        self.assertEqual((3, 4, 4, 5).count(4), 2)
        self.assertEqual((3, 4, 4, 5).index(5), 3)

    def test_in(self):
        self.assertTrue(3 in (3, 4, 4, 5))
        self.assertFalse(108 in (3, 4, 4, 5))


参考

3. Data model — Python 3.6.3 documentation
I'm able to use a mutable object as a dictionary key in python. Is this not disallowed? - Stack Overflow

2017/11/22

Python Tips: Python でインタフェースを使いたい

Python でインタフェースの機能を使う方法をご紹介します。

・・・といっても、 Python 3.6 の時点で Python には言語機能としてのインタフェースは存在しません。具体的にいうと、「継承先に特定のインタフェースの実装を強制できるような仕組み」が Python にはありません。

しかし、標準ライブラリの abc を使うと次の 2 つのことが実現できます。

  • a. 複数のクラスを抽象クラスでまとめる
  • b. 継承先クラスのインスタンス生成時にメソッドの存在チェックをかける

このことをもって、「 Python でも他の言語でいうところの『インタフェース』の機能をある程度は利用できる 」と考えてもよいのではないかな、と個人的には思います。

今回はこの Python におけるインタフェース的な機能を abc で実現する方法をご紹介します。上の a b を順に見ていきましょう。


a. 複数のクラスを抽象クラスでまとめる


最初に、複数のクラスを抽象クラスでまとめる方法を見てみます。

複数の異なるクラスを抽象クラスでまとめることで、それらのクラスのオブジェクトに対して isinstance() が True を返すようなチェック機構を作ることができます。

具体的なコードを見てみましょう。

# coding: utf-8

'''デジタルコンテンツインタフェースを作ってテストする
'''

from abc import ABC


class Movie:
    '''映画
    '''
    pass


class Book:
    '''書籍
    '''
    pass


class DigitalContentInterface(ABC):
    '''デジタルコンテンツのインタフェース
    '''
    pass


# 映画と書籍を DigitalContentInterface の仮想サブクラスとして登録する
DigitalContentInterface.register(Movie)
DigitalContentInterface.register(Book)


def is_digital_content(object):
    '''オブジェクトがデジタルコンテンツかどうかをチェック
    '''
    return isinstance(object, DigitalContentInterface)


# Movie のインスタンスは DigitalContentInterface のインスタンスと認識される
m1 = Movie()
print(is_digital_content(m1))  # => True

# Book のインスタンスも DigitalContentInterface のインスタンスと認識される
b1 = Book()
print(is_digital_content(b1))  # => True

# 登録されていないクラスのインスタンスでは当然 False が返る
d1 = {}
print(is_digital_content(d1))  # => False

ここでは abc.ABC というクラスを継承する形で DigitalContentInterface というインタフェース用のクラスを作成しました。

続いて、クラスメソッド register() を使って、 MovieBook という 2 つのクラスを DigitalContentInterface の仮想サブクラスとして登録しています。

その後に isinstance() でチェックをかけると、 MovieBook のインスタンスは DigitalContentInterface のインスタンスと認識されることが確認できます。

おもしろいですね。

register() メソッドが便利なのは、組み込みのクラスを含む定義済みのクラスも「独自に作ったインタフェース」の仮想サブクラスとして登録できるところです。

# リストは本来(当然) DigitalContentInterface のインスタンスではない
l1 = []
print(is_digital_content(l1))  # => False

# リストクラスを DigitalContentInterface の仮想サブクラスとして登録すると・・・
DigitalContentInterface.register(list)

# リストのインスタンスも DigitalContentInterface のインスタンスと認識されるようになる
print(is_digital_content(l1))  # => True

ちなみに、この、特定のクラスを仮想サブクラスとして登録する register() メソッドを使った方法とは別に、クラスメソッド __subclasshook__() を使った方法も用意されています。

公式のサンプルを少し縮めたサンプルを見てみましょう。

class MyIterable(ABC):

    @classmethod
    def __subclasshook__(cls, C):
        if cls is MyIterable:
            if any("__iter__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

ここでは、 __subclasshook__() メソッドを実装することで「 __iter__() メソッドを実装しているクラスは MyIterable のサブクラスとみなす」というロジックが実現されています。 __subclasshook__() を使えば、このようにより柔軟なロジックで仮想サブクラスを設定することができます(例えば、インタフェースの定義時には具体的な仮想サブクラスがわからない状態でも仮想サブクラスを登録することができます)。

ちなみに、この abc.ABC はメタクラス abc.ABCMeta をよりシンプルに扱えるクラスとして Python 3.4 で導入されました。

a については以上です。続いて b の方を説明していきます。


b. 継承先クラスのインスタンス生成時にメソッドの存在チェックをかける


こちらは、よりインタフェースっぽい感じのふるまいが実現できる機能です。

こちらも先にサンプルコードを見てみましょう。

# coding: utf-8

'''デジタルコンテンツインタフェースでメソッドの実装の強制機能をテストする
'''

from abc import ABC
from abc import abstractmethod


class DigitalContentInterface(ABC):
    '''デジタルコンテンツのインタフェース
    '''

    def __init__(self, title):
        self.title = title

    @abstractmethod
    def format_title(self):
        pass


class Movie(DigitalContentInterface):

    def format_title(self):
        return 'Movie: {}'.format(self.title)


class Book(DigitalContentInterface):
    pass


# Movie のインスタンスは問題なく生成できる
m1 = Movie('From Dusk Till Dawn')
print(m1.format_title())
# => Movie: From Dusk Till Dawn
print(isinstance(m1, DigitalContentInterface))
# => True

# Book のインスタンスは
# Book が format_title() メソッドを定義していないので作成できない
b1 = Book('The Martian')
# TypeError: Can't instantiate abstract class Book with abstract methods format_title

以下、コード内の処理の流れを説明します。

まず最初に、 a と同じような形で abc.ABC クラスを継承した DigitalContentInterface というクラスを定義しています。この中で abc.abstractmethod というデコレータ用の関数を使って format_title() というメソッドをアブストラクトメソッドとして登録しています。

つづいて、 a の register() を使った形ではなく、通常の継承のシンタックスを使って DigitalContentInterface を継承したクラス MovieBook を定義しています。

実際にインスタンスを生成しようとすると、 Movie の方は問題なく生成できますが、 Book の方はアブストラクトメソッド format_title() をきちんと実装していないということで例外が上がって生成できません。

・・・というように、「インスタンス生成時にメソッドの存在チェックをかける」ことができていることが確認できます。

こちらもおもしろいですね。うまく活用できると便利そうです。

ただし、 b の方は使う際に押さえておくべき注意点がいくつかあります。

  • register() を使った方法で登録した仮想サブクラスに対してはチェックが走らない
  • @abstractmethod で登録されたメソッド自身が `pass` 以外の実装を持つことができて、サブクラスから super() で呼び出すことができる

ここで取り上げたのはインスタンスメソッドを使ったパターンだけでしたが、クラスメソッド、スタティックメソッド、プロパティに対しても同様のチェックを行うこともできます。その場合はデコレータの記述順などにも厳密なルールがあるため、興味のある方は利用の前に公式のドキュメントなどでしっかり確かめてから使うようにするとよいかと思います。


以上です。

おもしろいですねー。


参考

abc” Abstract Base Classes — Python documentation
Python でも ABC (Abstract Base Class) を使えば抽象クラスが作れる | CUBE SUGAR STORAGE

2017/11/17

Python Tips: 改行をうまく扱いたい

Python での改行の扱い方についてまとめてみました。わりとピンポイントなテーマになりますが、興味のある方はご参考にしてみていただければと思います。

  • Python における改行コード
  • 改行なしで出力する
  • ファイルの中身を行単位で取得する
  • 文字列の末尾の改行コードを取り除く
  • 文字列を改行で分割する
  • 改行コードをそのまま出力する
  • ソースコード内で改行する

Python における改行コード


まずは、基本中の基本ですが、今回のお話の前提となる「 Python における改行コード」について見ておきましょう。

Python における改行コードは \n です。

print('五月雨を\nあつめて早し\n最上川')
# =>
# 五月雨を
# あつめて早し
# 最上川
#

print("蛤の\nふたみにわかれ\n行秋ぞ")
# =>
# 蛤の
# ふたみにわかれ
# 行秋ぞ
#

文字列中に \n を入れるとそれが改行コードとして解釈されます。

Python には文字列リテラルの表記が 2 種類ーー通常の引用符と二重引用符がーーありますが、どちらも改行コードに関するふるまいは同じで、改行コードは出力時に改行に変換されます。

改行なしで出力する


組み込みの print() 関数は、デフォルトでは末尾に改行を自動的に追加するふるまいになっています。

print('Hello')
print('world')
# =>
# Hello
# world

この改行を防ぐには 2 の方法があります。

  • a. print() の引数 end を指定する
  • b. print() の代わりに sys.stdout.write() を使う

a. print() の引数 end を指定する

print() 関数の宣言部は次のようになっていて、このうちの end を指定すると、末尾に自動で追加される改行コードを変更することができます。

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

改行なしの出力にしたい場合は単純に空文字を渡せば OK です。

print('Hello', end='')
print('world', end='')
# => Helloworld

b. print() の代わりに sys.stdout.write() を使う

標準ライブラリ syssys.stdout.write()print() と同じように標準出力に書き出すための関数ですが、こちらは末尾に改行を自動追加するようなことはしないので、文字列を渡せばそれがそのまま出力されます。

import sys

sys.stdout.write('Hello')
sys.stdout.write('world')
# => Helloworld

このトピックについては過去にも記事を書いているので、興味のある方はご覧になってみてください。


ファイルの中身を行単位で取得する


テキストファイルの中身を改行までをひとつの区切りとして取得する方法です。方法がいくつかあるのでひとつずつご紹介します。

  • a. ファイルオブジェクトを for ループで回す
  • b. ファイルオブジェクトの readline() メソッドを使う
  • c. ファイルオブジェクトの readlines() メソッドを使う

a. ファイルオブジェクトを for ループで回す

ファイルオブジェクトはイテレータプロトコルを備えているので、ループで回すことができます。その場合の各要素はファイル内の各行となります。

with open(FILE_IN) as f:
    for line in f:
        print(line, end='')
# => ファイルの各行が出力される

各行の末尾には改行コード \n が付いたままになっているので、他のファイルや標準出力に出力する際には末尾の改行が重複しないように気をつける必要があります。

b. ファイルオブジェクトの readline() メソッドを使う

ファイルオブジェクトには readline() というメソッドが備わっており、これを使えば for ループの場合と同様に行単位でファイルの中身を取得することができます。

with open(FILE_IN) as f:
    while True:
        line = f.readline()
        if not line:
            break
        print(line, end='')
# => ファイルの各行が出力される

こちらの場合は readline() の戻り値が空文字列になればファイルの末尾に到達したことになります。ファイル途中の空行の場合は空文字ではなく改行コード \n が 1 つだけ入った文字列が返るので、区別することができます。

a と b を比べると、 pythonic なのはどちらかということ a の方なので、 b で書きたい強い理由が特に無いかぎりは a のパターンで書くのがよいかと思います。

c. ファイルオブジェクトの readlines() メソッドを使う

ファイルオブジェクトの readlines() メソッドは、その名前が示すとおり、ファイルの中身を複数行まとめて取得できるメソッドです。

with open(FILE_IN) as f:
    print(f.readlines())
# => ファイルの全行が行単位で格納されたリスト

ファイルのサイズが巨大な場合にどうなるかは調べていないのでわかりませんが、私が確認したかぎりでは、ファイルの全行を行単位で格納したリストが生成されます。 a 、 b の方法と同じく、こちらの場合も末尾の改行は含まれたままとなっているので注意が必要です。

ちなみに、次の例のように、ファイルオブジェクトの readlines() メソッドに引数を渡すと取得行数を指定することもできます。

with open(FILE_IN) as f:
    while True:
        lines = f.readlines(5):
        if not lines:
            break
        print(lines)
# => ファイルの中身を 5 行ずつ取得したリスト
# ファイルの行数が 5 行よりも少ない場合は、例外などは上がらずファイルの行がすべて格納される

文字列の末尾の改行コードを取り除く


続いて、文字列の末尾の改行コードを取り除く方法です。上の「テキストファイルの中身を行単位で取得する」ときなどに、各行を前処理したい場合などに利用することがあるかと思います。

方法としては、おそらく rstrip() メソッドを使う方法が最もシンプルで、なおかつ間違いがありません。

with open(FILE_IN) as f:
    for line in f:
        trimmed = line.rstrip('\n')
        print(trimmed)
# => ファイルの各行が表示される

ちなみに、 for ループで各行を取得した場合には、行の先頭に改行文字列があることは無いので、ここの rstrip()strip() に置き換えても同じ結果を得ることができます。ただ、 strip() よりも rstrip() を使った方がコードの意図をより明確に示すことができるので、このようなケースでは私は rstrip() を好んで使っています。

文字列を改行で分割する


間に改行が挟まった文字列を改行のところで分解する方法についてです。

こちらは、文字列型の splitlines() メソッドを使うのがよいでしょう。

haiku = '夏草や\n兵どもが\n夢の跡'
lines = haiku.splitlines()
print(lines)
# => ['夏草や', '兵どもが', '夢の跡']

名前そのままの挙動でわかりやすいですね。

ちなみに、 splitlines()keepends という引数を受け取ることができます。これは「改行コードを残すかどうか」を指定するためのフラグで、デフォルト値は False になっています。 True にすると、戻り値のリストの各要素の末尾に改行コードが残ったままになります。

haiku = '夏草や\n兵どもが\n夢の跡'
lines = haiku.splitlines(keepends=True)
print(lines)
# => ['夏草や\n', '兵どもが\n', '夢の跡']

同様の処理は、文字列型の split() メソッドで行うこともできますが、区切り文字が改行コードなら、 splitlines() を使う方が意図をより明確に示せるのでよいかなと思います。

改行コードをそのまま出力する


次は、改行コードをそのまま出力する方法についてです。

改行コードを変換せずにそのまま出力するには、 repr() 関数をかませるか、フォーマット文字列シンタックスの変換フラグ !rrepr() )を使うとよいかと思います。どちらも得られる結果は同じです。

haiku = '石山の\n石より白し\n秋の風'
print(repr(haiku))
# => '石山の\n石より白し\n秋の風'

haiku = '石山の\n石より白し\n秋の風'
print('{!r}'.format(haiku))
# => '石山の\n石より白し\n秋の風'

ソースコード内で改行する


以上のお話はすべて「文字列の中の改行」についてのお話でしたが、これは文字列の中ではなく、 Python のソースコードの中で改行するにはどうすればよいか、というお話です。

Python は、 C 言語のように文の末尾に ; を付ける必要がない代わりに、通常は行末がそのままひとつの文の終わりを意味することになっています。では、メソッドチェーンなどで一文が長くなるときにはどうやって改行したらよいのでしょうか。メジャーなやり方は、大きく分けて 2 つあります。

  • a. 改行を \ でエスケープする
  • b. かっこの中で改行する

a. 改行を \ でエスケープする

コード中の改行を文の終わりにしたくない場合は、 \ (バックスラッシュ)を使えば OK です。改行の直前に \ を置きましょう。

Address.select() \
    .where(Address.geo_lng.is_null(False)) \
    .where(Address.geo_lat.is_null(False))

b. かっこの中で改行する

Python では通常文末が文の終わりを示すことになりますが、かっこの中だけはわりと自由に改行することができます。ここで「かっこ」というのは、 [](){} のことを指しています。

例えば、リスト内包は次のように改行を入れて書くことができます。

[x * 5
    for x in range(10)
    if x % 2 == 0]

また、関数やメソッドの () 内の引数は途中で自由に改行することができます。

Address.select(
    Address.title,
    Address.geo_lng,
    Address.geo_lat)

この方法を使えば、例えば、メソッドチェーンが長くなる場合に、式全体を () でくくることで、各行の行末に毎回 \ を入れる手間を省くことができます。

(Address.select()
    .where(Address.geo_lng.is_null(False))
    .where(Address.geo_lat.is_null(False)))

・・・

というわけで、以上です。

このあたりは基本的なところではありますが、他の言語を長く触っていて久しぶりに Python に戻ってきたときなんかによく忘れていたりするので、パッと確認できるようにしておくと便利かと思います。

Python の改行の扱い方をまとめて押さえておきたいときにぜひ参考にしてみてください。

2017/11/13

Python が学べる英語のオンラインコースいろいろ

プログラミング言語「 Python 」を学べる英語のオンラインコースについてかんたんにまとめてみました。

システム開発・プログラミングというのはそれなりに歴史のある分野なので、日本語にも Python 関連の良書や良いサービスがたくさんあります(最近は特に増えてきたようです)。

ですので、プログラミングをわざわざ「英語で学ばないといけない」ということはありませんが、もしあなたが英語にある程度馴れがあり「プログラミングを学びたい」と考えているのであれば、日本語のサービスに加えて英語のサービスも選択肢に入れることを強くおすすめします。

英語でプログラミングを学ぶことには次のようなメリットがあります。

  • 日本語のものだけから選ぶよりも選択肢が広いので、より自分の学習スタイルに合ったものが見つかる
  • 各概念を英語の名称で学べるので、後からそれらを検索するときにスムーズ
  • 英語の説明の方が日本語よりもシンプルでわかりやすいことがある(必ずしもそうとはかぎりません)

これらに加えて、「 MIT やハーバード大学のような大学の講義を自宅で受けられること」もメリットのひとつだと思います。 MIT やハーバードの教授はどんな風に教えているのか、学生さんはどんな感じで学んでいるのか、を感じることができます。

ということで、私がよく聞くものや目にするものを中心に、英語で Python を学べるコースをいくつか集めてました。「英語で Python を学んでみたい」という方にご参考にしてみていただければと思います。


Google's Python Class



Google 社が提供する「 Google for Education 」というコンテンツ集の中の Python チュートリアルです。パッと読んだところでは、基本のところからとても丁寧に解説してあります。


Coursera Python コース群



定番の Coursera には Python で検索すると 100 以上のコースが見つかります。

ただし、数はたくさんありますが、次の 3 種類の講座があって、このすべてが「 Python を学ぶためのもの」というわけではないのでご注意ください。

  • a. Python を使って「プログラミング」を学ぶコース
  • b. 「 Python 」を学ぶコース
  • c. Python で「◯◯」をする方法を学ぶコース

c の◯◯には「統計分析」や「データサイエンス」、「機械学習」、「ウェブ制作」などのキーワードが入ります。

Python は幅広い分野で使われている言語なので、 c の講座が大量にあります。プログラミングや Python そのものについて学びたい場合は、 c のものは目的に合わないので、タイトルや説明文をよく読んで a か b のものを受講する(= c を選ばない)ようにするとよいでしょう。

動画、テキスト、クイズ、エクササイズなど、いろんなものを組み合わせてコースが組み立てられています。

コースによって無償のものもあれば有料のものもあります。


edX Python コース群



こちらも定番の edX の Python 関連コースです。内容は Coursera と似たような感じです。どのコースも非常にクオリティが高いです。

余談ですが、私は昔 edX の「 MITx6.00: Introduction to Computer Science and Programming Using Python 」を受講したことがあります。


そのときは、軽い気持ちで受け始めたらハマってしまい、課題をすべて提出し、満点に近いスコアにしてから修了しました(ちなみに私は学生時代の専攻がシステム関係だったので、コース内で学ぶことの大半は受講前から知っていました。それでも、ここで学べたことはたくさんありました。)。

いま確認すると、公称では「 1 週間に 15 時間程度の努力 × 9 週間」が必要なコースとのことなので、真剣に取り組むと決して楽なコースではありません。ただ、 CS に興味があり「将来プログラミング関係の仕事に就きたい」と考えている学生さんなんかは、とてもおすすめです。英語に苦手意識があまりなく、意欲と時間があれば(春休みや夏休みなどに)チャレンジしてみるとおもしろいと思います。

私の個人的な感覚では、「とりあえずプログラマーを 10 年間やっています」という人よりも「 MITx6.00 をすべて理解して修了しました」という初心者の方がエンジニアとして信用できる可能性が高いと思っています。それだけ濃密なコースです。


codecademy Python コース



codecademy の Python コースです。このうちの何割ぐらいの人が修了したのかはわかりませんが、 250 万人以上の人が受講を開始したそうです。


Udacity Python コース



Udacity の Python の基礎コースです。詳しくはわかりません。


Microsoft Virtual Academy Python コース



Microsoft も最近はこのようなコースを提供しているそうです。こちらも詳しいところはわかりません。


Lynda.com Python コース群



オンラインコースの老舗 Lynda.com の Python コース群です。 Lynda.com 全体の規模で考えると、 Python 関連のコースはあまり豊富ではありません。有料です。

個人的な印象としては Lynda.com は実用を重視している感じなので、学生さんよりは社会人を対象としている印象があります。


Code School の Python シリーズ



Code School の Python シリーズです。 Python の基礎と Pytbon のウェブフレームワーク Django のコースが用意されています。有料ですが、一部のコースが無料で提供されています。


Real Python



ウェブ開発を行うための Python 入門サイトです。有料です。次の 3 つのコースが提供されています。

  • Course 1: Introduction to Python
  • Course 2: Web Development with Python
  • Course 3: Advanced Web Development with Django



SoloLearn の Python 3 チュートリアル



SoloLearn の Python チュートリアル。教材・ツールがリッチでおもしろそうですが、詳しくはわかりません。


以上です。

他にも無数にあるかと思いますが、現時点でのメジャーどころで間違いがないのはこのあたりになる、の、かな、と思います。

受講・修了した経験のある方は、おすすめのものがあればぜひ教えてください。

...


英語圏のものも玉石混淆・ピンキリなので、「英語のものは日本語のものより絶対にいい!」とは思いませんが、こと CS 、プログラミングに関しては英語のリソースは日本語の何倍・何十倍もあるので(個人的な感覚値です)、選択肢は多ければ多いほどいいもの、自分に合うものに出会える可能性は上がるかなと思います。

よろしければ参考にしてみてください。

2017/10/31

Python Tips:画像を指定のサイズに切り取りたい

Python を使って画像の一部を切り出して保存する方法をご紹介します。

Python 3 の場合は Python 2 で有名な画像処理ライブラリ PIL のフォークである Pillow を使う形がシンプルでかんたんです。

- Pillow: the friendly PIL fork

PIL の Image.open() で元画像を取得して、 crop() メソッドに切り出し場所の座標を渡すと画像を切り出せます。あとは save() で別ファイルとして保存すれば OK です。

from PIL import Image

infile = '01.jpg'
outfile = 'out-01.jpg'
area = (left, top, right, bottom)

img = Image.open(infile)
cropped_img = img.crop(area)
cropped_img.save(outfile)

例えば、画像の中心を切り出す場合のサンプルコードは次のようになります。

# coding: utf-8

"""画像の中心部分を指定されたサイズ切り取った画像を生成する
"""

from pathlib import Path
from PIL import Image


crop_info = (
  # 画像 01.jpg は横 200px - 縦 100px で切り出す
  ('01.jpg', 200, 100),
  # 画像 02.jpg は横 300px - 縦 500px で切り出す
  ('02.jpg', 300, 500),
)


def main():
    print('started.')
    crop_images(crop_info, 'out-')
    print('finished.')


def crop_images(crop_info, prefix):
    for infile, width, height in crop_info:
        path = Path(infile)
        outfile = prefix + path.name
        crop_image(infile, outfile, width, height)


def crop_image(infile, outfile, width=None, height=None):
    img = Image.open(infile)

    width_orig, height_orig = img.size
    if not width:
        width = width_orig
    if not height:
        height = height_orig

    center_h = width_orig / 2
    center_v = height_orig / 2
    width_half = width / 2
    height_half = height / 2

    # 中心の切り出し場所の座標を計算する
    left = center_h - width_half
    top = center_v - height_half
    right = center_h + width_half
    bottom = center_v + height_half
    area = (left, top, right, bottom)

    cropped_img = img.crop(area)
    cropped_img.save(outfile)


if __name__ == "__main__":
    main()

このスクリプトを実行すると、 01.jpg が次のような画像だった場合・・・



Photo by Terry zhou CC BY-NC-SA

次のような横 200px 、縦 100px の画像 out-01.jpg が生成されます。



メソッド名なども直感的でわかりやすく便利ですね。

2017/10/24

Python Tips:Python でファイルに権限を追加したい

Python でファイルに権限を追加する方法について見てみます。

イメージとしては、次のコマンドと同等の処理を Python で行うイメージです。

$ chmod u+w target_file


まず、ファイルの権限を指定する方法についてですが、 Python では os.chmod()pathlib.Path.chmod() を使ってファイルの権限を設定することができます。

os.chmod — Python documentation
pathlib.Path.chmod — Python documentation

一方、ファイルの権限の取得は os.stat()pathlib.Path.stat() で行うことができます。

os.stat — Python documentation
pathlib.Path.stat — Python documentation

また、各権限を表すフラグとして次の定数が用意されています。

# read (読み込み):
# stat.S_IRUSR  256
# stat.S_IRGRP   32
# stat.S_IROTH    4

# write (書き込み):
# stat.S_IWUSR  128
# stat.S_IWGRP   16
# stat.S_IWOTH    2

# execute (実行):
# stat.S_IXUSR   64
# stat.S_IXGRP    8
# stat.S_IXOTH    1

stat — Python documentation

ファイルの権限の追加(や削除)はこれらを組み合わせて行うことになります。
たとえば、書き込み権限を付与する関数 add_write_permission() は次のように書くことができます。

# coding:  utf-8

'''Provides functions to add permissions to files.
'''

import stat
from pathlib import Path
from functools import reduce


def add_write_permission(path: Path, target='u'):
    '''Add "write" permission to specified targets.
    '''
    mode_map = {
        'u': stat.S_IWUSR,
        'g': stat.S_IWGRP,
        'o': stat.S_IWOTH,
        'a': stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH,
    }

    mode_additional = combine_permissions(target, mode_map)

    path.chmod(path.stat().st_mode | mode_additional)


def combine_permissions(target, mode_map):
    modes = map(lambda x: mode_map[x], target)
    return reduce(lambda x, y: x | y, modes)

次のように利用します。

add_write_permission(Path('sample-1.txt'), 'ug')
add_write_permission(Path('sample-2.txt'), 'a')

書き込み権限、実行権限についても追加することができます。
GitHub Gist に書き込み権限、実行権限を付与するための関数も含めたサンプルをあげているので、興味のある方は参考にしてみてください。



Functions to add file permissions with Python. · GitHub

2017/10/17

Python Tips:アニメーション GIF から静止画をまとめて抽出したい

Python でアニメーション GIF ( animated GIF )からフレーム画像を抽出する方法をご紹介します。

早速結論ですが、 Python の画像処理用ライブラリ Pillow を使うのが比較的かんたんです。他にも方法は無数にあるかと思いますが、私は Pillow でやるのがスムーズでした。

Pillow は画像処理ライブラリ PIL のフォークで、 PIL が対応していない Python 3.x に対応しているのがポイントです。 Pillow は pip でインストールするときには名称 Pillow でインストールしますが、スクリプトの中で import するときは PIL と同じ PIL という名前を使用します。

$ pip install Pillow

from PIL import Image

Pillow のドキュメントサイトはこちらです。

Pillow — Pillow (PIL Fork) documentation

実際に、 Pillow を使ってアニメーション GIF を分解する方法を見てみましょう。

# coding: utf-8

'''アニメーション GIF のフレーム画像(静止画)を抽出する
'''

from pathlib import Path
from PIL import Image, ImageSequence

# 分割したいアニメーション GIF 画像
IMAGE_PATH = 'target.gif'
# 分割した画像の出力先ディレクトリ
DESTINATION = 'splitted'
# 現在の状況を標準出力に表示するかどうか
DEBUG_MODE = True


def main():
    frames = get_frames(IMAGE_PATH)
    write_frames(frames, IMAGE_PATH, DESTINATION)


def get_frames(path):
    '''パスで指定されたファイルのフレーム一覧を取得する
    '''
    im = Image.open(path)
    return (frame.copy() for frame in ImageSequence.Iterator(im))


def write_frames(frames, name_original, destination):
    '''フレームを別個の画像ファイルとして保存する
    '''
    path = Path(name_original)

    stem = path.stem
    extension = path.suffix

    # 出力先のディレクトリが存在しなければ作成しておく
    dir_dest = Path(destination)
    if not dir_dest.is_dir():
        dir_dest.mkdir(0o700)
        if DEBUG_MODE:
            print('Destionation directory is created: "{}".'.format(destination))

    for i, f in enumerate(frames):
        name = '{}/{}-{}{}'.format(destination, stem, i + 1, extension)
        f.save(name)
        if DEBUG_MODE:
            print('A frame is saved as "{}".'.format(name))


if __name__ == '__main__':
    main()

このスクリプトを実行すると、アニメーション GIF ファイル target.gif の静止画をすべて抽出して splitted というフォルダに書き出してくれます。元の GIF 画像には変更を加えることなく、抽出した画像を、末尾に連番をつけた形でファイルとして切り出します。

ポイントは PIL.ImageSequence.Iterator クラスです。 PIL.Image で開いた画像をこのコンストラクタに渡すと、アニメーション GIF 内の各フレーム(静止画)を返すイテレータオブジェクトを生成してくれます。

便利ですねー。


参考


逆に、静止画を組み合わせてアニメーション GIF を作る方法については、次の Stack Overflow ページでやりとりされています。興味のある方はこちらもよろしければ。

Programmatically generate video or animated GIF in Python? - Stack Overflow

ライブラリ Pillow の使い方についての日本語の解説はこちらのページが丁寧でわかりやすいです。

Python 3.5 対応画像処理ライブラリ Pillow (PIL) の使い方 - Librabuch

2017/10/09

Python Tips:ターミナルのサイズを取得したい

Python でコマンドラインで利用するちょっとしたツールを作る場合には、現在のターミナルのウィンドウサイズを知りたくなることがあります。

たとえば、よくあるのは「出力を画面幅いっぱいになるようにきれいに出したいので、 1 行の文字数を知りたい」といったケースなどでしょうか。

Python でターミナルのサイズを取得したい場合は組みこみの shutil ライブラリの get_terminal_size() を使えば OK です。

使ってみます。

import shutil

terminal_size = shutil.get_terminal_size()

print(type(terminal_size))
# => <class 'os.terminal_size'>

print(terminal_size.columns)
# => 120 など( 1 行の長さ(文字数))

print(terminal_size.lines)
# => 40 など(行数)

print(terminal_size[0])
# => columns と同じ結果

print(terminal_size[1])
# => lines と同じ結果

get_terminal_size() の戻り値は os.terminal_size クラスの named tuple で、アトリビュートでも、インデックスでも値を抽出することができます。

便利です。

どうも、 os の方にも同名の os.get_terminal_size() という関数があって、これもまったく同じ結果を返しますが、公式のドキュメントによると、「 os.get_terminal_size はローレベルの実装であり、通常はハイレベルな shutil.get_terminal_size() を使いましょう」とのことです。このあたりの詳しい理由はわかりませんでした。。

shutil.get_terminal_size() is the high-level function which should normally be used, os.get_terminal_size is the low-level implementation.


参考

How to get Linux console window width in Python - Stack Overflow

2017/10/04

Python Tips: Python で UTF-8 の BOM ありなしを見分けたい

Python で UTF-8 の BOM のありなしを見分ける方法について見てみたいと思います。

UTF-8 には、「バイト・オーダー・マーク」、通称「 BOM 」と呼ばれるものがあります。これはテキストの始まりをプログラムに伝えるためのデータ内の特定のマークのことであり、具体的にはユニコード文字 U+FEFF がそのマークとして使用されています。

UTF-8 にはこの BOM があるものと無いものとが存在していて、前者を「 BOM あり UTF-8 」( UTF-8 with BOM )、後者を「 BOM なし UTF-8 」あるいはただの「 UTF-8 」と呼んだりします。

詳しくは Wikipedia がわかりやすいので興味のある方はご覧になってみてください。

Byte order mark - Wikipedia


この UTF-8 の BOM を Python で扱う方法について見てみましょう。

ファイルを読む


ファイルを読む場合は、 open() 関数の引数 encoding で指定する文字コードを、 BOM なしの UTF-8 では 'utf-8' 、 BOM あり UTF-8 では 'utf-8-sig' と指定します。

BOM あり UTF-8 をあえて 'utf-8' で読み込むと最初の 1 文字が BOM を表す '\ufeff' (不可視文字)になるので、例えば、 BOM のありなしを自動判定してファイルの中身を読み込む関数を作るとしたら次のようになります。

def open_file_with_utf8(filename):
    '''utf-8 のファイルを BOM ありかどうかを自動判定して読み込む
    '''
    is_with_bom = is_utf8_file_with_bom(filename)

    encoding = 'utf-8-sig' if is_with_bom else 'utf-8'

    return open(filename, encoding=encoding).read()


def is_utf8_file_with_bom(filename):
    '''utf-8 ファイルが BOM ありかどうかを判定する
    '''
    line_first = open(filename, encoding='utf-8').readline()
    return (line_first[0] == '\ufeff')


content = open_file_with_utf8('file_with_utf8.txt')


ファイルを書く


逆にファイルを書くときも encoding を指定すれば OK です。 BOM なし UTF-8 として書きたければ 'utf-8' を、 BOM あり UTF-8 にしたければ 'utf-8-sig' を指定します。

with open('file_out_with_utf8_with_bom', 'w') as f:
    f.write('This is a file written with utf-8 with BOM.')

ちなみに、上述のとおり、 BOM あり UTF-8 のファイルを encoding 'utf-8' で読むと、最初の文字が '\ufeff' になります。逆に、 BOM なし UTF-8 のファイルを encoding 'utf-8-sig' で読むと、普通に読めて特に問題はありません。

BOM のありなしが特に問題にならない場合はよいのですが、ファイルの長さなどを厳密に比較したりしたいような場合には 'utf-8-sig' を適宜使うとよいでしょう。


参考


utf 8 - python utf-8-sig BOM in the middle of the file when appending to the end - Stack Overflow
7.2. codecs — Codec registry and base classes — Python 3.6.3 documentation

2017/09/28

Python Tips:Python で文字列を切り詰めたい

Python で文字列を切り詰める方法についてご紹介します。

いろんな方法があるように思いますが、今回はその中で次の 2 つの方法をご紹介してみます。

  • A. スライスで切り詰める
  • B. テンプレートに埋め込むときに切り詰める


A. スライスで切り詰める


こちらは文字列のスライスを使って切り詰める方法です。こちらはシンプルですね。

s1 = '露と落ち 露と消えにし 我が身かな 浪速のことは 夢のまた夢'
s1_truncated = s1[:10]

print(s1_truncated)
# => 露と落ち 露と消えに

関数化するなら例えば次のような形になるでしょうか。

def truncate(string, length, ellipsis='...'):
    '''文字列を切り詰める

    string: 対象の文字列
    length: 切り詰め後の長さ
    ellipsis: 省略記号
    '''
    return string[:length] + (ellipsis if string[length:] else '')

print(truncate('露と落ち 露と消えにし 我が身かな 浪速のことは 夢のまた夢', 10))
# => 露と落ち 露と消えに...
print(truncate('朝ぼらけ', 10))
# => 朝ぼらけ
# => 


B. テンプレートに埋め込むときに切り詰める


こちらは文字列型の format() メソッドの機能を使った方法です。

s2 = '嬉しやと 二度さめて一眠り うき世の夢は 暁の空'

print('{:.10}'.format(s2))
# => 嬉しやと 二度さめて

{} の中で : の後に .数字 という形で書くと文字数を指定することができます。この例では文字を最大 10 文字になるように切り詰めています。

この {:.10} という形は元々、小数点数の小数点以下の桁数を指定するためのものだと思うのですが、数字以外の場合にも動作して、数字以外の場合は文字数を指定できるものになっているようです。

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

The precision is a decimal number indicating how many digits should be displayed after the decimal point for a floating point value formatted with 'f' and 'F', or before and after the decimal point for a floating point value formatted with 'g' or 'G'. For non-number types the field indicates the maximum field size - in other words, how many characters will be used from the field content. The precision is not allowed for integer values.

6.1. string — Common string operations — Python documentation

以上です。


参考

How to truncate a string using str.format in Python? - Stack Overflow

2017/09/19

サンプルコード:フィボナッチ数を対数時間で求める

今回はフィボナッチ数を対数時間で求めるプログラムについてです。



Wikipedia より)


フィボナッチ数とは、次の漸化式で表せる数列です。

F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (n >= 2)

実際に各値を求めると次のような数値になります。

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, ...

Wikipedia には次のような一文があります。

フィボナッチ数(フィボナッチすう、英: Fibonacci number)は、イタリアの数学者レオナルド・フィボナッチ(ピサのレオナルド)にちなんで名付けられた数である。

フィボナッチ数 - Wikipedia
Fibonacci - Wikipedia

なんでも、 12 世紀頃の数学者であり「フィボナッチ」(ボナッチの息子)と(後に)呼ばれた、イタリアのレオナルドさん発案だそうです。

余談ですが、「レオナルド」はイタリアあたりの歴史にやたらに登場しますね。日本でいう「ごんべえ」的な感じでしょうか。

それはさておき。

フィボナッチ数の値は解析的に求めることもできますが、プログラミングの課題としては、再帰アルゴリズムあるいは動的計画法を使って解くことが求められることが多いようです。

まずはロジックが最もシンプルな線形時間でのアプローチを先に見てみましょう。


線形時間での求解


単純なループを使った動的計画法アプローチでは、線形時間 O(n) で解くことができます。



これは直感的な求め方ですね。続いて対数時間で求めるアプローチです。


対数時間での求解


フィボナッチ数は直感に反し、対数時間 O(log(n)) で求めることができます。

ポイントは次のとおりです。

  1. 行列で考えた場合、フィボナッチ数の漸化式は F(n) = P * F(n - 1)(ただし P((1, 1), (1, 0) という行列)で表せる。
  2. 結果、 F(n)F(n) = P^n * (1, 0)^T で求めることができる。
  3. ミソは P^n の計算で、これは単純に Pn 回積を繰り返して求めることもできるが、例えば n = 2^m となる場合(例えば n = 64 なら m = 6 )にかぎると、 P^64 = (P^32)^2 = ((P^16)^2)^2 = (((P^8)^2)^2)^2 = ((((P^4)^2)^2)^2)^2 = (((((P^2)^2)^2)^2)^2)^2 ということで、 2 乗を 6 回繰り返す形でも求めることができる(つまり指数時間で求められる)。これは 63 回積を繰り返すより断然早い。
  4. n = 2^m とならない n については、 n = 2^m1 + 2^m2 + 2^m3 + ... という形に分解することで(要は 2 進数表現にすることによって)各部を 2^m の形に表すことができる。この事実を利用すると、 P^n = P^(2^m1 + 2^m2 + 2^m3 + ...) = P^(2^m1) * P^(2^m2) * P^(2^m3) * ... で求められる。

詳細のロジックに興味のある方は、次のページの解説がとても丁寧でわかりやすいのでそちらをご覧になってみてください。

The Nth Fibonacci Number in O(log N)

実際のサンプルコードは次のとおりです。



コード上のポイントは次のとおりです。

  • フィボナッチ数を求める関数は get_fibonacci() で、それ以外の関数はすべてヘルパー関数です。
  • if __name__ == '__main__': 以下の部分はすべてテストコードです。
  • 上の 4. の 2 の乗数への分解を行う関数は get_2exponent_series() です。
  • 上の 3. の部分を担う関数は pow_matrix() です。
  • 行列の積の計算は multiply_matrix() で行っています。単純な 2 x 2 行列の計算なので、ライブラリは使わずそのまま計算しています。間違いやすいのでケアレスミスに注意が必要です。
  • やや本筋から外れる、キャッシングと畳み込みの計算には標準ライブラリ functoolslru_cache()reduce() を使っています。

以前の「ハノイの塔」のサンプルと同じく、ホワイトボードコーディングの本の中に出てきたので個人的なエクササイズを兼ねて書いてみました。

この話題(「フィボナッチ数を対数時間で求める方法」)については学生時代にちらっと学んだことがある気がするのですが、その詳細は完全に忘れてしまっていたので、今回改めて調べて「おぉ、これはすごい!」ととても感動しました(笑)。

再帰や動的計画法は「問題の本質をいかにシンプルな形で捉え抽出することができるか」がポイントで、このあたりは汎用の問題解決能力とも直結する気がします。私はこのあたりは特段苦手というわけではありませんが、「ものすごく得意」「閃きまくって仕方がない!」というわけでもないので、こういう問題解決が得意な人を見るとうらやましくなります。

2017/09/06

ライブラリ: watchdog

Python のライブラリである watchdog をご紹介してみます。

watchdog : Python Package Index

今回は次の組み合わせで動作確認をしています。

python 3.6.1 
watchmedo 0.8.3 

watchdog とは


watchdog とはファイルの変更を監視して、ファイルが変更されたら何らかの処理を行う自動化ツール作成のためのライブラリです。

watchdog を import してスクリプトを書く方法と、 watchmedo というコマンドを利用する方法の 2 つの利用方法が用意されています。

私はライトな使い方しかしておらず「コマンドを使用する方法」だけで事足りてしまっているので、今回は後者のみの紹介とさせていただきます。スクリプトを書く方法に興味のある方は公式のドキュメントをご覧になってみてください。

Watchdog — watchdog documentation

インストール


pip でそのままインストールしましょう。

$ pip install watchdog

基本的な使い方


上述のとおり、今回は watchdog が提供するコマンド watchmedo を使った方法をご紹介します。

といっても使い方はシンプルで、 watchmedo コマンドに監視対象と実行したいコマンドをオプションでただ渡すだけで OK です。

以下は私が PHP のプロジェクトで PHPUnit のテストを書いている最中に使ったコマンドです。 PHPUnit のテストファイルが更新されたら(私がファイルを保存したら)自動で対象のテストファイルに対してテストを実行するというものです。

watchmedo shell-command \
--patterns="*Test.php" \
--ignore-directories \
--recursive \
--command='./vendor/bin/phpunit ${watch_src_path} && echo ""'

watchmedo にサブコマンド shell-command を指定して実行しています。使用しているオプションについて以下にかんたんに説明します。

  • --patterns 対象のファイル名のパターン。
  • --ingore-directories ディレクトリを無視する場合につける。
  • --recursive 再帰的に子孫ディレクトリをウォッチする場合につける。
  • --command ファイルの変更が検出されたときに実行したいコマンド。

私がいま確認したところ、公式のドキュメントには詳しい記述がありませんが、 --command オプションで指定するコマンドの中では以下のパラメータを利用することができます。

${watch_src_path} 変更されたファイルのパス
${watch_dest_path} ファイル移動が発生するイベントにおいて移動先のファイルのパス
${watch_event_type} イベントのタイプ
${watch_object} ファイルかディレクトリか

以上です。

このあたりについては Node.js まわりでの開発が最近活発ですが Node.js は流行り廃りのサイクルが他の言語に比べて短いような気もするので、この Python の watchdog など短期間では廃りにくい方法をひとつ知っておくと便利だと思います。

参考

Watchdog — watchdog documentation
GitHub - gorakhargosh/watchdog: Python library and shell utilities to monitor filesystem events.

2017/08/28

サンプルコード:ハノイの塔

再帰( recursion )を使うと解法のロジックをわかりやすく表現できる問題のひとつに「ハノイの塔」というものがあります。



ハノイの塔(ハノイのとう、Tower of Hanoi)はパズルの一種。 バラモンの塔または ルーカスタワー(Lucas' Tower)とも呼ばれる。

以下のルールに従ってすべての円盤を右端の杭に移動させられれば完成。

  • 3本の杭と、中央に穴の開いた大きさの異なる複数の円盤から構成される。
  • 最初はすべての円盤が左端の杭に小さいものが上になるように順に積み重ねられている。
  • 円盤を一回に一枚ずつどれかの杭に移動させることができるが、小さな円盤の上に大きな円盤を乗せることはできない。

n枚の円盤すべてを移動させるには最低 2n − 1 [2]回の手数がかかる。

解法に再帰的アルゴリズムが有効な問題として有名であり、プログラミングにおける再帰的呼出しの例題としてもよく用いられる。

ハノイの塔 - Wikipedia

画像、テキストともに出典はいずれも Wikipedia です。

私事ですが、最近ホワイトボードプログラミングの本を読んでおりその中にハノイの塔が出てきたので、今回は個人的なエクササイズも兼ねてハノイの塔のプログラムを Python で書いておきたいと思います。

ここに載せるには少し長くなったので Gist に貼り付けました。



このプログラムを実行すると次のような内容が出力されます。「 move ... 」から始まる行はディスクの移動を表します。 s d e は各塔を表します。蛇足かもしれませんが、 s は source 、 d は destination 、 e は extra の略です。

started.
s: 3 2 1 
d: 
e: 
move disk-1 from s to d (1).
s: 3 2 
d: 1 
e: 
move disk-2 from s to e (2).
s: 3 
d: 1 
e: 2 
move disk-1 from d to e (3).
s: 3 
d: 
e: 2 1 
move disk-3 from s to d (4).
s: 
d: 3 
e: 2 1 
move disk-1 from e to s (5).
s: 1 
d: 3 
e: 2 
move disk-2 from e to d (6).
s: 1 
d: 3 2 
e: 
move disk-1 from s to d (7).
s: 
d: 3 2 1 
e: 
finished.

解法のコアとなる部分は TowerOfHanoi クラスの move_disk() メソッドのみです。その他の部分は、途中経過や移動の回数を表示するための周辺的コードです。

ハノイの塔の解法がなぜ move_disk() のような再帰を含むメソッド(関数)で書けるのかについてここでの説明は割愛します。興味のある方は Wikipedia の「解き方」の節をご覧になってみてください。

私事ですが、ふだんはウェブ開発をやっていて、このような難しいアルゴリズム問題を考えたりするようなことはあまりないので、書いていてとても新鮮な気持ちになりました。おもしろいです。


参考

ハノイの塔 - Wikipedia

2017/07/14

Python の join() が文字列型のメソッドである理由

Python で区切り文字を使って文字列を連結する join() は文字列型のメソッドです。

''.join(['松', '竹', '梅'])  # => '松竹梅'
' | '.join(['Home', 'About', 'Services'])  # => 'Home | About | Services'

他の言語では join() は配列( Python でいうリスト)のメソッドとして用意されているケースが多いため、他の言語を知った後に Python を知った人の多くがこの「 join() が文字列型のメソッドであること」に気持ち悪さを感じるようです。

ではそもそもなぜ Python の join() はリストではなく文字列型のメソッドとして用意されているのでしょうか。今回はこのあたりを見てみたいと思います。


経緯


文字列型の join() メソッドは 2000 年頃 Python 1.6 の頃に、当時存在した string モジュールの string.join(seq, delimiter) 関数と同等の機能を持つものとして導入されました。

興味のある方はリリースノートをご覧になってみてください。

Python 1.6 | Python.org

そのときの議論は「 String methods... finally 」というタイトルのスレッドで行われたようで、その全容はアーカイブ上で確認することができます。

[Python-Dev] String methods... finally


導入の際にはいろんな可能性が議論されたようで、そのあたりのことが次の Stack Overflow の質問への回答の中でわかりやすくまとめられています。

Python join: why is it string.join(list) instead of list.join(string)? - Stack Overflow

この回答によると、文字列を連結できる機能の候補として以下の 4 つがあげられたとのこと。

  • 文字列型のメソッド str.join(seq)
  • シーケンスのメソッド seq.join(str)
  • シーケンスのメソッド seq.reduce(str)
  • 関数 join(seq, delimiter)

ここで、 str は文字列を表すオブジェクト、 seq は sequence/iterable オブジェクトを意味します。

このうち、 seq.reduce(str) はわかりづらいという理由でボツとなり、関数 join() は文字列型との結びつきが強い機能なのにグローバルな名前空間を使うのはよくないという理由で候補から外れたようです。

残るは str.join(seq)seq.join(str) の 2 つですが、この 2 つの間では次のような理由で str.join(seq) の方に軍配が上がったようです。

  • str.join(seq) なら文字列やリストやタプルなど組み込みの型だけでなく、後から追加されるあらゆる sequence/iterable オブジェクトに自動で対応できる。
  • seq.join(str)seq の要素がすべて文字列型のときにだけ使えるメソッドであり、それが seq 一般のメソッドになっているのは変。
  • 区切り文字は省略できない形の方がよいが、 seq.join(str) では省略できるように見えてしまう。

その他、 seq の各要素を自動的に文字列型にキャストすべきかどうかといった議論もそのときになされたようです(結論は「キャストはしない」ということになっています)。


メリット / デメリット


上で見た導入時の議論ポイントも含めて、 join() が文字列型のメソッドであることのメリット / デメリットをまとめてみます。

メリット

  • 文字列型やリストやタプルだけでなく、後から追加される sequence/iterable なオブジェクトに自動で対応できる。
  • Python は sequence/iterable を扱う方法として、 sequence/iterable そのものにメソッドを追加するのではなく、 map()filter()sum()functools.reduce() などのように sequence/iterable を受け取る関数を用意していることが多い。 str.join(seq) にすれば、それらと一貫性がある。
  • sep.join(seq) は、「 separator joins sequence 」という順番になっているので英語としてむしろ自然な並びである。
  • join() の処理の中身はあくまでも文字列の連結なので、文字列型のメソッドになっている方がまとまりがよい。
  • 関数の join(sep, seq) なら引数の順番で迷う可能性があるが、 str.join(seq) なら迷わない。

デメリット

  • 他の言語の join() の使い方に慣れ親しんだ人にとってわかりづらい。

これぐらいでしょうか。このように挙げてみると、デメリットは慣れの問題だけで、総合して考えるとメリットの方が断然多いような気がしてきます。

中でも決め手になるのは Python には文字列を表すための型が複数ある点だと思います。もし仮に Python で文字列を表すための型が 1 つしかなかったなら seq.join(str) という書き方もありといえばありだった気もしますが、文字列を表すための型が複数ある状況では、 sequence/iterable な各クラスの方で毎回文字列型に対応させるためのプロトコルのようなものを実装するのはあまりよいやり方とはいえなさそうです。

# 複数の文字列型で共通の形で join が利用できる

# 1. str
sep = '☆'
seq = ('サマー', 'キャンペーン')
sep.join(seq)  # => b'サマー☆キャンペーン'

# 2. bytes
sep = bytes('☆', 'sjis')
seq = (bytes('サマー', 'sjis'), bytes('キャンペーン', 'sjis'))
sep.join(seq)  # => b'\x83T\x83}\x81[\x81\x99\x83L\x83\x83\x83\x93\x83y\x81[\x83\x93'

# 3. bytearray
sep = bytearray('☆', 'utf-8')
seq = (bytearray('サマー', 'utf-8'), bytearray('キャンペーン', 'utf-8'))
sep.join(seq)  # => bytearray(b'\xe3\x82\xb5\xe3\x83\x9e\xe3\x83\xbc\xe2\x98\x86\xe3\x82\xad\xe3\x83\xa3\xe3\x83\xb3\xe3\x83\x9a\xe3\x83\xbc\xe3\x83\xb3')

以上です。

なお、上であげた理由などはあくまでも各種リソースをもとにした私の解釈です。正確なニュアンスを知りたい方は直接原典にあたるなどしてみていただければと思います。


参考

[Python-Dev] String methods... finally
Python join: why is it string.join(list) instead of list.join(string)? - Stack Overflow
Why is join() in Python a method on strings rather than lists (or other iterables)? - Quora
Why is join() a string method instead of a list or tuple method?
1.14. Joining lists and splitting strings
Python and the Principle of Least Astonishment | Armin Ronacher's Thoughts and Writings

2017/06/27

Python 3 の print() 関数の使い方

Python 3 の print() まわりの機能をご紹介します。

print('Hello world')

Python 2 と Python 3 では print() の機能が大きく異なります。 Python 2 の print 文については次の記事などを参考にしてみてください。

Python 2 の print 文の使い方


Python 3 の print() の基本的な使い方


Python 3 の print() は文字列を出力するための関数です。

宣言部は次のようになっており、出力対象のオブジェクトの他にもさまざまな引数を受け取ることができます。

print(*objects, sep, end, file, flush)

各引数の意味合いはそれぞれ次のとおりです。

  • objects: 出力対象のオブジェクト。複数個渡すことができる。
  • sep: objects が複数個渡された場合の区切り文字(セパレータ)。デフォルトは半角空白。
  • end: 最後の要素の末尾に付けられる文字。デフォルトは改行文字。
  • file: 出力先。デフォルトは標準出力だが、ファイルオブジェクトなどを指定することもできる。
  • flush: バッファなしで出力するかどうか。デフォルトは False で、出力先によって自動的に定められる。

いくつかのパターンで使ってみましょう。

# 文字列を出力する
print('Python')
# => Python

# 複数の文字列をまとめて出力する
print('This', 'is', 'a', 'chair.')
# => This is a chair.

# 文字列以外のオブジェクトを出力する
print('Total: ', 1000)
# => Total:  1000

print('List: ', [3, 5, 7])
# => List:  [3, 5, 7]

# カスタムクラスのオブジェクトを出力する
class Dog: 
    def __init__(self, name): 
        self.name = name

    def __str__(self): 
        return 'Dog (' + self.name + ')'

jiro = Dog('Jovani')
print(jiro)
# => Dog (Jovani)

# 区切り文字を変える
# デフォルトは半角空白
print('P', 'T', 'A', sep='__')
# => P__T__A

# 通常最後に追加される改行を別の文字列に変える
print('ABC', end='\n-------\n')
print('DEF')
# => ABC
# => -------
# => DEF

# 通常最後に追加される改行を削除する
print('ABC', end='')
print('DEF')
# => ABCDEF

# 標準出力ではなくファイルに出力する
with open('out.txt', 'w') as f: 
    print('result 1', file=f)
    print('result 2', file=f)

print(open('out.txt').read())
# => result 1
# => result 2

print() 関数の機能については以上です。


str.format()


print() 関数といっしょに使うことが多いもののひとつに文字列オブジェクトの str.format() があります。

これは文字列の中に変数を展開して挿入してくれるメソッドです。いくつかサンプルを見てみましょう。

# 細かな指定は行わず {} だけで出力する
key = 'name'
value = 'python'
print('{}: {}'.format(key, value))
# => name: python

# 引数のインデックスを指定して出力する
# この場合、同じ引数を何度も利用できる
value = 'pain'
print("It's your {0} or my {0} or somebody's {0}".format(value))
# => It's your pain or my pain or somebody's pain

# キーワード引数の形式で指定する
print('{fruit}食えば鐘が鳴るなり{temple}'.format(temple='法隆寺', fruit='柿'))
# => 柿食えば鐘が鳴るなり法隆寺

# インデックスを指定して一部を出力する
company = {
  'name': 'Sharp',
  'description': 'Meno tsuke dokoro ga sharp.',
}
print('{0[name]} -- {0[description]}'.format(company))

# アトリビュート名を指定して一部を出力する
class Dog: 
    def __init__(self, name): 
        self.name = name

kiyoshi = Dog('Campanella')
print('name: {0.name}'.format(kiyoshi))
# => name: Campanella

# 型とフォーマットを指定して出力する
print('{:0.2f}'.format(15))
# => 15.00

print('{:^10d}'.format(15))
# => '    15    ' ( 10 文字の中で中央寄せされた 15 )

str.format() はレシーバである文字列の中の {} に引数を挿入して展開してくれます。各 {} に対して、どの引数を挿入するのか、どのようなフォーマットで挿入するのかといったことを細かく指定することができます。

ただ、 str.format() は機能が豊富なので、一気に覚えるのではなく、まず「 format() でどういうことができるのか」だけ把握しておいて、具体的なオプションの指定方法については必要になったときに都度覚えていくのがよいかと思います。

詳しくは公式のドキュメントを参考にしてください。

6.1. string — Common string operations — Python documentation
6.1. string — 一般的な文字列操作 — Python ドキュメント


フォーマットつき文字列リテラル


Python 3.6 以降に限定とはなりますが、 f'' という形式で記述するフォーマットつき文字列リテラルというものも利用できます。

first = '奥山に紅葉踏み分け鳴く鹿の'
second = '声聞く時ぞ秋は悲しき'

tanka = f'{first}\n  {second}'

print(tanka)
# => 奥山に紅葉踏み分け鳴く鹿の
# =>   声聞く時ぞ秋は悲しき

str.format() よりも直感的な形で文字列の中に変数を組み込むことができます。

このフォーマット付きの文字列リテラルに対応する PEP は 498 です。興味のある方は以下のページなどもご覧になってみるとよいかもしれません。

PEP 498 -- Literal String Interpolation | Python.org

2017/06/16

ライブラリ: attrs

Python のパッケージ attrs をご紹介します。

import attr

attrs はカスタムクラスを作成するときのマジックメソッドの記述を省略できる機能を提供するライブラリです。具体的には、クラスのアトリビュート(プロパティ)とイニシャライザ、その他いくつかのマジックメソッドの定義を省略することができます。

名前がよく似た attr というパッケージもあります。今回取り上げるのはそれではなく末尾に s がついた attrs の方なのでご注意ください。

こちらです。

- attrs : Python Package Index

こちらではありません。

- attr : Python Package Index

attrs も attr もコード内では s のつかない import attr でインポートする点は共通なので注意が必要です。

インストール


インストールには pip を使いましょう。

pip install attrs

上述のとおり attrs の末尾の s は必要なのでご注意ください。

使い方


サンプルコードを見ながら使い方を見ていきましょう。

import attr
    
@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib()

クラス Order の定義に @attr.s と attr.ib() という 2 つのものが使われています。これで Order クラスに id と created_at という 2 つのアトリビュートが追加されました。また、イニシャライザの引数に id と created_at が渡せるようになりました。

試しに Order インスタンスを作ってみましょう。

o1 = Order(5, 1497500000)
print(o1)
# => Order(id=5, created_at=1497500000)
print(o1.id)  # => 5
print(o1.created_at)  # => 1497500000

第 1 引数が id に、第 2 引数が created_at にそれぞれ渡されていることが確認できます。 `__init__()` をまったく書いていないのにこの挙動。これは attrs が裏側でよきようにやってくれているからです。

インスタンスを print() に渡したときの表示もきれいになっていることに注目してください。こちらも attrs の機能で、裏側でよきようにやってくれているためです。

引数はキーワード指定で渡すことも可能です。

o2 = Order(id=5, created_at=1497500000)
print(o2)
# => Order(id=5, created_at=1497500000)

attr.ib() で定義されたアトリビュートをイニシャライザに渡さないとどうなるでしょうか。

o2 = Order()  
# => # TypeError: __init__() missing 2 required positional arguments: 'id' and 'created_at'

TypeError が出ました。

attr.ib() の引数に default を指定するとそのアトリビュートのデフォルト値を設定することができます。デフォルト値が設定されたアトリビュートはイニシャライザの必須から外れます。

import attr

@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib(default=0)

o4 = Order(id=10)
print(o4)
# => Order(id=10, created_at=0)

デフォルト値はファクトリ機能を使って動的な値にすることもできます。

from datetime import datetime
import attr

@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib(default=attr.Factory(datetime.now))

o5 = Order(id=15)
o6 = Order(id=20)
print("{}\n{}".format(o5, o6))
# => Order(id=15, created_at=datetime.datetime(2017, 6, 15, 7, 1, 29, 262216))
# => Order(id=20, created_at=datetime.datetime(2017, 6, 15, 7, 1, 29, 262274))

また、 attrs を使って作られたクラスのインスタンスは比較演算子で比較できるようになります。これは attrs の機能を使ってクラスを書くと attrs が比較系のマジックメソッドを自動で登録してくれるためだそうです。

from datetime import datetime
import attr

@attr.s
class Order:
    id = attr.ib()
    created_at = attr.ib(default=attr.Factory(datetime.now))

o7 = Order(25)
o8 = Order(23)
print(o7 < o8)  # => False

now = datetime.now()
o9  = Order(100, now)
o10 = Order(100, now)
print(o9 == o10)  # => True
print(o9 is o10)  # => False

各アトリビュートにはバリデーションロジックをつけることもできます。

from datetime import datetime
import attr
from attr.validators import instance_of

@attr.s
class Order:
    id = attr.ib(validator=instance_of(int))
    created_at = attr.ib(default=attr.Factory(datetime.now))

o11 = Order('15')
# => TypeError

バリデーションはインスタンスの生成時に加えてその他のタイミングでも行うことができます。

o12 = Order(50)
o12.id = 'invalid'
attr.validate(o12)
# => TypeError

シンプルなコードを書くだけで各種マジックメソッドが自動的に定義されるので少し Explicit ではない感じもしますが、ほぼ定型のコードを毎度書くのは少しわずらわしかったりもするので、 attrs を使ってこのあたりが楽できるのはよいかもしれません。

他にもオリジナルのバリデータを指定したりなどさまざまなことができるので、興味のある方は公式のドキュメントをご覧になってみてください。

以上です。

参考

Using attrs for everything in Python | Hacker News
Deciphering Glyph :: The One Python Library Everyone Needs

公式

attrs: Classes Without Boilerplate — attrs documentation
python-attrs/attrs: Python Classes Without Boilerplate

2017/06/06

Python Tips:標準入力がどのように渡されているのかをチェックしたい

今回は Python で標準入力を扱う際に標準入力がどのように渡されているのかをチェックする方法についてご紹介したいと思います。

標準入力の渡し方は大きく分けて、ファイルからのリダイレクトやパイプによって渡す場合とキーボードからインタラクティブに渡す場合の 2 通りに分けることができます。

例えば、リダクレクトやパイプのときにだけ処理を行いたいような場合は次のようなコードを書くことになります。

import sys

if (標準入力がキーボードから渡されている):
    sys.stderr.write("キーボードからの標準入力には対応していません。\n")
    exit()

(やりたい処理)

ここで「(標準入力がキーボードから渡されている)」のところは具体的にどのように書けばいいのでしょうか。早速結論ですが、こちらは sys.stdin の isatty() メソッドを使えば OK です。

import sys

if sys.stdin.isatty():
    sys.stderr.write("キーボードからの標準入力には対応していません。\n")
    exit()

isatty() は読んでそのまま「 is a tty 」の意味らしく、標準入力がキーボードからの入力の場合(あるいは標準入力に何も渡されていない場合)は True を返します。ファイルのリダイレクトやパイプの場合には False を返します。

サンプルを見てみましょう。

check_stdin.py:

# coding: utf-8
import sys

if sys.stdin.isatty():
    sys.stderr.write('パイプあるいはリダイレクトで標準入力を渡してください。\n')
else:
    sys.stderr.write('標準入力をそのまま標準出力に流します。\n')
    sys.stdout.write(sys.stdin.read())

こちらを使うと次のようになります。

$ python check_stdin.py
パイプあるいはリダイレクトで標準入力を渡してください。
$ echo 'hello' | python check_stdin.py
hello
標準入力をそのまま標準出力に流します。

標準入力の渡し方を識別できていることがわかります。

以下はもう少し実用的なサンプルで、 csv 形式のテキストを標準入力から受け取る例です。

read_csv.py:

# coding: utf-8

"""標準入力から csv を読む
"""

import sys
import csv
from itertools import islice


def main():
    """標準入力で与えられた csv を読み込む

    - 最初の 5 行だけ、ヘッダーなしで読み込む
    """
    rows, header = csv_read_stdin(5, True)

    print("Rows: {}".format(list(rows)))


def csv_read_stdin(number, is_headerless):
    """標準入力から csv を読み込む
    """
    if sys.stdin.isatty():
        sys.stderr.write("標準入力はパイプまたはリダイレクトで渡してください。\n")
        exit()
    reader = csv.reader(sys.stdin)
    header = [] if is_headerless else next(reader)
    rows = islice(reader, number)

    return rows, header


if __name__ == "__main__":
    main()

試してみます。

$ python read_csv.py
標準入力はパイプまたはリダイレクトで渡してください。
$ python read_csv.py <<EOS
> Takeda,Shingen
> Takeda,Katsuyori
> EOS
Rows: [['Takeda', 'Shingen'], ['Takeda', 'Katsuyori']]

正しく識別できています。

以上です。シンプルでわかりやすいですねー。

2017/05/08

ライブラリ: argparse

Python のライブラリ argparse をご紹介したいと思います。


概要


argparse は「 arg(ument) + parse 」という名前のとおり、コマンドライン引数を管理するためのライブラリです。

import argparse

公式ページでは次のような説明がなされています。

The argparse module makes it easy to write user-friendly command-line interfaces. The program defines what arguments it requires, and argparse will figure out how to parse those out of sys.argv. The argparse module also automatically generates help and usage messages and issues errors when users give the program invalid arguments.

意訳: argparse モジュールを使うと、ユーザーフレンドリーなコマンドラインインタフェースがかんたんに作れます。プログラムがどのような引数を取るのかを定義しておけば、 argparse はそれらを sys.argv からパースする方法を理解してくれます。また、プログラムのヘルプや使い方のメッセージを生成したりユーザーが不正な引数を渡したときにエラーを出したりといったことも自動的に行ってくれます。

argparse — Parser for command-line options, arguments and sub-commands — Python documentation

Python でコマンドライン引数といえば sys.argv ですが、 sys.argv はあくまでもコマンドライン引数をそのまま格納しただけのリストです。 argparse は引数の取得だけでなく、引数の検証や制限、自動変換、フォールバック、ヘルプの生成など引数の取り扱いに関する一連の便利機能を提供してくれます。

私は食わず嫌いで argparse を触らなかった時期が長いのですが、その使いやすさを知ったときは「もっと早く興味を持っていれば!」と残念がりました。今は Python でちょっとしたコマンドラインツールを作るときにはほぼ毎回といっていいほどよく利用しています。


インストール


インストール方法についてです。といっても、 argparse は Python の 2.7 、 3.2 以降は標準ライブラリとして Python 本体に同梱されています。 pip などで別途インストールする必要はありません。


使い方


基本的な使い方を見ていきましょう。

まずは最もシンプルなサンプルコードから。

argparse_sample_1.py:

# coding: utf-8

import argparse
from pathlib import Path

"""ファイルのメタ情報を確認する

usage:

    $ python argparse_sample_1.py sample.txt
"""

def main():
    """本スクリプトの main
    """
    # 引数を取得する
    args = get_args()

    # 引数のうち file の部分を取得してそのメタ情報を出力する
    path = Path(args.file)
    print(path.lstat())


def get_args():
    """ファイル名をコマンドライン引数から取得する
    """
    parser = argparse.ArgumentParser('Show file meta info.')
    parser.add_argument('file', help='Target file.')

    return parser.parse_args()


if __name__ == '__main__':
    main()

argparse とは別に利用されているもうひとつのライブラリ pathlib はファイルシステムを扱うための標準ライブラリです。ここでの説明は割愛します。興味のある方は公式ページなどをご覧になってみてください。

pathlib — オブジェクト指向のファイルシステムパス — Python ドキュメント


肝心の argparse についてです。 argparse は関数 get_args() の中で利用されています。

    parser = argparse.ArgumentParser('Show file meta info.')

まずは ArgumentParser() で ArgumentParser オブジェクトを生成しています。引数の 'Show file meta info.' というのはプログラム名です。このプログラム名は省略することが可能で、省略するとスクリプト名になります(この場合は argparse_sample_1.py )。

    parser.add_argument('file', help='Target file.')

つづいて ArgumentParser オブジェクトの add_argument() メソッドでコマンドライン引数の定義を追加しています。ここで、 add_argument() の第 1 引数 'file' は引数名です。第 2 引数の help はヘルプテキストです。こちらはコマンドのヘルプテキストにおいて引数の説明文となります。

    return parser.parse_args()

最後に parse_args() メソッドで引数の取得処理( sys.argv のパース)を行っています。

続く関数 main() の中では parse_args() の戻り値を利用しています。この戻り値は argparse.Namespace というクラスのオブジェクトで、パースされた引数をプロパティとして格納しています。 add_argument('file', help='Target file.') で定義された引数には args.file でアクセスすることができます。

ここまでで見たとおり、 argparse の基本は次の 4 ステップになります。

1. ArgumentParser オブジェクトの生成
2. 引数定義の追加
3. 引数のパース
4. 引数の利用

このスクリプトファイル argparse_sample_1.py はコマンドラインから以下のような形で利用することができます。

引数なしで利用:

$ python argparse_sample_1.py
usage: Show file meta info. [-h] file
Show file meta info.: error: the following arguments are required: file

引数を渡して利用:

$ python argparse_sample_1.py sample.txt
os.stat_result(st_mode=33188, st_ino=54935441, st_dev=16777220, st_nlink=1, st_uid=501, st_gid=20, st_size=559, st_atime=1493990821, st_mtime=1493990817, st_ctime=1493990817)

help オプションをつけて利用:

$ python argparse_sample_1.py --help
usage: Show file meta info. [-h] file

positional arguments:
  file        Target file.

optional arguments:
  -h, --help  show this help message and exit

引数 file が正しく渡されなかった場合は使用方法とエラーが表示され、その後の処理は行われません。引数が正しく渡されたときにだけ、その後の処理が実行されるようになります。

また、 --help オプションが自動的にサポートされるようになりました。

argparse_sample_1.py は引数を 1 つだけ取る最もシンプルな例でした。もう少し長い例を見てみましょう。

argparse_sample_2.py:

# coding: utf-8

import argparse

def main():
    """引数をチェックする
    """
    args = get_args()

    print(args)
    print(args.format)
    print(args.encode)
    print(args.verbose)
    print(args.max_lines)


def get_args():
    """コマンドライン引数を取得する
    """
    parser = argparse.ArgumentParser('diff csv files.')
    parser.add_argument('-f', '--format', dest='format', default='csv', choices=('csv', 'tsv'), help='file format (default: csv).')
    parser.add_argument('-e', '--encode', dest='encode', default='utf8', choices=('utf8', 'sjis'), help='file encoding.')
    parser.add_argument('-v', '--verbose', action='store_true')
    parser.add_argument('-m', '--max-lines', type=int, default=0)
    parser.add_argument('files', nargs=2, type=argparse.FileType('r'), help='2 csv files.')

    return parser.parse_args()


if __name__ == '__main__':
    main()

こちらは私が以前作成した「 2 つの csv ファイルを比較するコマンドラインツール」から引数の部分のみを抽出したものです。

argparse_sample_1.py よりも add_argument() メソッドの呼び出し行が増えたというちがいはあるものの、次の 4 ステップは共通しています。

1. ArgumentParser オブジェクトの生成
2. 引数定義の追加
3. 引数のパース
4. 引数の利用

add_argument() の行を順番に見ていきましょう。

parser.add_argument('-f', '--format', dest='format', default='csv', choices=('csv', 'tsv'), help='file format (default: csv).')

この行では --format というオプション引数を定義しています。

'-f' と '--format' はそのオプション引数の指定パターンを指定しています。 dest='format' は parse_args() の戻り値となる argparse.Namespace オブジェクトにおいて当該引数を格納するプロパティ名を指定しています。ですので、こちらは args.format で取得できることになります。 default='csv' は --format オプションが指定されなかった場合のデフォルト値を、 choices=('csv', 'tsv') は受け付け可能なパターンを指定しています。ですので、 --format に指定できる値は 'csv' と 'tsv' のどちらかに絞りたくて、一切指定しなかった場合の値は 'csv' にフォールバックしてほしい、という意味合いになります。

parser.add_argument('-e', '--encode', dest='encode', default='utf8', choices=('utf8', 'sjis'), help='file encoding.')

続くこの行では --encode というオプション引数を定義しています。引数のパターンは --format の場合と同じなので説明は不要でしょう。こちらで対象ファイル読み込み時の文字コード指定ができるようになりました。

parser.add_argument('-v', '--verbose', action='store_true')

こちらは --verbose というオプション引数を定義しています。 action='store_true' で、オプションが指定された場合は True を、指定されなかった場合は False がその値となります。 action のパターンとしてはこの他に 'store' 'store_const' 'store_true' 'store_false' 'append' などさまざまなものが用意されています。

parser.add_argument('-m', '--max-lines', type=int, default=0)

この行は --max-lines というオプションを定義しています。 type=int でそのオプションに渡せる値が int 型限定であることを表しています。ここに int 型にならない値を渡すと、わかりやすいメッセージを伴ってエラーが上がります。

parser.add_argument('files', nargs=2, type=argparse.FileType('r'), help='2 csv files.')

最後のこの行は実際の対象ファイルを指定するための引数を定義しています。 nargs は引数の数を指定するものです。ここでは 2 つのファイルを比較したいので 2 を指定しています。 nargs には、正の整数の他に '?' ( 0 個または 1 つ)や '*' ( 0 個以上)、 '+' ( 1 つ以上)、 argparse.REMAINDER (残り全部)などを指定することができます。

以上です。

こちらのスクリプトに --help オプションを指定して実行すると次のような出力が表示されます。わかりやすいですね。

$ python argparse_sample_2.py --help
usage: diff csv files. [-h] [-f {csv,tsv}] [-e {utf8,sjis}] [-v]
                       [-m MAX_LINES]
                       files files

positional arguments:
  files                 2 csv files.

optional arguments:
  -h, --help            show this help message and exit
  -f {csv,tsv}, --format {csv,tsv}
                        file format (default: csv).
  -e {utf8,sjis}, --encode {utf8,sjis}
                        file encoding.
  -v, --verbose
  -m MAX_LINES, --max-lines MAX_LINES

以上です。

規模の大きなコマンドラインツールを開発する場合には argparse 以外のオプションを検討した方がよいような気がしますが、ちょっとしたツールを作りたい場合であれば必要なインタフェースは argparse でほぼほぼまかなえるように思います。

より詳しく知りたい方は公式ページなどをご覧になってみてください。


参考


Argparse チュートリアル — Python ドキュメント
Cool Python Tips: コマンドラインアプリにはargparse

2017/04/26

Python Tips: GetText (.po) ファイルの要素を抽出したい

Python で、 GetText (.po) ファイルの要素を抽出する方法をご紹介します。

「 GetText って何?」という方は Wikipedia を参考になさってみてください。

- gettext - Wikipedia

pip パッケージのひとつに polib というものがあり、こちらを使うと GetText (.po) ファイル(以下 .po ファイル)を Python でシンプル・かんたんに扱うことができます。


インストール


インストールにはおなじみ pip コマンドを使用しましょう。

pip install polib


使い方


.po ファイルの読み込みには pofile() 関数を使います。

import polib

po = polib.pofile('path/to/catalog.po')

作成日や作成者を含むメタデータは metadata プロパティに格納されています。

print(po.metadata)

.po ファイルに含まれる各翻訳テキストは POFile オブジェクトをイテレータとして使用すると取得することができます。

for entry in po:
    print('{}: {}'.format(entry.msgid, entry.msgstr))

有効な翻訳文を持つ翻訳テキストのみに限定して取得したい場合は translated_entries() メソッドが便利です。

for entry in po.translated_entries():
    print('{}: {}'.format(entry.msgid, entry.msgstr))

以上です。

ここでご紹介したのは .po ファイルの要素を抽出する方法だけですが、 polib では他にも「要素の追加」「 .po ファイルの新規作成」「 .po ファイルの更新」などひととおりの処理がサポートされています。興味のある方は公式の Quick start guide をご覧になってみてください。

Quick start guide — polib documentation

私の場合は「巨大な .po ファイルのうち一部の要素を切り出して小さな .po ファイルを作りたい」という要望があり、 .po ファイルからの要素抽出スクリプトを作成するのに使用しました。興味のある方は次の gist の方も参考にしてみてください。



参考

- polib : Python Package Index
- Welcome to polib’s documentation! — polib documentation

2017/04/02

Python Tips:特定のサイズ以上のファイルを検索したい

バタバタしており久しぶりの投稿になってしまいました。

今回は Python で指定されたサイズ以上のファイルを検索する方法をご紹介します。

これを実現するアプローチとしてはいくつかの方法が考えられるかと思いますが、今回は標準ライブラリの pathlib を使った方法をご紹介してみたいと思います。

from pathlib import Path

pathlib の Path クラスを使えば OOP スタイルで OS のファイルシステムを操作することができます。

Path クラスの数あるメソッドのうち次の 4 つほどをおさえておけば、ファイルを探す処理の実装には十分でしょう。

- iterdir() ディレクトリの中にあるファイル / ディレクトリを全件返す
- is_dir() ディレクトリかどうかをチェックする
- is_file() ファイルかどうかをチェックする
- stat() ファイルサイズを含むファイルのメタ情報を返す

サンプルコードを書いてみます。

# coding: utf-8
from pathlib import Path

def search_files(path, size_min_in_byte):
    """指定されたパスの下にある指定されたサイズ以上のファイル名を一覧表示する
    """
    size_min_in_mb = size_min_in_byte << 20

    p = Path(path)

    # 指定されたパス以下のファイルを再帰的にチェックする
    # 指定されたサイズ以上のファイルは「 10MB  ファイル名」といった感じに表示する
    for file in p.iterdir():
        if file.is_dir():
            search_files(file, size_min_in_byte)
        elif file.is_file():
            size = file.stat().st_size
            if size >= size_min_in_mb:
                # resolve() を使って絶対パスを表示する
                print('{:.1f}MB\t{}'.format(size >> 20, file.resolve()))


if __name__ == '__main__':
    # hayato のデスクトップ以下にあるサイズが 1MB 以上のファイルを表示する
    path = '/Users/hayato/Desktop'
    size_in_mb = 2
    search_files(path, size_in_mb)


ファイル名を script_name.py として保存しターミナルで実行してみましょう。私の環境では例えば次のような出力が出ます。

$ python script_name.py
3.5MB /Users/hayato/Desktop/山月記.md
2.2MB /Users/hayato/Desktop/弟子.md
2.7MB /Users/hayato/Desktop/李陵.md

これはちょうど、 find コマンドでファイルサイズを指定した場合と同じような結果になります。

find /Users/hayato/Desktop -type f -size +2M

ここでは単純にサイズとファイル名を標準出力に返していますが、対象のファイルそれぞれに対して特定の処理を行いたい場合などは print 文の前後を必要な処理に差し替えるとよいものと思います。

以上です。

例えば、コマンドライン引数をかんたんに扱うための標準ライブラリ argparse といっしょに使うともう少し汎用性の高いスクリプトを作ることができます。興味のある方は次のスニペットもよろしければ参考にしてみてください。