辞書を値でソートする

Python で辞書をソートしようとすると、

>>> h = {'a' : 200, 'b' : 300, 'c' : 500, 'd' : 100, 'e' : 400}
>>> for k, v in sorted(h):
...    print k, v
...
Traceback (most recent call last):
  File "", line 1, in 
ValueError: need more than 1 value to unpack

エラーになった。これは、sorted(h) がキーしか返さないからだ。

>>> for k in sorted(h):
...     print k
...
a
b
c
d
e

キーと値がほしいときにはこうする。

>>> for k, v in sorted(h.items()):
...     print k, v
...
a 200
b 300
c 500
d 100
e 400

辞書の items メソッドは、キーと値からなるタプルのリストを返してくれる。

>>> h.items()
[('a', 200), ('c', 500), ('b', 300), ('e', 400), ('d', 100)]

で、そのタプルの第1要素でソートされる。つまり辞書のキーでソートされるわけだ。

でも、今日やりたいのは値でソートすること。そのためには、sorted に key 引数を渡してやればいい。具体的にはこうする。

>>> for k, v in sorted(h.items(), key=lambda x: x[1]):
...     print k, v
...
d 100
a 200
b 300
e 400
c 500

key 引数に渡しているのは、タプルの第2要素を返す関数だ。つまりこの関数の返り値でソートされるわけだな。
ちなみに、降順にするには、reverse=True を渡してやる。

>>> for k, v in sorted(h.items(), key=lambda x: x[1], reverse=True):
...     print k, v
...
c 500
e 400
b 300
a 200
d 100

__main__.py

今日は Python。

ディレクトリやzipファイルに、__main__.py という名前のスクリプトを含めておくと、そのディレクトリやzipファイルを Python の引数に指定することで、__main__.py が実行される。

例を示そう。

print "Hello, Python."

これを、hello フォルダの中においておく。で、次のように実行すると:

^o^ > ls hello
__main__.py

^o^ > python hello
Hello, Python.

このとおり、__main__.py が実行された。

zipファイルにした場合も同じように動く。

^o^ > cd hello

^o^ > zip hello.zip __main__.py
  adding: __main__.py (164 bytes security) (stored 0%)

^o^ > python hello.zip
Hello, Python.

こんな機能があったとは。でも、どういうときに使うんだろ。

トリボナッチ数

トリボナッチ数というのを知った。

 cf. フィボナッチ数,トリボナッチ数 – arik_egaのノート

要するに、フィボナッチ数が前2項の和であるのにたいして、トリボナッチ数は前3項の和である数。最初の3項は 0,0,1。詳しくは Wikipedia で。

Python のジェネレータで書いてみた。

def tribonacci(n):
    a, b, c = 0, 0, 1
    m = 0
    while m < n:
        yield a
        a, b, c = b, c, (a + b + c)
        m += 1

for t in tribonacci(20):
    print t [/crayon]

この例では、最初の20項を出力している。 実行例:

^o^ > python tribonacci.py
0
0
1
1
2
4
7
13
24
44
81
149
274
504
927
1705
3136
5768
10609
19513

Pythonで文字列を固定長でスライスする

たまには Python も書かねば。

タイトルのとおりなんだけど、ちょうどいい関数やメソッドが見当たらなかったので書いた。
要するに Ruby の Enumerable#each_slice みたいなのの文字列版。

def string_each_slice(s, n):
    i = 0
    r = []
    while i < len(s):
       r.append(s[i:i+n])
       i += n
    return r

s = "abcdefg"
print string_each_slice(s, 2)
^o^ > python string_each_slice.py
['ab', 'cd', 'ef', 'g']

最後が指定した固定長に足らない場合は、足らないまま返す。まあ、これは Ruby の Enumerable#each_slice と同じ振る舞いにしただけ。

スクリプトの–versionオプションについて

※ 追記あり

argparse モジュールを使っているうち、気になった、というか気に入らないことがあったのでメモしておく。
とりあえず、こんなスクリプトがあったとする。

import argparse

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', action='store',
                    help='specify name instead "world".')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

これは以前 argparse を試してみたときのスクリプトにちょっと変更を加えたもので、どこを変えたかというと、位置引数 NAME のデフォルト値をなくして必須の引数にした点だ。

^o^ > python hello2.py -h
usage: hello2.py [-h] [-m] [-t N] NAME

Hello world program.

positional arguments:
  NAME             specify name instead "world".

optional arguments:
  -h, --help       show this help message and exit
  -m, --morning    good morning
  -t N, --times N  repeat N times

^o^ > python hello2.py Andy
Hello, Andy.

^o^ > python hello2.py -m Andy
Good morning, Andy.

^o^ > python hello2.py -m -t 3 Andy
Good morning, Andy.
Good morning, Andy.
Good morning, Andy.

^o^ > python hello2.py
usage: hello2.py [-h] [-m] [-t N] NAME
hello2.py: error: too few arguments

最後の実行例のように、NAME 引数を省略するとエラーになる。これは期待通りの動作。
で、もうひとつ覚えておいてほしいのは一番最初の -h オプションを指定した実行例。NAME 引数が無いにもかかわらずエラーにならずにヘルプが表示されている。これも期待通りの動作だ。

さて、本題はこれから。一通りの機能を実装し終わったら(そしてこれからも機能を拡張するつもりがあるなら)、スクリプトのバージョンを表示するオプションを追加したくなる。
というわけで、バージョンを表示する -v (–version) オプションを追加したのがこれだ。

import argparse

script_version = "v0.1.0"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', dest='version', action='store_true',
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

if args.version:
    print script_version
    exit()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

ところが、これは期待通りには動作しない。

^o^ > python hello2a.py -v
usage: hello2a.py [-h] [-v] [-m] [-t N] NAME
hello2a.py: error: too few arguments

NAME 引数が無いために、引数が足りないと怒られてしまう。
この問題は、NAME 引数を定義するところで、nargs=’?’ とすることでとりあえず回避できる。

import argparse

script_version = "v0.1.0"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', nargs='?', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', dest='version', action='store_true',
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

if args.version:
    print script_version
    exit()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)
^o^ > python hello2b.py -v
v0.1.0

が、一方でこれは別の問題を引き起こす。NAME 引数が無くてもスクリプトが動いてしまうのだ。これは期待している動作とは違う。

^o^ > python hello2b.py
Hello, None.

NAME 引数がない場合はちゃんとエラーになってほしい。でもって -v オプションを指定したときには NAME 引数が無くてもエラーになってほしくない。思い出してほしいが -h オプションはまさにそのように動作している。どうにかして期待通りに動作するようにはできないものだろうか。argparse のドキュメントを見たもののどうも解決策はなさそうに見える。

 cf. http://docs.python.jp/2/library/argparse.html

で、結局、NAME 引数が指定されているかどうかを自前でチェックするようにした。

import argparse

script_version = "v0.1.0"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', nargs='?', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', dest='version', action='store_true',
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

if args.version:
    print script_version
    exit()

if args.name is None:
    print parser.prog + ": error: too few arguments. type " + parser.prog + " -h for help"
    exit()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

これで期待通り。

^o^ > python hello2c.py -v
v0.1.0

^o^ > python hello2c.py
hello2c.py: error: too few arguments. type hello2c.py -h for help

でもやっぱり、argparse で何とかできてほしいなあ。

[追記]
argparse のドキュメントをよくよく読んでみたら、やっぱり解決策があった。
add_argument() の引数に、action=”version” と version=バージョンナンバー を指定すればいい。直したのがこれ:

import argparse

script_version = "v0.1.1"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', action='version', version=script_version,
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

実行結果:

^o^ > python hello2d.py -h
usage: hello2d.py [-h] [-v] [-m] [-t N] NAME

Hello world program.

positional arguments:
  NAME             specify name instead "world".

optional arguments:
  -h, --help       show this help message and exit
  -v, --version    show version and exit
  -m, --morning    good morning
  -t N, --times N  repeat N times

^o^ > python hello2d.py -v
v0.1.1

^o^ > python hello2d.py Andy
Hello, Andy.

^o^ > python hello2d.py
usage: hello2d.py [-h] [-v] [-m] [-t N] NAME
hello2d.py: error: too few arguments

YAMLの読み書き

Python で YAML を読み書きするには、PyYAML というパッケージがある。
というわけで、忘れないようにメモ。

インストールは、ほかのパッケージと同様、pip でインストールできる。

^o^ > pip install pyyaml
Downloading/unpacking pyyaml
  Downloading PyYAML-3.10.tar.gz (241kB): 241kB downloaded
  Running setup.py egg_info for package pyyaml

Installing collected packages: pyyaml
  Running setup.py install for pyyaml
    checking if libyaml is compilable
    Unable to find vcvarsall.bat
    skipping build_ext

Successfully installed pyyaml
Cleaning up...

まずはサンプルのYAMLファイルを用意する。

^o^ > type sample_dict.yaml
Python: Guido
Ruby: Matz
Perl: Larry

YAMLの読み込み

yaml.load関数を使う。

>>> import yaml
>>> f = open('sample_dict.yaml', 'r')
>>> data = yaml.load(f)
>>> f.close()
>>> data
{'Python': 'Guido', 'Ruby': 'Matz', 'Perl': 'Larry'}
>>> type(data)
<type 'dict'>

ちゃんと辞書型のデータになっているのがわかる。ファイルオブジェクトを作らなきゃいけないのが Ruby と比べてちょっと面倒だな。
あ、それから、インポートするのは、pyyaml じゃなくて yaml ね。

YAMLの書き出し

書き出すときには、yaml.dump関数。文字列が返ってくるので、これもやっぱりファイルオブジェクトを使ってファイルに書き出す。

>>> f = open('sample_dict2.yaml', 'w')
>>> f.write(yaml.dump(data))
>>> f.close()
>>> exit()

^o^ > type sample_dict2.yaml
{Perl: Larry, Python: Guido, Ruby: Matz}

ふむ、辞書(YAMLではマップって言うんだっけ?)のフォーマットが読み込んだファイルとは違うな。PyYAMLではこういうフォーマットにしかならないんだろうか。

リストの場合

リストでも試してみよう。

^o^ > type sample_list.yaml
- Python
- Ruby
- Perl

読み込み:

>>> import yaml
>>> f = open('sample_list.yaml', 'r')
>>> data = yaml.load(f)
>>> f.close()
>>> data
['Python', 'Ruby', 'Perl']
>>> type(data)
<type 'list'>

書き出し:

>>> f = open('sample_list2.yaml', 'w')
>>> f.write(yaml.dump(data))
>>> f.close()
>>> exit()

^o^ > type sample_list2.yaml
[Python, Ruby, Perl]

やっぱりフォーマットが違う。
まあ、データの量とかも関係があるのかも。何百行にもなるようなリストを上のようなフォーマットで出力されたら見難くてしょうがない。

Webページから画像を一括ダウンロードするPythonスクリプト

urllib とか BeautifulSoupでのスクレイピングとかの練習を兼ねて作ってみた。何に使うかは想像にお任せする;p

# coding: utf-8

import urllib
import re
import argparse
import os
from BeautifulSoup import BeautifulSoup

script_version = 'v0.0.1'
re_image = re.compile(".+\.(jpg|png|gif)")

def get_linked_images(soup):
    for a in soup("a"):
        for i in a("img"):
            a2 = i.parent
        if re_image.match(a2["href"]):
            image = a2["href"]
            file = url_to_filename(image)
            try:
                print image
                urllib.urlretrieve(image, file)
            except IOError:
                pass

def get_embeded_images(soup):
    for i in soup("img"):
        image = i["src"]
        if re_image.match(image):
            file = url_to_filename(image)
            try:
                print image
                urllib.urlretrieve(image, file)
            except IOError:
                pass

def url_to_filename(url):
    filename = url.split('/')[-1]
    filename = re.sub('\?.+', '', filename)
    if args.dir:
        filename = os.path.join(args.dir, filename)
    return filename

parser = argparse.ArgumentParser(description="Download images from web page.")
parser.add_argument('url', metavar='URL', nargs='?', action='store',
                    help='specify URL.')
parser.add_argument('-v', '--version', dest='version', action='store_true',
                    help='show version and exit')
parser.add_argument('-e', '--embeded-image', dest='embeded', action='store_true',
                    help='download embeded images(default)')
parser.add_argument('-l', '--linked-image', dest='linked', action='store_true',
                    help='download linked images')
parser.add_argument('-d', '--dir', dest='dir', metavar='DIR', action='store',
                    help='download into DIR')
args = parser.parse_args()

if args.version:
    print script_version
    exit()

url = args.url
if args.dir:
    os.makedirs(args.dir)

print "Download images from " + url + "\n"
res = urllib.urlopen(url).read()
soup = BeautifulSoup(res)
if args.linked:
    get_linked_images(soup)
else:
    get_enbeded_images(soup)

ハイフンでつながれた数字を連続する数列に変換するPythonスクリプト

ようするに、この間の逆をやろうってことだ。
 cf. 連続する数列をハイフンでまとめるPythonスクリプト – blog.panickblanket.com

import re

def expand_num(s):
    a = re.split(", *", s)
    r = re.compile("\d+-\d+")

    def f(s1):
        a, b = map(int, s1.split('-'))
        return range(a, b+1)

    def g(x):
        if r.match(x):
            return f(x)
        else:
            return [int(x)]

    return reduce(lambda x, y: x + g(y), a, [])

if __name__ == '__main__':
    print expand_num("1-3")
    print expand_num("1-3, 5, 7-8")
    print expand_num("1, 3-5, 7")

実行結果:

^o^ > python expand_num.py
[1, 2, 3]
[1, 2, 3, 5, 7, 8]
[1, 3, 4, 5, 7]