2018/09/10

Python のアトリビュート取り扱いの仕組み

Python で オブジェクトのアトリビュートへのアクセスがあったときに内部で起こっていること について説明してみます。

# オブジェクトのアトリビュートへのアクセスがあると・・・?
obj.attr1

私は他の言語においてこのあたりの仕組みをよく理解してないため厳密な比較はできませんが、 Python のこの仕組みはとてもユニークでおもしろいと思います。

早速説明していきます。馴染みの無い方にとっては少し複雑なので、ざっくりとした説明から始めて徐々により詳細で厳密な説明へと進んでいきます。

目次


  • レベル 1: 基本 1
  • レベル 2: 基本 2
  • レベル 3: 基本 3
  • レベル 4: 発展 1
  • レベル 5: 発展 2
  • レベル 6: 発展 3


レベル 1: 基本 1


アトリビュートへのアクセスがあると、オブジェクト固有のデータ保持領域で要素が探索される。要素が存在すればその値が返され、存在しなければ AttributeError があがる。

これは標準的なオブジェクト指向の概念に慣れている方にとっては直感的な挙動ですね。

「オブジェクト固有のデータ保持領域」というのは、具体的には各オブジェクトに備わった __dict__ アトリビュートのことを指しています。 __dict__ はデフォルトでは空の dict です。

コードで確認してみましょう。

class A:
    pass

a1 = A()
# アトリビュートがセットされなければ、 __dict__ の初期状態は空の dict
print(a1.__dict__)
# => {}

# アトリビュートへのアクセスがあると __dict__ 内の該当する要素が返される
a1.__dict__['attr1'] = 10
print(a1.attr1)
# => 10

# __dict__ 内に該当する要素がなければ AttributeError があがる
a2 = A()
print(a2.attr1)
# => AttributeError

a1 については、あらかじめ __dict__attr1 というキーで要素を格納したあとに a1.attr1 にアクセスしています。 a1.attr1 にアクセスすると a1.__dict__['attr1'] の値が返されることが確認できています。

一方、 a2 では前準備などせずすぐに a2.attr1 にアクセスしています。結果、例外 AttributeError があがります。

まずはこれが基本です。

レベル 2: 基本 2


レベル 1 の説明を少し更新します。

アトリビュートへのアクセスがあると、オブジェクト固有のデータ保持領域で要素が探索される。要素が存在すればその値が返される。 存在しなければ、オブジェクトのクラスが __getattr__ メソッドを持っているかどうかがチェックされる。持っていればそれが呼び出され、その戻り値が返される。 持っていなければ AttributeError があがる。

強調部分がレベル 1 との違いです。

レベル 1 では「オブジェクト固有のデータ保持領域に該当する要素が存在しなければ AttributeError があがる」と説明しましたが、実はその間にはプログラマが自由に処理をはさめるようになっていて、そのための仕組みが __getattr__ メソッドです。

コードで確認してみましょう。

class B:
    def __getattr__(self, name):
        return name

# __dict__ 内に該当する要素がなくて、クラスが __getattr__ を定義していればその戻り値が返される
b1 = B()
print(b1.attr1)
# => 'attr1'

a1 のところで見たとおり、コンストラクタで何もせず単純にオブジェクトを生成すると、そのオブジェクトの __dict__ は空の dict となります。 b1__dict__ は空なので、キー attr1 に対応する要素は存在しません。結果、 __getattr__ の呼び出しが発生し、その戻り値が返されます。

__getattr__ の引数 name には オブジェクト.アトリビュートアトリビュート に相当する文字列が渡されます。つまり、 b1.attr1 が実行された場合の name には文字列 'attr1' が格納されています。 B__getattr__ は戻り値として name をそのまま返しているので、結果として b1.attr1 にアクセスすると文字列 'attr1' が返ってきます。

ここでは説明のために __getattr__name を返す単純は実装にしていますが、実践的なコードではここにさまざまな工夫を加えます。例えば次のようにすると、データ保持領域の要素を int に変換して返させることができます。

class B2:
    def __getattr__(self, name):
        # 実在するアトリビュートの後ろに _as_int をつけた名前に対応する
        if name.endswith('_as_int'):
            stripped = name[:-len('_as_int')]
            if stripped in self.__dict__:
                return int(self.__dict__[stripped])
        raise AttributeError()

b2 = B2()
b2.pi = 3.14
b2.radius = 5.25

# __getattr__ の戻り値が返される
print(b2.pi_as_int)
# => 3
print(b2.radius_as_int)
# => 5

レベル 3: 基本 3


アトリビュートへのアクセスがあると、オブジェクト固有のデータ保持領域で要素が探索される。要素が存在すればその値が返される。存在しなければ、 オブジェクトのクラスのデータ保持領域で要素が探索される。存在しなければ、継承をたどってすべての親のデータ保持領域で要素が探索される。それでも存在しなければ、 オブジェクトのクラスが __getattr__ メソッドを持っているかどうかがチェックされる。持っていればそれが呼び出され、その戻り値が返される。持っていなければ AttributeError があがる。

強調部分がレベル 2 との違いです。

レベル 2 では「オブジェクトのデータ保持領域で対応する要素が見つからなかった場合は __getattr__ メソッドを持っているかどうかがチェックされ・・・」と説明しましたが、実は、「オブジェクトのデータ保持領域に要素が見つからなかった」と「 __getattr__ メソッドを持っているかどうかがチェックされ」の間に、クラスのデータ保持領域での要素の探索が発生します。

コードで確認してみましょう。

class C:
    attr1 = 10

    def __getattr__(self, name):
        return name

# クラスアトリビュートは __dict__ に格納される
print(C.__dict__['attr1'])
# => 10

c1 = C()

# クラス C の __dict__['attr1'] が返される
print(c1.attr1)
# => 10

# __getattr__ の戻り値が返される
print(c1.attr2)
# => 'attr2'

オブジェクト c1 において c1.attr1 にアクセスすると、クラス C で定義されたアトリビュート attr1 の値が返されました。これは内部的には C のデータ保持領域である C.__dict__ に格納された値です。クラスのデータ保持領域に該当する要素が見つかった場合は、メソッド __getattr__ の呼び出しは発生しません。

このサンプルでは示されていませんが、クラスのデータ保持領域の探索はクラスの継承関係を辿って行われます。つまり、 C.__dict__['attr1'] が存在しなかった場合はその親クラスの __dict__['attr1'] が探索され、そこに無ければまた親の・・・と続いていきます。最終的な親である object.__dict__ まで探索しても要素が見つからなかった場合は、レベル 2 での説明のとおり __getattr__ へと処理が移っていきます。上の c1.attr2 でのアクセスではまさにこの流れを辿った末に値が返された結果 'attr2' という文字列が返ってきています。

内部的には __dict__ が介在していますが、表面的には単純に「オブジェクトに該当するアトリビュートがなければ、クラスの同名のアトリビュートにフォールバックする」という挙動になるので、このあたりはふだん利用するときには難しく考えなくても直感的に利用できるでしょう。

と、ここまでは他の言語でもわりとよく見られるパターンなので、何らかのプログラミング言語に馴染みのある方であればすんなり受け入れられるところではないかと思います。続いて、 Python の特徴である descriptor (ディスクリプタ)も含めた説明へと進みます。

レベル 4: 発展 1


アトリビュートへのアクセスがあると、オブジェクト固有のデータ保持領域で要素が探索される。要素が存在すればその値が返される。存在しなければ、オブジェクトのクラスのデータ保持領域で要素が探索される。 該当する要素が存在した場合、それが __get__ メソッドを備えていれば __get__ メソッドが呼ばれ、その戻り値が返される。 __get__ メソッドを備えていなければそのオブジェクトそのものが返される。 同様の探索が、継承をたどってすべての親のデータ保持領域で行われる。それでも存在しなければ、オブジェクトのクラスが __getattr__ メソッドを持っているかどうかがチェックされる。持っていればそれが呼び出され、その戻り値が返される。持っていなければ AttributeError があがる。

強調部分がレベル 3 との違いです。ここで新たに __get__ メソッドというものが出てきました。

レベル 2 で述べたとおり、オブジェクトのアトリビュートへのアクセスが起こったときに、オブジェクトそのもののデータ保持領域 __dict__ に該当する要素がなければ、クラスのデータ保持領域 __dict__ での探索が行われます。その際、ふつうはその要素(=オブジェクト)そのものが返されるのですが、それが __get__ メソッドを持っている場合にかぎり、 __get__ メソッドが実行され、その戻り値が返されます。

ことばでの説明だけだと意味が分かりづらいですね。コードで確認してみましょう。

class Descriptor1:
    def __init__(self, name):
        self._name = name

    def __get__(self, instance, owner):
        print(self, instance, owner)
        return '{}.__get__ for {}'.format(self.__class__.__name__, self._name)

class D:
    attr1 = Descriptor1(name='attr1')
    attr2 = 10

d1 = D()

# Descriptor1 の __get__ メソッドの戻り値が返される
d1.attr1
# => 'Descriptor1.__get__ for attr1'

# __get__ を持たないアトリビュートの場合は値がそのまま返される
d1.attr2
# => 10

d1.attr1 にアクセスすると 'Descriptor1.__get__ for attr1' という文字列が返ってきます。これは Descriptor1 で定義されているメソッド __get__ の戻り値です。

通常、オブジェクトのアトリビュートへのアクセスでクラスのアトリビュートの参照が発生するとその値がそのまま返されますが、そのアトリビュートの値が __get__ メソッドを持っている場合にかぎり __get__ メソッドが実行されその戻り値が返されます。

これが Python のいわゆる descriptor です。 Python の descriptor とは「そのインスタンスが他のクラスのアトリビュートとして利用されたときに特殊な挙動をするクラス」です。

descriptor プロトコルを構成するメソッドは __get__ の他に __set____delete__ があります。

ちなみに、上の Descriptor1__init__ メソッドは、 descriptor オブジェクト自身がアトリビュート名 を知れるように次の形で利用するためのものです。

attr1 = Descriptor1(name='attr1')

Python 3.6 で __set_name__ という特殊メソッドが追加され、 Python 3.6 以降では descriptor オブジェクト自身がアトリビュート名をかんたんに知れるようになりました。

class Descriptor1:
    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        print(self, instance, owner)
        return '{}.__get__ for {}'.format(self.__class__.__name__, self._name)

class D:
    attr1 = Descriptor1()

d1 = D()

d1.attr1
# => 'Descriptor1.__get__ for attr1'

attr1 = Descriptor1() が実行されると __set_name__ が呼び出され引数 name にアトリビュート名が渡されるので、 descriptor 側でアトリビュート名を利用することができます。

descriptor のロジックはこれだけではありません。

レベル 5: 発展 2


アトリビュートへのアクセスがあると、 オブジェクトのクラスのデータ保持領域で要素が探索される。該当する要素が存在し、かつ、該当する要素が __get__ メソッドと __set__ メソッドを備えていれば __get__ メソッドが呼ばれその戻り値が返される。 そうでない場合は、オブジェクト固有のデータ保持領域で要素が探索される。要素が存在すればその値が返される。存在しなければ、オブジェクトのクラスのデータ保持領域で要素が探索される。該当する要素が存在した場合、それが __get__ メソッドを備えていれば __get__ メソッドが呼ばれ、その戻り値が返される。 __get__ メソッドを備えていなければそのオブジェクトそのものが返される。同様の探索が、継承をたどってすべての親のデータ保持領域で行われる。それでも存在しなければ、オブジェクトのクラスが __getattr__ メソッドを持っているかどうかがチェックされる。持っていればそれが呼び出され、その戻り値が返される。持っていなければ AttributeError があがる。

強調部分がレベル 4 との違いです。

レベル 4 までは「アトリビュートのアクセスがあるとオブジェクト固有のデータ保持領域で要素が探索される」と言っていましたが、実は、アトリビュートのアクセスがあったときに最初に行われることは、オブジェクトではなくクラスのデータ保持領域 __dict__ での探索です。そこに該当する要素があり、なおかつその要素が __get____set__ の 2 つのメソッドを備えていれば、その __get__ メソッドが呼ばれて戻り値が返されます。クラスのデータ保持領域に該当する要素がなかったり、あっても __get__ メソッド・ __set__ メソッドを備えていない場合は、通常どおりオブジェクトのデータ保持領域 __dict__ での探索が行われます。以降の処理はレベル 4 での説明のとおりです。

コードで確認してみましょう。

class Descriptor2:
    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        return '{}.__get__ for {}'.format(self.__class__.__name__, self._name)

    def __set__(self, instance, value):
        pass

class E:
    attr1 = Descriptor2()

e1 = E()
e1.__dict__['attr1'] = 10

# オブジェクトの __dict__ よりもクラスの __dict__ が優先される
print(e1.attr1)
# => 'Descriptor2.__get__ for attr1'

e1.attr1 にアクセスすると 'Descriptor2.__get__ for attr1' という文字列が返されました。これは Descriptor2__get__ の戻り値です。

ポイントは、 e1.__dict__ には attr1 というキーの要素があらかじめセットされているにもかかわらず Descriptor2__get__ が優先して呼び出されている点です。通常はオブジェクトそのものの __dict__ が先に探索されますが、クラスのデータ保持領域にある同名の要素が __get____set__ の 2 つのメソッドを備えている場合のみ、それが優先的に利用されます。

一見とてもトリッキーな動きですが、 Python がこの仕組みを用意してくれているおかげで、プログラマは「クラス定義時にそのオブジェクトの特定のアトリビュートを特別扱いする指示ができる汎用的な方法」を作ることができます。

この仕組みを利用したかんたんな例をあげてみます。

class TitleField:
    def __init__(self, length):
        self._len = length

    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, instance, owner):
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError('アトリビュート {} には文字列のみがセットできます。'.format(self.self._name))
        if len(value) > self._len:
            raise ValueError('アトリビュート {} の最大長さは {} です。'.format(self.self._name, self._len))
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]


class Article:
    title = TitleField(length=32)


a1 = Article()
a1.title = 10
# => ValueError: アトリビュート title には文字列のみがセットできます。


class Page:
    title = TitleField(length=20)


p1 = Page()
p1.title = 'Guardians of the Galaxy 2'
# => ValueError: アトリビュート title の最大長さは 20 です。

p1.title = 'Jurassic World'
print(p1.title)
# => 'Jurassic World'

クラス TitleField__get____set__ の 2 つのメソッドを持った descriptor クラスです。これを ArticlePage の 2 つのクラスで利用しています。

TitleField__set__ にバリデーション処理があるので、 ArticlePagetitle アトリビュートに文字列以外のオブジェクトや指定された長さよりも長い文字列をセットすることはできません。

このような処理は __setattr__ メソッドや property を使っても実装することができますが、 TitleField という独立したクラスに定義することによって、複数のクラス・複数のアトリビュートで使い回せるというメリットが生まれます。

尚、ここであげた TitleField の各メソッドの定義では descriptor として不十分なところがあります。 TitleField はあくまでも descriptor の少し実用的なイメージを示すためのサンプルなので、実際に descriptor クラスを書こうというときにはぜひ公式ドキュメントや詳しい書籍を参照してください。

書籍や記事でご存知の方にはおなじみですが、この __set__ メソッドを持つ desriptor を data descriptor (データ・ディスクリプタ) 、持たない descriptor を non-data decriptor (ノンデータ・ディスクリプタ) と呼びます。私は「 non-data 」を日本語で書く場合は「ノンデータ」とカタカナで書くのが好みですが、 non-data descriptor は「非データ・ディスクリプタ」と訳されているのをよく目にします。

この data decriptor ・ non-data descriptor という概念を使って見ると、レベル 4 でのディスクリプタの呼び出しタイミングは non-data descriptor の挙動の説明で、レベル 5 の「オブジェクトの __dict__ の前にクラスの __dict__ が参照される」は data descriptor の挙動の説明でした。

ここまででお腹いっぱいになりそうですが、もうひとレベルあります。次のレベルが最後です。

レベル 6: 発展 3


アトリビュートへのアクセスがあると、 真っ先にメソッド __getattribute__ が呼ばれる。オブジェクトのクラスとその先祖クラスで __getattribute__ を定義しているものがなければ、基底クラス object__getattribute__ が呼ばれる。その中で以下の処理が行われる。

まずは、オブジェクトのクラスのデータ保持領域で要素が探索される。該当する要素が存在し、かつ、該当する要素が __get__ メソッドと __set__ メソッドを備えていれば __get__ メソッドが呼ばれ、その戻り値が返される。そうでない場合は、オブジェクト固有のデータ保持領域で要素が探索される。要素が存在すればその値が返される。存在しなければ、オブジェクトのクラスのデータ保持領域で要素が探索される。該当する要素が存在した場合、それが __get__ メソッドを備えていれば __get__ メソッドが呼ばれその戻り値が返される。 __get__ メソッドを備えていなければそのオブジェクトそのものが返される。同様の探索が、継承をたどってすべての親のデータ保持領域で行われる。それでも存在しなければ、オブジェクトのクラスが __getattr__ メソッドを持っているかどうかがチェックされる。持っていればそれが呼び出され、その戻り値が返される。持っていなければ AttributeError があがる。

強調部分がレベル 5 との違いです。

レベル 5 の説明は、実は object.__getattribute__ が呼ばれた後の処理の流れを説明したものです。プログラマがクラスで __getattribute__ を定義すると、この流れをカスタマイズすることができます。

ただ、 __getattribute__ を定義しないといけないようなケースというのは非常に稀だと思います。 __getattribute__ を上書きできる仕組みは用意されてはいるものの、独自の __getattribute__ はおそらくメリットよりも多くのデメリットをもたらすので、よほどのことでないかぎり __getattribute__ の上書きが必要なケースは無いでしょう。

ここまで来ると、 Python のアトリビュートアクセスの仕組みをある程度正確に把握したと言ってよいのではないでしょうか。

ここでは obj.attr1 という形でアトリビュートが「参照」されたときの処理の流れだけを説明しましたが、 obj.attr1 = ... という「代入」のときの流れや del obj.attr1 という「削除」のときの流れも同様にあります。上の説明の中に __set__ メソッド・ __delete__ メソッドへの言及が少しありましたが、これらが「代入」や「削除」のときの流れをコントロールするためのものになります。

というわけで、 Python のオブジェクトにおいてアトリビュートへのアクセスがあったときに起こる処理の流れ についてでした。

Python でコードを書くときにこのあたりのところをどこまで押さえておくべきか、についてですが、私は必ずしもすべて頭に入れておく必要は無いと思います。ひとまず基本的な使い方をするだけであればレベル 1 ・ 2 あたりを押さえておけば十分で、アトリビュート周りについて発展的な使い方をしたいときにレベル 3 ・ 4 を、 descriptor を活用したパッケージにコントリビュートしたり自身で descriptor を使ったパッケージを書いたりしたい場合に必要に応じて 5 ・ 6 までを押さえる、というのがよいかと思います。

興味がある方のご参考になれば幸いです :)

descriptor を深掘りしたくて実践的な例を見てみたい方は、 peewee 等の ORM マッパライブラリのコードを見られるとよいかと思います。

参考


2018/08/31

Python Tips: switch 文を使いたい

Python に似た言語にはよくあって Python に無いもののひとつに「 switch 文」があります。今回は switch 文を持たない Python において switch 文を書きたくなったときの代わりの書き方 をご紹介したいと思います。

おそらく Pythonista の多くが使っているのは次の 2 つの方法のどちらかです。

  • switch 文の代わり 1: ifelif
  • switch 文の代わり 2: dict

switch 文の代わり 1: ifelif


第一の方法は単純に ifelif 構文を使うというものです。

def printer_factory(name):
    if name == 'json':
        return JsonPrinter()
    elif name == 'yaml':
        return YamlPrinter()
    elif name == 'csv':
        return CsvPrinter()
    else:
        raise ValueError('Invalid printer: {}'.format(name))

この方法だと name == の部分を分岐の数だけ繰り返す必要がありますが、キーワード elif が短いおかげでわりとシンプルに書くことができます。

最後の else のところに来たときに例外をあげるかフォールバック値を返すかはそのときどきで適切な方を選ぶとよいでしょう。

switch 文の代わり 2: dict


もうひとつの代表的なアプローチは dict を使った方法です。

def printer_factory_改(name):
    printer_map = {
        'json': JsonPrinter,
        'yaml': YamlPrinter,
        'csv': CsvPrinter,
        'xml': XmlPrinter,
        'html': HtmlPrinter,
        'mild': MildPrinter,
        'wild': WildPrinter,
    }

    try:
        return printer_map[name]()
    except KeyError as e:
        raise ValueError('Invalid printer: {}'.format(name))

分岐を表すマップをあらかじめ定義しておき、辞書のキールックアップを使って分岐させます。

指定された値が存在しなかったとき(= switch 文で default に来たときに相当)の挙動として、例外をあげたいのであれば KeyError をキャッチして適切な例外をあげ直せば OK です。フォールバック値を返したければブラケット( [] ではなく get() メソッドを使ってデフォルト値を設定しながら値を返すとよいでしょう。

# 該当するものが見つからなかった場合はフォールバック値を返す
default_value = HtmlPrinter
return printer_map.get(name, default_value)()

こちらの方法で気をつけるべき点は、マップを作成するときに値を評価してしまわないことです。例えば上の printer_factory_改 は次のように書くこともできますが、こうするとマップを用意しているときにすべての Printer クラスのインスタンスが生成されてしまうのであまりよくありません。

def printer_factory_改悪(name):
    printer_map = {
        'json': JsonPrinter(),
        'yaml': YamlPrinter(),
        'csv': CsvPrinter(),
        'xml': XmlPrinter(),
        'html': HtmlPrinter(),
        'mild': MildPrinter(),
        'wild': WildPrinter(),
    }

    try:
        return printer_map[name]
    except KeyError as e:
        raise ValueError('Invalid printer: {}'.format(name))

Python ではクラスそのものもオブジェクトでありクラスを dict の値として格納することができるので、ファクトリクラスはできるだけ printer_factory_改悪 よりも printer_factory_改 の形で書くのがよいでしょう。

以上です。

ifelifdict のどちらを使うべきかはそのときの分岐の数や周辺のコード、コーディングルール等によって変わってくると思うので、そのときどきでより適切な方を選ぶとよいでしょう。

アーキテクチャの良し悪しの観点からいえば、 switch 文を使うべき場面は非常にかぎられてくるはずなので、 switch 文が無いということは Python らしいいい制約、なのかもしれません。

というわけで、 Python における「 switch 文の代わりの書き方」についてでした。

参考

Python 公式のドキュメントの FAQ に「なぜ Python には switch case が無いの?」という項目があるので、経緯等に興味がある方はそちらもご覧になってみるとよいかと思います。

2018/08/14

書籍紹介: Unity ゲーム プログラミング・バイブル

書籍『 Unity ゲーム プログラミング・バイブル 』の一部を出版社ボーンデジタルさんよりご恵贈いただきました。中身を読み実践してみたので、かんたんに書籍の紹介とレビューをしてみたいと思います。

書籍について


『 Unity バイブル プログラミングブックス 』はゲーム開発用ソフトウェア(=開発環境)である Unity でゲームを作る方法を解説した書籍です。サンプルを使った実践的な内容になっているので、読むタイミングとしては「入門書をひと通り終え Unity の基本的な使い方に慣れた後」に読むのが最適だと思います。




今回はその書籍のうち「 TensorFlow を組み合わせて AI プレイヤーを作る」というテーマの章(= Python が関わっている章)の抜粋をご恵贈いただきました。

※ Unity そのものは Python で利用するソフトウェアではありません

やったこと


書籍の内容をざっと読むだけではなかなか評価しづらいところがあるので、 Unity をインストールし、紹介されているプログラムを試しながら読んでみました。この文章を書いている時点で、ご提供いただいた章の内容の半分強ぐらいを動かしました。




出版社さんの公式ページでサンプルの Unity プロジェクト(≒サンプルコード)が公開されているので、それを利用するとコードを書かずともとりあえずプログラムを試すことが可能です。


ちなみに私は Unity / C# に不慣れなので、今のところコードはちらっと確認する程度で、ありもののファイルをほぼそのままの形で動かすだけで読み進めています。

感想


Unity を使ったゲーム作り、または、 Unity 縛りがなくても一般にゲーム作りに関心のある方にはとても興味深い内容ではないかと思います。画像が多く解説も丁寧なので、お勉強感覚ではなく雑誌感覚で楽しみながら読み進めることができます。

こんな方におすすめです。

  • プログラミングの基礎を学んだ後に、「何かおもしろいことがしたいな」と思っている方
  • Unity でどんなことができるのかを知りたい方
  • Unity の基礎を押さえた後に実践的な制作に取り組みたい方
  • ゲーム作りに興味がある方

プログラミングを日頃行っている方であれば高校生――もしかしたら中学生の方でも楽しく読めるかもしれません。

ちなみに、私の印象では、プログラミングがまったくわからないと、スムーズなときは別に問題ありませんが、書いてあることを Unity 上でどう実践すればよいかわからない場合や罠にハマった場合等、イレギュラーが起こったときに前に進めずにつらいと思います。なので、最低限「変数とは何ぞや」「関数とは何ぞや」「クラスとは何ぞや」ぐらいのベースの知識がある方が読むのがよいと思います。

いまひとつなところ

よかったところだけをあげるとアレなので、いまひとつなところをあげてみます。

書かれている操作を実際にどうやればよいのかわからない: これは GUI ソフトウェアの操作説明の常でもありますが、説明されている操作を実際にどうやればよいかわからないところがときどきありました。

概念と操作の説明が混ざっていて迷子になる: これも章によるかと思いますが、概念的な説明と具体的な操作の説明とが混ざっているところがあって、読んでいて「これは何の説明がされているんだろう」「この一連の操作をすると何が起こるんだろう」となることがありました。ただ、このあたりは Unity をよく知る読者であれば行間を読んでカバーできるところなのかもしれません。

よかったところ

画像が多くてわかりやすい: 画像が多めで細かなところにも丁寧な説明があったりするので、わかりやすかったです。文字ばかりのモノクロの技術書は読むのにエネルギーが要るので、「カラーの書籍はわかりやすくていいなぁ」と改めて思いました。

サンプルプロジェクトが公開されている: 上述のとおり、本書の中で紹介されているサンプルプロジェクト(≒サンプルコード)が公式サイト他で公開されています。「コードを書かずにサンプルをさっと動かしたい」「実際に動くプロジェクトを見て学びたい」というときにはとてもよいと思います。私が読んだのはごく一部ですが、書籍全体では数多くのサンプルが紹介されているので、それぞれからアイデアをもらうだけでちょっとしたオリジナルゲームを作ることが可能でしょう。

というわけで、書籍『 Unity ゲーム プログラミング・バイブル 』のご紹介でした。

私自身はゲーム作りはやったことがありませんがこれを機に少しやってみたくなりました。ゲームや Unity に興味のある方はぜひお手に取ってみてください :)

尚、最近同出版社さんが Unity の中でも特に「 Unity ✕ 機械学習 」というテーマに絞った書籍を出版されたそうです(具体的には Unity ML-Agents Toolkit を利用した Unity ✕ 機械学習の書籍です)。ゲーム作りに興味のある方にかぎらず、機会学習・ AI に興味のある方はチェックしてみるとよいかもしれませんよ。



2018/08/10

Python Tips: Enum 型を使いたい

Python で Enum 型(列挙型)を使う方法について、手短に説明してみます。

お断り: 以下に記載するコードについては動作確認はしていますが、私はたくさん Enum 型を使ってきたわけでは無いので、理解が間違っている可能性もあります。ご了承ください。

Python の Enum 型


Python では Python 3.4 から enum という標準モジュールが追加され、 Python 本体に Enum 型が同梱されるようになりました。

import enum

Enum 型の基本的な使い方


定義

オリジナルの Enum 型は enum.Enum 型を継承して作成します。

from enum import Enum

class Status(Enum):
    ACTIVE = 1
    INACTIVE = 0
    CANCELED = -1

アイテム(=インスタンス)はクラスアトリビュートとして定義します。このサンプルの場合は、 Status.ACTIVE Status.INACTIVE Status.CANCELED という 3 つのアイテムを定義していることになります。

ふるまい

Enum 型のふるまいを unittest で確認してみましょう。

from enum import Enum
from collections import OrderedDict
import unittest


class Status(Enum):
    ACTIVE = 1
    INACTIVE = 0
    CANCELED = -1


class TestStatus(unittest.TestCase):
    def test_types(self):
        # クラスのアトリビュートは自動的にインスタンスとして扱われる
        self.assertIsInstance(Status.ACTIVE, Status)
        self.assertIsInstance(Status.INACTIVE, Status)
        self.assertIsInstance(Status.CANCELED, Status)

    def test_for(self):
        # iterable プロトコルを持っているので for ループで回せる
        it = iter(Status)
        self.assertIs(next(it), Status.ACTIVE)
        self.assertIs(next(it), Status.INACTIVE)
        self.assertIs(next(it), Status.CANCELED)
        with self.assertRaises(StopIteration):
            next(it)

    def test_in(self):
        # iterable プロトコルを持っているので in 演算子も使える
        self.assertIn(Status.CANCELED, Status)

    def test_name_and_value(self):
        # キーと値はアトリビュート `name` と `value` で取得できる
        self.assertIs(Status.INACTIVE.name, 'INACTIVE')
        self.assertIs(Status.INACTIVE.value, 0)

    def test_accessors(self):
        # 各アイテムにアクセスする方法として次の 3 種類が要されてている
        self.assertTrue(Status['CANCELED'] == Status(-1) == Status.CANCELED)

    def test_inequality_with_value(self):
        # キーや値と勝手に等しくなったりはしない
        self.assertNotEqual(Status.CANCELED, 'CANCELED')
        self.assertNotEqual(Status.CANCELED, -1)

    def test_dunder_members(self):
        # クラスの __members__ アトリビュートには OrderedDict が入っている
        self.assertEqual(
            Status.__members__,
            OrderedDict(
                {
                    'ACTIVE': Status.ACTIVE,
                    'INACTIVE': Status.INACTIVE,
                    'CANCELED': Status.CANCELED,
                }
            ),
        )

なんだか直感的なふるまいをしてくれる印象がありますが、いかがでしょうか。 enum.Enum を使って定義できる Enum 型は私が Enum 型に期待する機能をひととおり揃えていてかつシンプルなので、私にはとても使いやすい印象です。

自動採番

Python 3.6 以降なら、 enum.auto() を使って各アイテムの値を自動でセットすることができます。

from enum import Enum, auto
import unittest


class Prefecture(Enum):
    HOKKAIDO = auto()
    AOMORI = auto()


class TestPrefecture(unittest.TestCase):
    def test_auto_values(self):
        # auto() を使うと 1 始まりで整数が自動的に振られる
        self.assertEqual(Prefecture.HOKKAIDO.value, 1)
        self.assertEqual(Prefecture.AOMORI.value, 2)

シンプル定義

Enum 型の定義方法としてメジャーなのはおそらく上の enum.Enum クラスを継承する方法ですが、次のように enum.Enum を関数のように使用してワンラインで Enum 型を定義する方法も用意されています。

PublishedStatus = Enum('PublishedStatus', 'PUBLISHED DRAFT')


class TestPublishedStatus(unittest.TestCase):
    def test_instances(self):
        self.assertIsInstance(PublishedStatus.PUBLISHED, PublishedStatus)
        self.assertIsInstance(PublishedStatus.DRAFT, PublishedStatus)

    def test_values(self):
        self.assertEqual(PublishedStatus.PUBLISHED.value, 1)
        self.assertEqual(PublishedStatus.DRAFT.value, 2)

この形で Enum 型を定義する場合は、 enum.Enum の第 1 引数に Enum 型の名前を、第 2 引数にアイテムをスペースや , で区切りの文字列で渡します。第 2 引数は listdict で渡すことも可能です。詳細は公式ページの Functional API の節でご確認ください。


というわけで、 Python の Enum 型の使い方についてでした。

シンプルな用途で Enum 型を使う場合は以上の内容を知っていれば十分ではないかと思いますが、その他細かな機能がもろもろ用意されているので、詳細を知りたい方は公式ページをご覧になってから使うのがよいかと思います。

参考

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

...

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

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

参考

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 コマンドで実行します。

$ 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 、と言うことができます。

さらにちなみに、 Python 3.3 でこの「 __init__.py が無いディレクトリが自動的にネームスペースパッケージになる」という仕組みが導入される前は、ネームスペースパッケージの仕組みそのものは存在していました。しかし、ネームスペースパッケージ化するには __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 のサブモジュールであるはずの childimport することができません。 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) は他のインポートシステムのパターンはカバーできていないため、厳密に言うならこれも不正確ということになるでしょう( b) よりも厳密な c) の説明ができるはずです)。

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

まとめ


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

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

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

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

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


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

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


参考