2017/07/14

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

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

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

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

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


経緯


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

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

Python 1.6 | Python.org

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

[Python-Dev] String methods... finally


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

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

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

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

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

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

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

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

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


メリット / デメリット


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

メリット

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

デメリット

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

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

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

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

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

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

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

以上です。

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


参考

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

2017/06/27

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

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

print('Hello world')

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

Python 2 の print 文の使い方


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


str.format()


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

PEP 498 -- Literal String Interpolation | Python.org

2017/06/16

ライブラリ: attrs

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

import attr

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

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

こちらです。

- attrs : Python Package Index

こちらではありません。

- attr : Python Package Index

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

インストール


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

pip install attrs

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

使い方


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

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

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

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

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

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

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

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

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

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

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

TypeError が出ました。

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

import attr

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

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

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

from datetime import datetime
import attr

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

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

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

from datetime import datetime
import attr

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

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

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

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

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

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

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

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

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

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

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

以上です。

参考

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

公式

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

2017/06/06

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

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

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

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

import sys

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

(やりたい処理)

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

import sys

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

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

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

check_stdin.py:

# coding: utf-8
import sys

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

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

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

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

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

read_csv.py:

# coding: utf-8

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

import sys
import csv
from itertools import islice


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

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

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


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

    return rows, header


if __name__ == "__main__":
    main()

試してみます。

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

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

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

2017/05/08

ライブラリ: argparse

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


概要


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

import argparse

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

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

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

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

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

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


インストール


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


使い方


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

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

argparse_sample_1.py:

# coding: utf-8

import argparse
from pathlib import Path

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

usage:

    $ python argparse_sample_1.py sample.txt
"""

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

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


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

    return parser.parse_args()


if __name__ == '__main__':
    main()

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

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


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

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

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

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

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

    return parser.parse_args()

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

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

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

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

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

引数なしで利用:

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

引数を渡して利用:

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

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

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

positional arguments:
  file        Target file.

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

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

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

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

argparse_sample_2.py:

# coding: utf-8

import argparse

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

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


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

    return parser.parse_args()


if __name__ == '__main__':
    main()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

以上です。

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

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

positional arguments:
  files                 2 csv files.

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

以上です。

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

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


参考


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

2017/04/26

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

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

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

- gettext - Wikipedia

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


インストール


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

pip install polib


使い方


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

import polib

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

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

print(po.metadata)

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

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

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

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

以上です。

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

Quick start guide — polib documentation

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



参考

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

2017/04/02

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

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

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

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

from pathlib import Path

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

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

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

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

# coding: utf-8
from pathlib import Path

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

    p = Path(path)

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


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


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

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

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

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

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

以上です。

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