2018/11/10

Python Tips: unittest で例外のテストをしたい

今回は Python の標準ライブラリのひとつ unittest で例外関係の部分をテストする方法についてまとめてみました。

対象の Python のバージョンは Python 3.7 です。

import unittest

例外を含むパターンの説明に入る前に、かんたんに unittest の基本の使い方を見ておきましょう。 unittest の基本的な書き方は次のとおりです。

test_sample.py:
import unittest


class TestRange(unittest.TestCase):
    def setUp(self):
        super().setUp()

    def tearDown(self):
        super().tearDown()

    def test_length(self):
        self.assertEqual(len(range(5)), 5)

    def test_min_max(self):
        self.assertIn(0, range(10))
        self.assertIn(9, range(10))
        self.assertNotIn(10, range(10))

unittest.TestCase を継承したクラス TestXXX を作成し、名前が test で始まる名前のメソッドを作成しその中にテストケースを書きます。すると、各メソッドが独立したテストケースとなります。

setUp() は各テストケースの前に、 tearDown() は各テストケースの後に実行されます。

テストケースが書かれたファイルを実行するにはコマンド python -m unittest を使用します。

python -m unittest test_sample.py

上の test_sample.py に対して実行すると次のような出力が表示されます。

python -m unittest test_sample.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

デフォルトではテストケースの成功 1 件につき 1 つの . が出力されます。

-v オプションをつけることで、どのテストが実行されたかを確認することができます。

python -m unittest -v test_sample.py
test_length (test_exception.TestRange) ... ok
test_min_max (test_exception.TestRange) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

クラス単位・メソッド単位で絞り込んで個別のテストだけを実行することも可能です。

python -m unittest test_exception.TestRange.test_length
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

名前に特定の文字列を含むテストだけを絞って実行することなんかもできます。

python -m unittest test_exception -k min_max
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

本当にざっくりとだけですが、テストの書き方と実行方法を押さえました(以下のコードを手元で実行したい方等はご参考にしてください)。続いて本題である例外のテストの仕方を見ていきます。

1. 特定の例外があがることをテストする


特定の例外があがることをテストしたいときは unittest.TestCaseassertRaises() メソッドを使用します。 assertRaises() はコンテキストマネージャとして作られているので、 with キーワードとともに使用します。

import unittest


def periodic_table(sign):
    amap = {
        'H': '水', 'He': '兵', 
        'Li': 'リー', 'Be': 'ベ',
        'B': 'ぼ', 'C': 'く', 
        'N': 'の', 'O': 'ぉ', 
        'F': 'ふ', 'Ne': 'ね',
    }
    try:
        return amap[sign]
    except KeyError:
        raise ValueError('不正な元素記号が指定されました: {} 。'.format(sign))


class TestPeriodicTable(unittest.TestCase):
    def test_invalid_key(self):
        with self.assertRaises(ValueError):
            periodic_table('Ubn')

テスト test_invalid_key() では、関数 periodic_table() に不正な値を与えると例外が上がることをチェックしています。

このテストを上の python -m unittest で走らせると無事成功することが確認できます。

ちなみに、 test_invalid_key() は次のように書いても( ValueErrorException に変更しても)成功します。

def test_invalid_key(self):
        with self.assertRaises(Exception):
            periodic_table('Ubn')

なぜなら、 assertRaises() は通常の tryexcept の仕組みと同じように、例外クラスの継承ツリーにおける親クラスは子孫の例外クラスも捕捉するようになっているためです。

つまり、 assertRaises()Exception のような大きめの例外を渡すと、想定と異なる例外があがったのにそれをキャッチし、本来失敗すべきケースを成功とみなしてしまうことがあります。そのため、 assertRaises() には適切な粒度の例外を渡さなくてはなりません。

2. 例外のメッセージも含めてテストする


例外のメッセージも含めてテストするには assertRaises()msg 引数を使用します(ちなみに、 msg 引数は Python 3.3 で追加されたので、 Python 3.2 以前では使用することができません)。

class TestPeriodicTable(unittest.TestCase):
    def test_invalid_key(self):
        with self.assertRaises(ValueError, msg='不正な元素記号が選択され'):
            periodic_table('Ubn')

msg は完全なメッセージである必要はありません。 msg が例外のメッセージの中に含まれていればテストはパスしたものとみなされます。

例外をメッセージも含めてテストする方法は他にもあります。そのひとつは、コンテキストマネージャ変数を使う方法です。

class TestSillyCase(unittest.TestCase):
    def test_silly_check(self):
        with self.assertRaises(NotImplementedError) as cm:
            raise NotImplementedError('実装してから呼ぶのじゃ')

        self.assertIsInstance(cm.exception, NotImplementedError)
        self.assertEqual(cm.exception.args[0], '実装してから呼ぶのじゃ')

コンテキストマネージャ変数(この場合は cm )にはアトリビュート exception がありキャッチされた例外がその中に格納されています。

さらにもうひとつの方法として、 assertRaises() の代わりに assertRaisesRegex() メソッドを使う方法もあります。

class TestShop(unittest.TestCase):
    def test_lack_of_gold(self):
        with self.assertRaisesRegex(ValueError, '^手持ちは \d+ ゴールドです。 \w+ は買えません。$'):
            raise ValueError('手持ちは 15 ゴールドです。 ひのきの棒 は買えません。')

assertRaisesRegex() はその名のとおり正規表現でメッセージのチェックを行う機能を提供します。

・・・というわけで、 unittest で例外を含むパターンをテストする方法についてかんたんにではありますがまとめてみました。

ちなみに、 assertRaises()assertRaisesRegex() ともに、コンテキストマネージャではなく通常のメソッドとして利用することもできます。

class TestPeriodicTable(unittest.TestCase):
    def test_invalid_key(self):
        self.assertRaises(ValueError, periodic_table, 'Ubn')

この使い方をする場合は、第 1 引数に想定される例外クラスを、第 2 引数に対象の callable を、第 3 引数以降は第 2 引数の callable に渡したい引数を渡します。結果はコンテキストマネージャパターンと同じになります。

以上です。

参考

2018/10/21

ライブラリ: dataclasses

今回は Python 3.7 で標準ライブラリに追加された dataclasses について見ていきたいと思います。

import dataclasses

目次


  • dataclasses とは
  • dataclasses の使い方
  • dataclasses の使いどころ

dataclasses とは


dataclasses は、独自のクラスを定義するときに定型的な特殊メソッド( special methods )( __init__() 等)の記述を省略できる機能を提供するモジュールです。 Python 3.7 から標準ライブラリとして Python 本体に同梱されるようになりました。

代表的な機能は次の 2 つです。

  • dataclasses.dataclass: クラスのデコレータ。対象のクラスのクラス変数をもとに特殊メソッドを追加する。
  • dataclasses.field: クラス変数に使う。 dataclass とあわせて使うことでアトリビュートごとの挙動を細かく設定できる。

dataclasses の使い方


まずインストールについてですが、 dataclasses は Python 3.7 以降に同梱されているためインストールする必要はありません。

代表的な機能である次の 2 つから見ていきましょう。

  • dataclasses.dataclass
  • dataclasses.field

dataclasses.dataclass

dataclass はクラスに対して使うデコレータです。

dataclass でデコレートされたクラスには、 __init__()__repr__()__eq__() の 3 つの特殊メソッドが自動的に追加されます。

from dataclasses import dataclass

TAX_RATE = 0.1


@dataclass
class Book:
    title: str
    isbn13: str
    price: int

    def price_with_tax(self):
        return int(self.price * (1 + TAX_RATE))


# __init__() が自動的に定義されている:
book1 = Book('小僧の神様・城の崎にて', '978-4101030050', 562)

# クラス変数と同名のアトリビュートが利用できる:
print(book1.title)
# => '小僧の神様・城の崎にて'
print(book1.isbn13)
# => '978-4101030050'
print(book1.price)
# => 562
print(book1.price_with_tax())
# => 618

# __repr__() も自動的に定義されている:
print(repr(book1))
# => Book(title='小僧の神様・城の崎にて', isbn13='978-4101030050', price=562)

# __repr__() の出力は eval() すると新しいインスタンスが作れる形になっている:
book2 = eval(repr(book1))

# __eq__() も自動的に定義されている:
print(book1 == book2)
# => True

dataclass の最もシンプルな使い方は上のサンプルのように @dataclass という形でデコレートするものですが、 @dataclass にはオプションを渡すこともできます。オプションでは、特殊メソッドの追加の有無等を指定することができます。

from dataclasses import dataclass


@dataclass(repr=False, eq=False)
class Book:
    title: str
    isbn13: str
    price: int


# __init__() が自動的に定義されている:
book1 = Book('暗夜行路', '978-4101030074', 961)

# repr=False により __repr__() は定義されていないので object のデフォルトの出力になる:
print(repr(book1))
# => <__main__.Book object at 0x106fa94a8>

book2 = Book('暗夜行路', '978-4101030074', 961)

# eq=False により __eq__() は自動追加されていないので、デフォルトの object.__eq__() で判定される:
print(book1 == book2)
# => False

ちなみに、 dataclass デコレータが @dataclass でも @dataclass() でも同じような使い方ができるのは、 dataclass がそのように(シンプルに使えるように)工夫して作られているからであり、一般のデコレータはこのようには行きません。

dataclass デコレータがアトリビュート化する要素として認識するのは タイプアノテーションのあるクラス変数だけ です。タイプアノテーションの無いクラス変数は無視されるので、そこは注意が必要です。次のコードではタイプアノテーションの無い isbn13 は自動追加されません。

from dataclasses import dataclass


@dataclass
class Book:
    title: str
    isbn13 = ''
    price: int


# isbn13 は dataclass には無視される:
book1 = Book('和解', 464)
print(repr(book1))
# => Book(title='和解', price=464)

また、 dataclass は「タイプアノテーションがあるかどうか」という点だけを見ており、具体的な型が何なのかは見ていないという点もポイントです。つまり、 int と定義されている変数に str 型の値を代入しても何も問題ありません(バリデーション等は特に追加されません)。

# 次のコードも特に問題なし
book1 = Book('和解', '464')

尚、 Python 3.7 における dataclass の宣言部は次のようになっています。

@dataclasses.dataclass(
    *, 
    init=True, 
    repr=True, 
    eq=True, 
    order=False, 
    unsafe_hash=False, 
    frozen=False)

引数がすべて *, の後に並んでいるので、引数はすべてキーワードを明示的に指定した渡し方をする必要があります。

dataclassesdataclass でデコレートされたクラスとそのインスタンスに対して使える関数をいくつか用意しています。次のコードでは fields()asdict()astuple() の 3 つを試しています。

from dataclasses import dataclass, fields, asdict, astuple


@dataclass
class Book:
    title: str
    isbn13: str
    price: int


# fields() 関数はそのクラスあるいはインスタンスのフィールドの一覧をタプルで返す:
# タプルの各要素は dataclasses.Field クラスのインスタンス
print(type(fields(Book)))
print([x.name for x in fields(Book)])
# => ['title', 'isbn13', 'price']

book1 = Book('小僧の神様・城の崎にて', '978-4101030050', 562)
print([x.name for x in fields(book1)])
# => ['title', 'isbn13', 'price']

# asdict() 関数は辞書を生成する
print(asdict(book1))
# => {'title': '小僧の神様・城の崎にて', 'isbn13': '978-4101030050', 'price': 562}

# astuple() 関数は tuple を生成する
print(astuple(book1))
# => ('小僧の神様・城の崎にて', '978-4101030050', 562)

dataclasses のアイデアの概要は以上でおおよそ押さえられたかと思いますが、実際に利用する場合は詳細を確認してから使うようにしてください。


つづいて field を見てみましょう。

dataclasses.field

fielddataclass でデコレートされたクラスのアトリビュートの挙動を細かく指定するためのものです。

例えば、アトリビュートのデフォルト値に list を指定したいときには field が次のように使えます。

from dataclasses import dataclass, field
from typing import List


@dataclass
class Film:
    title: str = ''
    cast: List[str] = field(default_factory=list)


f1 = Film('ジュラシック・ワールド', ['クリス・プラット', 'ブライス・ダラス・ハワード'])
print(repr(f1))
# => Film(title='ジュラシック・ワールド', cast=['クリス・プラット', 'ブライス・ダラス・ハワード'])


f2 = Film('ジュラシック・ワールド 炎の王国', ['クリス・プラット', 'ブライス・ダラス・ハワード'])
print(repr(f2))
# => Film(title='ジュラシック・ワールド 炎の王国', cast=['クリス・プラット', 'ブライス・ダラス・ハワード'])

また、次のように書くと、一部の要素( subtitle )を __eq__() での比較の対象外にすることができます。

from dataclasses import dataclass, field


@dataclass
class Film:
    title: str
    subtitle: str = field(compare=False)


f1 = Film('007', 'ワールド・イズ・ノット・イナフ')
f2 = Film('007', '慰めの報酬')
f3 = Film('007', 'スカイフォール')

# __eq__() には subtitle が使われないので、 == 演算で title のみが比較された結果、 True が返る
print(f1 == f2 == f3)
# => True

field を使うとその他さまざまなカスタマイズを行うことができます。 Python 3.7 における field の宣言部は次のようになっているので、実際に利用するときは事前に詳細を確認するようにしてください。

dataclasses.field(
    *, 
    default=MISSING, 
    default_factory=MISSING, 
    repr=True, 
    hash=None, 
    init=True, 
    compare=True, 
    metadata=None)


かんたんにですが、以上 dataclasses の基本的な使い方についてでした。

最後に使いどころについて。

dataclasses の使いどころ


私の認識では dataclassesnamedtuple の進化版的な位置づけのものです。

使い捨てのスクリプト作成等の場合は、単純に特殊メソッドの作成を省略したいときに気軽に使うとよさそうです。規模が大きめの開発では、アーキテクチャの境界をまたぐ情報の受け渡しに使用するシンプルなデータ構造やプレーンなドメインモデルクラス等で使うと便利かと思います。

dataclass デコレータが追加してくれる特殊メソッドのうち、 __init__()__eq__() は(自分で定義したいので)不要な場合も多そうですが、 __repr__() は多くの場合そのまま使えて便利なのではないでしょうか。

dataclasses の機能を濫用すると「 Explicit is better than implicit. 」に反するコードになりそうな気もしますが、今後 dataclasses がクラス定義における定番と言われるくらいよく使われるようになれば、( Python に不慣れな人にはわかりづらいけれど)ある程度経験のある Pythonista には implicit というほどのものではなくなってくる、のかも、しれません。

というわけで、 Python 3.7 で導入された dataclasses の説明でした。

同じく「特殊メソッドの自動定義」で人気のライブラリ attrs との違いや dataclasses が標準ライブラリに追加された経緯等について興味のある方には以下のページ等が参考になるかと思います。


関連記事


参考

2018/09/25

Python Tips: functools.reduce() を活用したい

Python が標準で提供する関数のひとつに functoolsreduce() があります。

from functools import reduce

reduce() は一見使いどころがわかりづらいのですが、活用できるようになるととても便利な関数です。今回はそんな reduce() について、 使いどころと実践的なサンプル をご紹介してみたいと思います。

使いどころ


reduce() は「 シーケンス → ひとつの値 」という処理をしたいときに利用できる関数です。関数でいえば「引数にシーケンスを受け取り、何らかの処理をして、戻り値をひとつだけ返す」というふるまいの関数を書きたいときに使えます。

このパターンは標準的なビジネスロジックの中にたくさん登場します。いくつか例をあげてみます。

注文の合計金額を計算する: 「注文のラインアイテム」というシーケンスから「合計金額」を計算する。

複数の権限がすべて満たされているかをチェックする: 「複数の権限」というシーケンスから「操作の可否」を計算する。

複数のロールのうちひとつでもユーザが所属するものがあるかをチェックする: 「複数のロール」というシーケンスから「ユーザの所属の有無」を計算する。

リストを連結する: 「複数のリスト」というシーケンスから「連結したリスト」を計算する。

集合の共通部分を取る: 「複数の集合」というシーケンスから「共通部分」を計算する。

集合の和集合を取る: 「複数の集合」というシーケンスから「和集合」を計算する。

複数のフラグをまとめる: 「複数のフラグ」というシーケンスから「合算したフラグ」を計算する。

reduce() については「 reduce() を使うと [3, 5, 10]150 といった数列の積の計算がかんたんにできます」といったシンプルな例を使った說明がよくなされますが、それだけ聞いても「え、それ、業務のプログラミングで使います?」となりがちです。対して、上のような実際にありそうな例を見ると「 reduce() が活用できる場面は意外に多いなぁ」と思えるのではないかと思うのですがいかがでしょうか。

続いて、上にあげた例のいくつかに対してサンプルコードを見てみましょう。

実践的なサンプル


注文の合計金額を計算する

注文のラインアイテムから合計金額を計算します。

from collections import namedtuple
from functools import reduce

LineItem = namedtuple('LineItem', ['商品ID', '単価', '数量'])

def calc_total(items):
    """ラインアイテムの合計金額を計算する"""
    return reduce(lambda accum, line_item: accum + line_item.単価 * line_item.数量, items, 0)

line_items = [
    LineItem('shirt a', 1000, 2),
    LineItem('shirt b', 1200, 1),
    LineItem('shirt c', 4000, 2),
]

calc_total(line_items)
# => 11200

関数 calc_total() は各ラインアイテムの単価と数量を見てその合計金額を計算する関数です。 reduce() の中の accum には「単価 ✕ 数量」の値が蓄積され、最終的に注文全体の合計金額が出ます。

余談ですが、 accum の値がどのように変化していくのかを見たい場合は lambda をローカル関数に差し替えて中身を出力するとよいでしょう。

def calc_total(items):
    """ラインアイテムの合計金額を計算する"""
    def _sumproduct(accum, line_item):
        print(accum)
        return accum + line_item.単価 * line_item.数量

    return reduce(_sumproduct, items, 0)
    # return reduce(lambda accum, line_item: accum + line_item.単価 * line_item.数量, items, 0)

calc_total(line_items)
# 0
# 2000
# 3200
# => 11200

ただ、この処理は、例えば内包表記を使って次のように書くこともできます。

def calc_total(line_items):
    return sum(x.単価 * x.数量 for x in line_items)

reduce() を使った書き方がしっくり来ない場合はムリに reduce() を使わず、そのときどきでコードの意図がわかりやすい・メンテナンスしやすい書き方を選ぶとよいかと思います。

複数の権限がすべて満たされているかをチェックする

あらかじめ定義されている権限を対象ユーザがすべて持っているかどうかをチェックします。

from functools import reduce


class User:
    def __init__(self, permissions):
        self._permissions = permissions

    def has_perm(self, permission):
        return permission in self._permissions

    def has_all_perms(self, permissions):
        """指定された権限をすべて持っているかチェックする"""
        return reduce(lambda accum, p: accum and self.has_perm(p), permissions, True)


required_permissions = ('perm_a', 'perm_b', 'perm_c')


user1 = User(('perm_c',))
print(user1.has_all_perms(required_permissions))
# => False

user2 = User(('perm_a', 'perm_b', 'perm_c'))
print(user2.has_all_perms(required_permissions))
# => True

クラス User のメソッド has_all_perms() は、 list あるいは tuple で渡された一連の権限をユーザがすべて持っているかどうかをチェックします。 reduce() を使うことでわかりやすくシンプルに書くことができています。

上の「注文の合計金額」が sum() を使って書けたのと同様に、こちらは all() と内容表記を使って書くこともできます。

def has_all_perms(self, permissions):
        """指定された権限をすべて持っているかチェックする"""
        return all(self.has_perm(p) for p in permissions)

こちらも all() の方が読みやすくメンテナンスしやすいのであればムリに reduce() を使う必要はありません。

ちなみに、上の「 複数のロールのうちひとつでもユーザが所属するものがあるかをチェックする 」はこのチェック対象のデータが権限からロールに変わって all()any() に変わるようなイメージです。

リストを連結する

各要素がリストのリストに対して、その要素をすべて連結した大きなリストを生成します。

from functools import reduce


def concat_lists(lists):
    """リストを連結する"""
    return reduce(lambda accum, x: accum + x, lists, [])


lists1 = [[1, 2, 3], [5, 8], [13, 21, 34, 55]]
print(concat_lists(lists1))
# => [1, 2, 3, 5, 8, 13, 21, 34, 55]

関数 concat_lists() は、リストからなるリストの各要素を連結します。こちらも reduce() を使うことで、わかりやすく簡潔に書くことができています。

もし処理の途中経過が見たければ、上で述べたように lambda の部分を print() 等で出力を行うローカル関数に差し替えるとよいでしょう。

def concat_lists(lists):
    """リストを連結する"""
    def _extend(accum, x):
        print(accum)
        return accum + x
    return reduce(_extend, lists, [])
    # return reduce(lambda accum, x: accum + x, lists, [])

lists1 = [[1, 2, 3], [5, 8], [13, 21, 34, 55]]
print(concat_lists(lists1))
# []
# [1, 2, 3]
# [1, 2, 3, 5, 8]
# [1, 2, 3, 5, 8, 13, 21, 34, 55]
# => [1, 2, 3, 5, 8, 13, 21, 34, 55]

この concat_lists() の処理は sum() を使って書くことも可能です。

def concat_lists(lists):
    """リストを連結する"""
    return sum(lists, [])

この単純な例だと sum() を使った方がむしろわかりやすいかもしれませんね。

集合の和集合を取る

複数ある集合の和集合を計算します。

from functools import reduce


def union_multiple(*sets):
    """和集合を作る"""
    return reduce(lambda accum, x: accum | x, sets, set({}))


set1 = {'鹿児島', '宮崎', '熊本'}
set2 = {'大分', '佐賀'}
set3 = {'長崎', '福岡'}

print(union_multiple(set1, set2, set3))
# => {'熊本', '長崎', '福岡', '大分', '宮崎', '佐賀', '鹿児島'}

関数 union_multiple() は任意の数の引数( set )を受け取って、それらの和集合を返す関数です。 Python では 2 つの集合の和集合は set_a | set_b (あるいは set_a.union(set_b) )で計算できるので、ここで必要なのは reduce() を使ってそれを蓄積していくことだけです。

標準ライブラリ operator になじみがあって、コードのわかりやすさが損なわれないと考えられるのであれば、同じ処理は operator.or_ を使って次のように書いてもよいでしょう。

from functools import reduce
from operator import or_


def union_multiple(*sets):
    """和集合を作る"""
    # return reduce(lambda accum, x: accum | x, sets, set({}))
    return reduce(or_, sets, set({}))

Python では +-*/&| 等の 2 項演算子をオブジェクトとして扱うことができませんが、その代わりに operator の中にそれらに相当する関数が用意されています。

一例:

  • +: operator.add() または operator.concat()
  • -: operator.sub()
  • *: operator.mul()
  • /: operator.truediv()
  • &: operator.and_()
  • |: operator.or_()

ちなみに、 reduce()operator が提供する関数を知っておくとよりシンプルにわかりやすく書けることがあるので、 reduce() を活用したい方は operator もあわせてチェックしておくとよいかもしれません。

尚、上の「 集合の和集合を取る 」と「 複数のフラグをまとめる 」については、ここではサンプルコードは示しませんがこの union_multiple() と同じような考え方で書くことができます。

応用的なサンプル


ちょっと応用的(トリッキー?)な例もいくつかあげてみます。 reduce() をむやみやたらと使ってしまうとかえってコードがわかりづらくなりますが、こういう使い方もできるということを知っておくと実装の選択肢の幅が広がってよいのではないかと思います。

入れ子の辞書の要素にアクセスする

これは Stack Overflow で紹介されていた使い方です。私は目からウロコでした。

入れ子になった dict がありそれを掘り下げる一連のキーが変数として与えられたときに、要素にアクセスするロジックを reduce() を使ってスムーズに書くことができます。

from functools import reduce


def dict_deep_access(adict, key_tree):
    """ネストされた dict の要素にアクセスする"""
    return reduce(lambda elem, key: elem[key], key_tree, adict)
    # 次のように書くこともできる
    # return reduce(dict.__getitem__, key_tree, adict)


d1 = {
    '沖縄': {
        '恩納村': {
            '万座毛': '隆起サンゴの断崖',
            'なかゆくい市場': '道の駅',
            '真栄田岬': '夕日スポット',
        }
    }
}

key_trees = [('沖縄', '恩納村', '万座毛'), ('沖縄', '恩納村', '真栄田岬')]

for tree in key_trees:
    print(dict_deep_access(d1, tree))
# =>
# 隆起サンゴの断崖
# 夕日スポット

特定のディレクトリからの相対パスでアクセス

特定のディレクトリから対象ファイルへの相対パスが list として与えられたときに、対象ファイルのパスを生成するというものです。

from functools import reduce
from pathlib import Path


def file_relative_from(root, relative_path):
    """指定された場所からの相対パスでファイルを取得する"""
    file = reduce(lambda parent, dir: parent / dir, relative_path, root)
    return file.resolve()


path1 = Path('/private/tmp/abc/abc1.txt')
relative_path1 = ['..', '..', 'def', 'def1.txt']

file_relative_from(path1, relative_path1)
# => /private/tmp/def/def1.txt

関数の組み合わせ

複数の関数を組み合わせて順次適用していくというものです。

from functools import reduce


def apply_filters(seq, filters):
    """シーケンスに複数のフィルタを連続で適用する"""
    return reduce(lambda result, fn: fn(result), filters, seq)


filters = (
    lambda seq: [x.upper() for x in seq],
    lambda seq: [x.center(12) for x in seq],
    lambda seq: ['⚡ {} ⚡'.format(x) for x in seq],
    lambda seq: '\n'.join(seq),
)

関数 apply_filters() は、第 2 引数に callable のシーケンスを受け取り、その要素を第 1 引数に順次適用した結果を返してくれます。

apply_filters()filters を組み合わせると次のような処理になります。

apply_filters(['teenage', 'mutant', 'ninja', 'turtles'], filters)
# =>
# ⚡   TEENAGE    ⚡
# ⚡    MUTANT    ⚡
# ⚡    NINJA     ⚡
# ⚡   TURTLES    ⚡

apply_filters(['genetically', 'modified', 'punk', 'rock', 'pandas'], filters)
# =>
# ⚡ GENETICALLY  ⚡
# ⚡   MODIFIED   ⚡
# ⚡     PUNK     ⚡
# ⚡     ROCK     ⚡
# ⚡    PANDAS    ⚡

この処理は例えば map() や内包表記を使うと次のように書くこともできます。

# 内包表記で書いた場合
def apply_filters_hard_1(seq):
    return '\n'.join(
        '⚡ {} ⚡'.format(x) for x in (
            x.center(12) for x in (
                x.upper() for x in seq
            )
        )
    )

# map() で書いた場合
def apply_filters_hard_2(seq):
    return '\n'.join(
        map(
            lambda x: '⚡ {} ⚡'.format(x),
            map(lambda x: x.center(12),
                map(lambda x: x.upper(),
                    seq
                )
            )
        )
    )

・・・が、これだと関数の適用順序とは逆に lambda を書かなくてはなりません。このように書くぐらいであれば各処理の結果を変数に格納し行を分けて書いた方がよいでしょう。

サンプルの紹介は以上です。

まとめ


というわけで、 Python の reduce() の使いどころと実践的なサンプルについてでした。

これは受け売りですが、 reduce() については、要は「 sum()all()any() 等の「シーケンスからひとつの値を生成する」タイプの処理を抽象化したものが reduce() 」という捉え方ができます。もし OOP のクラスと同じような親子関係(継承関係)が関数にもあるなら、 reduce()sum()all() の親関数、と捉えてもよさそうです。

余談ですが、 Python ・ Ruby ・ JavaScript あたりではこの操作を行う関数に reduce という名前がつけられていますが、関数型ベースの言語においては reduce よりも fold という名前が選ばれることが多いような気がします。

以上です。

関連記事


参考

reduce() については以下のページ等もおもしろいです。

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 の傾向等に興味のある方はそちらもご覧になるとよいかもしれません。