投稿者「python-tksssk」のアーカイブ

itertools.groupbyを使ったグルーピング・ブレイク処理

何かと気づきが得られるモジュールitertoolsですが、あるリストを走査しながら特定の条件でブレイクして出力を行うと行った処理を上手く記述する方法を検討します。

例えば次のようなリスト(deptでソート済み)があり、dept別にファイルに出力するケースの場合、どのように行うか考えてみます。

emp_list = [
  {'dept': 10, 'ename': 'scott'},
  {'dept': 10, 'ename': 'john'},
  {'dept': 10, 'ename': 'tom'},
  {'dept': 20, 'ename': 'bob'},
  {'dept': 20, 'ename': 'maria'},
]

簡単に思いつくのは、順番に走査しながらdeptが変わったタイミングでファイルに出力する or ファイルを切り替えるといったものです。
以下のコードは、emp_listを上から走査しながら、deptが変わったタイミングでファイルに出力。変わらなかったら貯めこむという例です。

# deptが変わるまで溜め込んでから、ファイルに書き出す例
out = []
for emp in emp_list:
  if len(out) > 0 and out[0]['dept'] != emp['dept']:
    # deptがブレイクした時
    with open("dept-%d.txt" % out[0]['dept'], "wt") as f:
      f.write("\n".join(str(e) for e in out))
    out.clear()
  out.append(emp)
if len(out) != 0:
  # 残ったoutを出力
  with open("dept-%d.txt" % out[0]['dept'], "wt") as f:
    f.write("\n".join(str(e) for e in out))

見た感じ分かりにくいコードですが、この方式のイマイチな点は、ブレイク条件「deptが変わる」でファイル出力する時と、ブレイクしないまま最後に残ったものをファイルに出力する時のコードが重複している点です。groupbyを使うともっと綺麗に書けます。

from itertools import groupby

dept_emp_groups = groupby(emp_list, lambda e:e['dept'])
for dept, dept_group in dept_emp_groups:
    with open("dept-%d.txt" % dept, "wt") as f:
        f.write("\n".join(str(e) for e in dept_group))

groupbyを使うと指定したリスト(listでなくてイテレーション可能なもの)から、特定のキー別にグルーピングした結果を得ることができます。
(実際にはlistではなくイテレータですが)こんなイメージです。

[
  (10, [{'dept': 10, 'ename': 'scott'},{'dept': 10, 'ename': 'john'},  {'dept': 10, 'ename': 'tom'}],
  (20, [{'dept': 20, 'ename': 'bob'},  {'dept': 20, 'ename': 'maria'}],
]

deptをキーにグルーピングしているため、存在するdept別に処理を行います。deptが0件であっても問題ありません。

参考:10.1. itertools — 効率的なループ実行のためのイテレータ生成関数 — Python 3.5.3 ドキュメント

秒から時刻表記、時刻の正規化

100秒を 1min 40secと表記したり、時間の構成要素から人間が理解しやすい時刻表記に正規化する方法。

標準モジュールではないですが、定番のdateutilsモジュールを使うのが楽です。

$ pip install dateutils

100sec -> 1min 40sec

>>> from dateutil.relativedelta import relativedelta
>>> relativedelta(seconds=100).normalized()
relativedelta(minutes=+1, seconds=+40)

123456sec -> 1day 10hours 17minues 36sec

>>> relativedelta(seconds=123456).normalized()
relativedelta(days=+1, hours=+10, minutes=+17, seconds=+36)

normalizedで正規化しているので、1.5hoursは1hour 30minになる

>>> relativedelta(hours=1.5).normalized()
relativedelta(hours=+1, minutes=+30)

文字列として時刻表記にする例

>>> "{0.hours:02}:{0.minutes:02}:{0.seconds:02}".format(relativedelta(seconds=85))
'00:01:25'

日付に月単位で加算減算する (relativedelta)

日付に加算・減算する際には、timedelta が使えますが、月の加算はできません。

日付の加算 減算 timedelta | Python Snippets

標準ライブラリではありませんが、日付処理のデファクトスタンダードともいえる python-dateutil モジュールを使用することで月の加算ができる relativedeltaが使えます。

ライブラリをインストールします。

$ pip install python-dateutil
....

relativedelta(months=1) とすることで直感的に1ヶ月加算することができます。

>>> import datetime
>>> from dateutil.relativedelta import relativedelta
>>> datetime.datetime(2016, 12, 1) + relativedelta(months=1)  #年をまたいだ1ヶ月加算
datetime.datetime(2017, 1, 1, 0, 0)

これを応用することで簡単に月末日を得ることもできます。

>>> datetime.datetime(2015, 2, 1) + relativedelta(months=1, days=-1)  # 2/1から1ヶ月加算して1日減算
datetime.datetime(2015, 2, 28, 0, 0)
>>> datetime.datetime(2016, 2, 1) + relativedelta(months=1, days=-1)
datetime.datetime(2016, 2, 29, 0, 0)

pythonでループのネストを減らす定石 itertools

pythonでのネストされたループを減らすためのよく知られた方法を考察。
2重のネストループ程度であればありがちな例でも問題ないが、3重、4重となってくるとitertoolsのありがたみがわかる。

10×10の座標を全走査するときなど

//ありがちなネストループの例
for x in range(10):
  for y in range(10):
    print("%d, %d" % (x,y))

itertools.productを使って全通りの組み合わせを出す。

import itertools
for x, y in itertools.product(range(10), range(10)): #Xの10通り、Yの10通りの全組み合わせ
  print("%d,%d" % (x,y))

※itertoolsはイテレータを生成しているので、全通りの操作をするために膨大な件数の組み合わせを作ったとしてもメモリを大量に消費することはない。

x,y,zの3つのネストでも同じようにネストせずに記述できる。

import itertools
for x, y,z in itertools.product(range(10), range(10), range(10)):
  print("%d,%d,%d" % (x,y,z))

配列、文字列の部分列を全通り取得したい場合

例えば、文字列”ABCDE”の部分文字列を、"A", "AB", "ABC", "ABCD", "ABCDE", "B", "BC"... のように取得したい場合。

//ありがちなネストループの例
text = "ABCDE"
l = len(text)

for s in range(l+1):   #開始インデックスを外側のループに
  for e in range(s+1, l+1):   #終了インデックスを開始インデックスから始めて末端まで
    print(text[s:e])   #部分文字列取得

itertools.combinationsを使って、開始、終了インデックスの決定のパターンを取得する例。

// itertools.combinationsをつかったインデックスの組み合わせ取得
text = "ABCDE"
l = len(text)

for s, e in itertools.combinations(range(l+1), 2):
  print(text[s:e])

参考:組み合わせや順列の列挙に便利なitertoolsの機能

10.1. itertools — 効率的なループ実行のためのイテレータ生成関数 — Python 3.5.1 ドキュメント

リスト(list), 辞書(dict), setのソート(sorted)

リストをソートするにはsorted関数を使用する。
sorted関数は、ソートしたリストを返す関数で、元になるリストは変更されない(immutable)。

一方リストにはsortメソッドがあり、リスト自体をソートする操作(mutable)もあるので注意。

>>> l = [3, 2, 5, 4, 1]
>>> sorted(l)
>>> [1, 2, 3, 4, 5]
>>> l
[3, 2, 5, 4, 1]

sorted関数は辞書(dict)のキーのソートやsetのソートにも使える。

>>> d = {'c': 10, 'b':1, 'a':30}
>>> sorted(d)   #ソートされたキーのリストを返す
['a', 'b', 'c']

>>> d = set([3,1,2])
>>> d
set([1, 2, 3])
>>> sorted(d)
[1, 2, 3]

sorted関数のreverseをTrueにすれば逆順ソート。

>>> l = [3, 2, 5, 4, 1]
>>> sorted(l, reverse=True)
[5, 4, 3, 2, 1]

sorted関数のkeyを指定することで、任意の条件でソートすることができる。

key関数が返す値でソートされる。以下の例はHogeオブジェクトのname属性を返す関数を指定したことで、name属性でソートしている。

>>> class Hoge:
...   def __init__(self, i, name):
...     self.i = i
...     self.name = name
...   def __repr__(self):
...     return "%d:%s" % (self.i, self.name)
...
>>> l2 = [Hoge(2, 'c'), Hoge(3, 'a'), Hoge(1, 'b')]
>>> l2
[2:c, 3:a, 1:b]
>>> sorted(l2, key=lambda h: h.name) #name属性を返す
[3:a, 1:b, 2:c]

上の例では、name属性を返す関数をlambda式で指定したが、特定の属性を返す関数は、operatorモジュールのattrgetter関数を使えば簡単に作成できる。

>>> import operator
>>> l2 = [Hoge(2, 'c'), Hoge(3, 'a'), Hoge(1, 'b')]
>>> sorted(l2, key=operator.attrgetter('name'))
[3:a, 1:b, 2:c]

with文で使えるコンテキストマネージャー型を作成する

openの時のようにwith文を使えるようにするには、contextlibモジュールのcontextmanagerデコレータを使えば簡単に実装できます。

openの時の例

with open('hoge.txt') as f:
    f.read()
# withブロックを抜けるとファイルclose

contextmanagerを使用した例

from contextlib import contextmanager
@contextmanager
def myfunc():
    print('前処理')
    yiled  #ここで処理が中断
    print('後処理')

# with文で使ってみる。
with myfunc():
    print(1+2)

# 前処理
# 3
# 後処理

abcモジュールで抽象クラス、抽象メソッドを作成する

Pythonは動的型づけというゆるい感じの片付けなので、サブクラスにメソッド実装を強制するような作りが(言語仕様としては)できないが、abcモジュールを使うことで抽象クラス・抽象メソッドを作成することができる。

抽象クラスは、metaclassにabc.ABCMetaを指定して作成し、抽象メソッドには@abstractmethodデコレータを指定する。

from abc import ABCMeta, abstractmethod

class Shape(metaclass=ABCMeta):
    @abstractmethod
    def area(self):
        pass

具象クラスは、普通にスーパークラスに指定して作成する。

class Circle(Shape):
    def __init__(self, r):
        self.r = r

    def area(self):
        return pi * self.r ** 2

c = Circle(10)
c.area()

areaメソッドは、抽象メソッドになっているので実装しない場合には、インスタンス生成時にTypeErrorが発生する。

TypeError: Can't instantiate abstract class Circle with abstract methods area

エラーが発生するのは、インスタンス生成時でclassを定義した時点では発生しないので注意が必要。

29.7. abc — 抽象基底クラス — Python 3.4.3 ドキュメント

argparseでコマンドライン引数の解析

argparseは、コマンドラインプログラムの引数を解析するのに便利なモジュールで、C言語のgetopt的なものです。似たような機能をもつモジュールとしてgetoptモジュール(C言語のものと同等)、optparseモジュールがありますがそれらは古いモジュールで、argparseを使うのが推奨されてるようです。

基本的な使い方

import argparse
p = argparse.ArgumentParser()
p.add_argument("foo") # 位置引数fooを定義
args = p.parse_args()   # デフォルトでsys.argvを解析
print(args.foo)

このように作成したtest.pyファイルを、引数なしで実行してみます。

$ python test.py
usage: test.py [-h] foo
test.py: error: too few arguments

エラーが発生して、簡易的なヘルプメッセージが表示されました。

引数を指定することで解析は成功し、args.fooから引数の値を参照することができます。

$ python test.py hello
hello

オプション引数

引数に、-fや–fooのようにハイフンで始まるようにするとオプション引数(位置で決まらない引数)になる。

p = argparse.ArgumentParser()
p.add_argument('-f', '--foo')    #短い引数-f 長い引数--fooを定義
args = p.parse_args(['--foo', 'hello'])
print(args.foo)    # hello

よく使う使い方

短い引数、長い引数、ヘルプあたりを指定するのが楽。

p.add_argument("-b", "--bar", help="bar argument")

type指定, choice指定

p.add_argument('-n',  type=int)   #整数のみ
p.add_argument('-f', type=open) #ファイル
p.add_argument('-a',  type=int, choices=[1,2,3])   #整数1,2,3のどれか

default指定

引数が指定されたなかった時のデフォルト値を定義。

p.add_argument('-n', default=1)
args = p.parse_args([])
print(args.n)   # 1

const指定

フラグの指定のみで固定値が引数に設定される。

p.add_argument("-n", const=9, nargs="?")  #nargsは引数の数が0 or 1

args = p.parse_args(["-n"])  # 0の時はconstが使用される
print(args.n) #9

args = p.parse_args(["-n", "1"])
print(args.n) # 1

action指定

action指定では、解析した引数をどのように格納するかを指定できる。
store_trueアクションは、フラグが指定されたことでTrueが設定されるアクション。

p.add_argument("-y", action="store_true")
print(p.parse_args(["-y"]))  #True
print(p.parse_args([]))    # False

-hを置き換えながら–helpはヘルプとして利用したい場合

-h, –helpはデフォルトでヘルプ機能になっているが、これが邪魔な場合は、add_help=Falseで作成すると良い。

邪魔で消したものの、他のオプション引数でヘルプを使いたい場合は、action=”help”で追加すれば良い。

-hは消して、–helpだけでヘルプを使う場合は、add_help=Falseで追加しないようにしてから、あらためて–helpをaction=”help”で追加する。

p = argparse.ArgumentParser(add_help=False)
p.add_argument("--help", action="help")
p.add_argument("-h", "--host", help="some hostname")

16.4. argparse — コマンドラインオプション、引数、サブコマンドのパーサー — Python 3.4.3 ドキュメント

pythonのバリデーションライブラリ FormEncode

FormEncodeはHTMLフォームからの送信を検証したり、HTMLフォームを生成したりする高機能なライブラリなのですが、最近ではあまりHTMLフォームを使わず、JSONデータのやりとりということも多いので、HTMLフォームは使わないことも多いと思います。入力チェックのバリデーション機能に関しては長い間メンテされていただけあって信頼に足ると思います。ここでは、バリデーション関数の使い方を紹介します。

インストール

$ pip install FormEncode

validators

たくさんのvalidatorがあります。ここでは整数をチェックするInt。

>>> from formencode import validators
>>> v = validators.Int()
>>> v.to_python("1")   # 1
>>> v.to_python("one") # Invalid: 整数を入力してください
>>> v2 = validators.Int(min=10, max=15)
>>> v2.to_python("5") #  Invalid: Please enter a number that is 10 or greater

似たようなValidatorがたくさんある。
http://www.formencode.org/en/latest/modules/validators.html

python3のbytes型とstr型の比較と変換方法

python3では、文字列とバイト列の区別が明確になりました。python2でバイナリデータを文字列で扱っていたり、strとunicodeの使い分けで混乱していたのがスッキリしたと思いますが、python2から移行するとちょっと混乱したりするのでまとめて置きます。

ちょっと語弊のある書き方かもしれませんが、ありがちな使われ方の例としてマトリックスを作成しました。

python3 python2
byte列 bytes (b'') str ('')
通常の文字列 str ('') str ('')
unicode文字列 str ('') unicode (u'')

3では文字列はunicode文字列として扱われるようになったので、すべてpython2でいうところのu''になりました。u''表記してもエラーにはなりませんが(python3.3以上)同じ意味です。

バイナリデータを扱うときには、bytes型に変換します。bytes型は文字列っぽく扱えますが、strとbytesの連結などはできないので用途に合わせて適切な型で持つようにします。

例えば特定の文字コードの文字列を持ちたい場合はbytes型になります。str文字列として加工などをして最終的に出力する際に、任意の文字コードのバイト列としてエンコードして出力するという使い方になります。

strからbytes

encodeでbytes型(utf-8)に変換します。

>>> 'あいう'.encode('utf-8')
b'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'    #utf8のバイト列

UTF-8 bytesからstr

decodeで文字列に変換します。

>>>> b'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86'.decode('utf-8')
'あいう'

文字列のエンコードを意識した操作

たとえばファイルへShift_JISエンコードの文字列を出力したい場合、Python3においてはそれはbytes型のバイナリデータの出力という意味になります。

Python3の文字列は内部的にUnicodeで保持していますが、これをShift_JISにエンコードすることでbytes型のバイナリデータになります。(python2の時には、unicode文字列からstr文字列になっていました)

ShiftJISやEUCの文字列を正規表現で比較したい場合

Python3では、SJISやEUCの文字列はbytes型になってしまいますが、そのようなバイナリデータを正規表現でマッチングしたい場合にはどうすればよいでしょうか?

通常のマッチングであれば、decodeによってUnicode文字列に変換してから、Unicode文字列の世界でマッチングすれば問題ありません。

どうしてもSJISの文字コードの範囲などでマッチングしたい場合は、パターン文字列もbytes型で記述することで可能です。

>>> import re
# SJISにおける'ABCD'という全角文字列
>>> sjis_str = b'\x82\x60\x82\x61\x82\x62\x82\x63'  
>>> sjis_str.decode('sjis')
'ABCD'

#bytes型文字列にstringのパターンは使用できない
>>> re.search(r'\x82\x61', sjis_str).group(0).decode('sjis')  
TypeError: cannot use a string pattern on a bytes-like object

#パターン文字列もbytes型にする
>>> re.search(rb'\x82\x61', sjis_str).group(0).decode('sjis') 
'A'

# ※上記はどうしてもsjisの文字コードで正規表現パターンを作りたかったため行ったもの。
# 単に'A'を探したいだけであれば、Unicode文字列に変換後にマッチングするほうが一般的。
>>> re.search(r'A', sjis_str.decode('sjis')).group(0)
'A'