転置インデックスによる検索システムを作ってみた

転置インデックスによる検索システムを作ってみよう! にインスパイアされて作ってみました。

検索記事は

[記事ID][SPC][記事内容]\n

以上のフォーマットで、文字コードUTF-8とします。

検索対象ファイルとして

1 これはペンです
2 最近はどうですか?
3 ペンギン大好き
4 こんにちは。いかがおすごしですか?
5 ここ最近疲れ気味
6 ペンキ塗りたてで気味が悪いです
7 ペンペンペンペン

という内容のtest.txt用意しました。

インデックス

n-gramをkeyとして、各記事のtf(記事中のn-gram出現頻度)と記事IDのタプルをtf降順にsortしたリストを登録した辞書

index[n-gram] => [(tf, 記事ID), ...]  #タプルはtf降順にsortしておく

をcPickleでシリアライズしたものをインデックスファイルとして使うことにします。

cPickleを使うと

Pythonのオブジェクト -> pickle -> 文字列

という変換をして、変換した文字列から

文字列 -> pickle -> Pythonのオブジェクト

といった具合に元のオブジェクトに戻すことができます。

具体的に書くと

#pickletest.py
import cPickle

a = range(5)
print a

obj = cPickle.dumps(a)
print obj

b = cPickle.loads(obj)
print b

a = {}
for i in range(5):
  a[i] = str(i**10)

print a

obj = cPickle.dumps(a)
print obj

b = cPickle.loads(obj)
print b
> python pickletest.py
[0, 1, 2, 3, 4]
(lp1
I0
aI1
aI2
aI3
aI4
a.
[0, 1, 2, 3, 4]
{0: '0', 1: '1', 2: '1024', 3: '59049', 4: '1048576'}
(dp1
I0
S'0'
sI1
S'1'
sI2
S'1024'
p2
sI3
S'59049'
p3
sI4
S'1048576'
p4
s.
{0: '0', 1: '1', 2: '1024', 3: '59049', 4: '1048576'}

といったようなことが可能です。

以上のことを踏まえると、インデックスを作るプログラム、makeindex.pyを以下のようになります。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import codecs
import cPickle

# n-gramのn
n = int(sys.argv[1])

# インデックス作成対象ファイルの名前
input_file = sys.argv[2]

index = {}
doc_num = 0

# 文字コードを指定してファイルを開き、一行ずつ読み込む
for line in codecs.open(input_file, 'r', 'utf-8'):
  d = {}
  # 読み込んだ行を記事IDと記事に分解
  l = line.split(' ')
  id = int(l[0])
  doc = l[1][:-1]

  # 記事の文字数回ループ
  for i in range(len(doc)):
    # 記事のi文字目からn文字をn-gramとする
    ngram = doc[i:i+n]
    try:
      # 既に出現していたら出現回数+1
      d[ngram] += 1
    except:
      # 初めてなら出現回数1
      d[ngram] = 1

  # 記事に出現したn-gram全てに対して...
  for ng in d:
    # (tf, 記事ID) というタプルを作る
    # tf = n-gramが記事に出現した回数 / 記事の文字数
    t = (1.0*d[ng]/len(doc), id)
    try:
      # 他の記事で既に出現していたら単純に追加
      index[ng].append(t)

      # tf降順でsort
      index[ng].sort(reverse=True)

    except:
      # 初めて出現したn-gramならリストを作って自分を追加
      index[ng] = [t]

  # 後でtf-idfを計算するために記事の数を覚えておく
  doc_num += 1

# indexに記事の数を登録
index['__DOC_NUM__'] = doc_num

# indexをcPickleで文字列に変換してファイルに書き込む
file(input_file + '-' + str(n) + 'g.idx', 'w').write(cPickle.dumps(index))

例えばtest.txtの2gram単位のインデックスファイルを作りたい場合は

> ./makeindex.py 2 test.txt

というように実行することで、test.txt-2g.idxというインデックスファイルが作成されます。

検索

検索を行うプログラム、search.pyは以下のようになります。


2007/12/05追記: 記事のスコアにtf-idfを加算するところを間違えていたので修正しました。すいません……。この修正により、最後の検索結果も変わります。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import codecs
import cPickle
import math

# インデックスファイルの名前
index_file = sys.argv[1]

# インデックスファイルを読み込み、cPickleで文字列から辞書に変換
index = cPickle.loads(file(index_file).read())

# インデックスファイルの名前からn-gramのnを決定
n = int(index_file.split('-')[-1][0])

# 標準入力の文字コードを指定
sys.stdin = codecs.getreader('utf-8')(sys.stdin)

# 検索結果のスコアを登録する辞書を用意
result = {}

# 標準入力から検索語を読み込む
for line in sys.stdin:
  print line,

  # 検索語の文字数回ループ
  for i in range(len(line)):
    # 検索語のi文字目からn文字をn-gramとする
    ngram = line[i:i+n]

    # n-gramが出現した記事があるなら...
    if ngram in index:
      # indexから記事数取得
      doc_num = index['__DOC_NUM__']

      # n-gramが出現した記事のリスト取得
      elems = index[ngram]

      # df算出
      # df = n-gramが出現した記事数 = n-gramが出現した記事のリストの長さ
      df = len(elems) + 1

      # n-gramが出現した記事のリストに対して...
      for tf, id in elems:
        # n-gramに対する記事のtf-idf算出
        # tf-idf = tf * log(総記事数 / n-gramが出現した記事数)
        tfidf = tf * math.log(1.0*doc_num/df)
        try:
          # 既に他のn-gramでスコアが算出されていたらtf-idfを加算

          # 修正前:
          # score = resul[id]                   <- resultのtypo(ここで例外発生)。タプルをそのままscoreとするのも間違い。
          # result[id] += (score + tfidf, id)   <- タプルにタプルを足している……!

          # 2007/12/05修正:
          # タプルの最初の要素をスコアとして、tfidfを加算するのが正しい
          score = result[id][0]
          result[id] = (score + tfidf, id)
        except:
          # 記事IDのスコアをtf-idfとして登録
          result[id] = (tfidf, id)

  # 辞書resultを値だけのリストにして、スコア降順にソート
  for score, id in sorted(result.values(), reverse=True):
    # 結果出力
    print 'ID:' + str(id), 'SCORE:' + str(score)

基本的には

  1. 検索語をn-gramに分解する
  2. n-gramに対する記事のtf-idfを計算して足し合わせる
  3. tf-idfの合計値が高いものが検索語に関して価値の高い記事であるとする

という流れになっています。

実際に使ってみると以下のようになります。

> echo '最近ペンギンが好き' | ./search.py test.txt-2g.idx
最近ペンギンが好き
ID:3 SCORE:0.178966138356
ID:7 SCORE:0.168236118311
ID:5 SCORE:0.105912232548
ID:2 SCORE:0.0941442067097
ID:1 SCORE:0.0480674623745
ID:6 SCORE:0.0224314824414
> echo 'ペンギン' | ./search.py test.txt-2g.idx
ペンギン
ID:3 SCORE:0.178966138356
ID:7 SCORE:0.168236118311
ID:1 SCORE:0.0480674623745
ID:6 SCORE:0.0224314824414
> echo 'ペン' | ./search.py test.txt-2g.idx
ペン
ID:7 SCORE:0.168236118311
ID:3 SCORE:0.0480674623745
ID:1 SCORE:0.0480674623745
ID:6 SCORE:0.0224314824414
> echo 'ペ' | ./search.py test.txt-2g.idx    
ペ

2gram単位のインデックスなので、一文字では検索できませんでした。こういう場合は…… えーっと、どうするのかな……。


2007/12/05追記: '最近ペンギンが好き' で検索しても 'ペンギン' で検索しても 記事ID:3のスコアが同じことから、上の結果はおかしいことがわかります。修正したsearch.pyで検索すると以下のような結果になります。

> echo '最近ペンギンが好き' | ./search.py test.txt-2g.idx
最近ペンギンが好き
ID:3 SCORE:0.584965877444
ID:7 SCORE:0.168236118311
ID:5 SCORE:0.105912232548
ID:2 SCORE:0.0941442067097
ID:1 SCORE:0.0480674623745
ID:6 SCORE:0.0224314824414
> echo 'ペンギン' | ./search.py test.txt-2g.idx          
ペンギン
ID:3 SCORE:0.405999739087
ID:7 SCORE:0.168236118311
ID:1 SCORE:0.0480674623745
ID:6 SCORE:0.0224314824414
> echo 'ペン' | ./search.py test.txt-2g.idx  
ペン
ID:7 SCORE:0.168236118311
ID:3 SCORE:0.0480674623745
ID:1 SCORE:0.0480674623745
ID:6 SCORE:0.0224314824414
> echo 'ペ' | ./search.py test.txt-2g.idx    
ペ

簡単なProxyを作ってみた

参考資料


参考資料ではSimpleHTTPServer.SimpleHTTPRequestHandlerのdo_GETをオーバーライドしていますが、必要なものはdo_GETとcopyfileだけなのでBaseHTTPServer.BaseHTTPRequestHandlerでいいや、と思ったのでした。

# -*- coding: euc-jp -*-
import BaseHTTPServer
import os
import shutil
import StringIO
import urllib

class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
  def encode(self, s, c):
    for i in ('euc-jp', 'sjis', 'utf-8'):
      try:
        return unicode(s, i).encode(c), i
      except:
        pass
    return s, None

  def do_GET(self):
    print 'get...', self.path
    f = urllib.urlopen(self.path)
    if 'text' in f.info().gettype() and os.path.splitext(self.path)[1] in ('', '.htm', '.html', '.cgi', '.php'):
      s, e = self.encode(f.read(), 'euc-jp')
      if e:
        s = s.replace('。', '(笑')
        f = StringIO.StringIO(self.encode(s, e)[0])
    shutil.copyfileobj(f, self.wfile)

port = 3128
print 'proxy at', port
BaseHTTPServer.HTTPServer(('', port), ProxyHandler).serve_forever()

使い方

  • python proxy.py で起動
  • ブラウザの設定をいじって起動したproxyに繋ぐ
    • こんな感じ 
  • 日本語のページを見たときに '。' が '(笑' に書き換わってイラッとする


ときどき書き換わらなかったりするのは仕様です。嘘です。よくわかりません。


処理の流れ

  • ブラウザが何かをGETしようとするとdo_GETが呼ばれて、self.pathにブラウザがGETしようとしているURLが入る
  • urllib.urlopenでself.pathをGET
  • GETした結果得られるものはファイルオブジェクトっぽいものなのでreadで文字列だけを取り出して書き換える
  • StringIOで文字列からファイルオブジェクトっぽいものに変換
  • 変換したファイルオブジェクトっぽいものをshutil.copyfileobjでself.wfileにコピー
  • 最終的にブラウザに表示されるものはself.wfileの中身

urllib

urllibを使ってみます。

  • urllib.urlopenの引数にURLを入れる
  • 返ってきたオブジェクトはファイルのように扱える

基本的にはこれだけです。簡単ですね。

# get.py
import sys
import urllib

print urllib.urlopen(sys.argv[1]).read()

実行例は以下のようになります。

> python get.py http://d.hatena.ne.jp/pythonco/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=euc-jp">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>pythonco(ぱいそんこ)の日記</title>
<link rel="start" href="./" title="pythonco(ぱいそんこ)の日記">
<link rel="help" href="/help" title="ヘルプ">
<link rel="prev" href="/pythonco/?of=5" title="前の5日分">
...
> python get.py http://www.hatena.ne.jp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<link rel="start" href="http://www.hatena.ne.jp/" title="・  ・>
<link rel="stylesheet" href="/css/base.css" type="text/css" media="all">
<link rel="search" type="application/opensearchdescription+xml" href="http://search.hatena.ne.jp/opensearch/all.xml" tit
<link rel="search" type="application/opensearchdescription+xml" href="http://q.hatena.ne.jp/opensearch/question.xml" tit

<title>・・・/title>
<meta name="description" content="??・鋍腓障・・・吟㏍若・若х蕭罘純㏍違篋冴℡査膈篋阪罎膣・Q&A鐚純若激c・・若・RSS・若
<meta name="keywords" content="・・・hatena,篋阪罎膣↑純若激c・・若・㏍穐ゃ≪・蒔RSS,≪・祉壕В%
...

文字化けしてしまいました。仕方がないのでヘッダのcharsetを見てencodeしてみます。

# get.py
import sys
import urllib

f = urllib.urlopen(sys.argv[1])
c = f.headers.getparam('charset')
print c
print unicode(f.read(), c).encode('euc-jp')
> python get.py http://www.hatena.ne.jp/
utf-8
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<link rel="start" href="http://www.hatena.ne.jp/" title="はてな">
<link rel="stylesheet" href="/css/base.css" type="text/css" media="all">
<link rel="search" type="application/opensearchdescription+xml" href="http://search.hatena.ne.jp/opensearch/all.xml" tit
<link rel="search" type="application/opensearchdescription+xml" href="http://q.hatena.ne.jp/opensearch/question.xml" tit

<title>はてな</title>
<meta name="description" content="株式会社はてなが運営。キーワードで繋がる高機能ブログ、人がたずね人が答える人力検索(Q&
く便利になるサービスが揃っています。">
<meta name="keywords" content="はてな,hatena,人力検索,ソーシャルブックマーク,ブログ,ダイアリー,RSS,アクセス解析">
<link rel="stylesheet" href="/css/top.css" type="text/css" media="all">
<link rel="stylesheet" href="/css/minwidth.css" type="text/css" media="all">
<SCRIPT type="text/javascript">
<!--
var cookie_name = "PORTAL_TAB";
...

これで完璧かな、などと思っていたのですが、charsetを返さないサーバもあるので困りました。

> python get.py http://www.google.co.jp
None
Traceback (most recent call last):
  File "get.py", line 17, in ?
    print unicode(f.read(), c).encode('euc-jp')
TypeError: unicode() argument 2 must be string, not None

仕方がないので片っ端からunicodeに変換して、例外が出なかった文字コードを採用する、という方法でやってみます。

# get.py
import sys
import urllib
import encodings

def encode(s, c):
  for i in encodings.aliases.aliases:
    try:
      return unicode(s, i).encode(c), i
    except:
      pass
  return s, None

s, e =  encode(urllib.urlopen(sys.argv[1]).read(), 'euc-jp')
print e
print s
> python get.py http://www.google.co.jp
mskanji
<html><head><meta http-equiv="content-type" content="text/html; charset=Shift_JIS"><title>Google</title><style><!--
body,td,a,p,.h{font-family:arial,sans-serif}
.h{font-size:20px}
.h{color:#3366cc}
.q{color:#00c}
--></style>
<script>
<!--
function sf(){document.f.q.focus();}
// -->
</script>
</head><body bgcolor=#ffffff text=#000000 link=#0000cc vlink=#551a8b alink=#ff0000 onload=sf() topmargin=3 marginheight=
idth=100%><font size=-1><a href="/url?sa=p&pref=ig&pval=3&q=http://www.google.co.jp/ig%3Fhl%3Dja&usg=__zzRfDx_8QtfWxWHbI
https://www.google.com/accounts/Login?continue=http://www.google.co.jp/&hl=ja">ログイン</a></font></div><img alt="Google
br><form action="/search" name=f><table border=0 cellspacing=0 cellpadding=4><tr><td nowrap><font size=-1><b>ウェブ</b>&
o.jp/imghp?ie=Shift_JIS&oe=Shift_JIS&hl=ja&tab=wi">イメージ</a>&nbsp;&nbsp;&nbsp;&nbsp;<a class=q href="http://news.goog
...

まあとりあえずこれでいい、のかな……。

xmlrpclib

xmlrpclibを使ってみます。

基本的にはこれだけです。簡単ですね。以下ははてなの公開APIを使った例です。


はてなダイアリーキーワード連想語API

import xmlrpclib

s = xmlrpclib.ServerProxy('http://d.hatena.ne.jp/xmlrpc')
r = s.hatena.getSimilarWord({'wordlist': ['Hatena', 'Python']})
print ', '.join(w['word'] for w in r['wordlist'])

実行結果

ReportLab, はてな伝説, ISBN:, 言語, アプリ, :detail, モジュール, VM, ビルド, Nokia, 付属, シンプル, Guido van Rossum, ...

再帰的にゲット(数が多くなるので最大でも10個ずつに限定)

# getword.py
import sys
import xmlrpclib

def get(s, w, n, p=''):
  for i in s.hatena.getSimilarWord({'wordlist': w})['wordlist'][:10]:
    print p+i['word'].encode('euc-jp')
    if n:
      get(s, i['word'], n-1, p+'-')

s = xmlrpclib.ServerProxy('http://d.hatena.ne.jp/xmlrpc')
get(s, sys.argv[2:], int(sys.argv[1]))

実行例

> python getword.py 2 hatena apple
GUI
-MacOS
--はてなダイアリーFAQ「その他のFAQ」
--2000年
--マルチタスク
--NeXT
--業界
--パーソナルコンピューター
--Mac
--リリース
--クリエイター
--Apple社
-1984年
--柴田あゆみ
--Apple
--Macintosh
--6月13日
--金田美香
--10月7日
--ひまわり
--水泳
--1949年
--3月29日
...

はてなダイアリーキーワード自動リンクAPI

# -*- coding: euc-jp -*-
import xmlrpclib

s = xmlrpclib.ServerProxy('http://d.hatena.ne.jp/xmlrpc')
r = s.hatena.setKeywordLink({'body': 'はてなダイアリーのキーワードをリンクして!',
                             'score': 20,
                             'cname': ['book', 'movie'],
                             'a_target': '_blank',
                             'a_class': 'keyword'})
print r.encode('euc-jp')

実行結果

はてなダイアリーの<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%a5%ad%a1%bc%a5%ef%a1%bc%a5%c9">キーワード</a>をリンクして!

はてなダイアリーキーワード連想語APIで取得したキーワードをはてなダイアリーキーワード自動リンクAPIでリンクする(全カテゴリのキーワードをリンクするためにcnameを省略)

# getword_setlink.py
import sys
import xmlrpclib

s = xmlrpclib.ServerProxy('http://d.hatena.ne.jp/xmlrpc')
for w in s.hatena.getSimilarWord({'wordlist': sys.argv[1:]})['wordlist']:
  print s.hatena.setKeywordLink({'body': w['word'].encode('euc-jp'),
                                 'a_target': '_blank',
                                 'a_class': 'keyword'}).encode('euc-jp')

実行例

> python getword_setlink.py りんご
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%c5%a3%b5%dc%cd%fd%b7%c3">釘宮理恵</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%a5%ea%a5%f3%a5%b4">リンゴ</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/TOWER%20RECORDS">TOWER RECORDS</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/HIPHOP">HIPHOP</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%a5%ec%a5%d3%a5%e5%a1%bc">レビュー</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/2004%c7%af">2004年</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%a5%a2%a5%eb%a5%d0%a5%e0">アルバム</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/ken">ken</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%c8%af%c9%bd">発表</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/bounce">bounce</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%a5%c0%a5%e1%a5%ec%a5%b3">ダメレコ</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/MC">MC</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/Da%2eMe%2eRecords">Da.Me.Records</a>
<a class="keyword" target="_blank" href="http://d.hatena.ne.jp/keyword/%a5%ec%a1%bc%a5%d9%a5%eb">レーベル</a>

気がついたことなど

  • APIに食わせた結果として返ってくる文字列はunicode
    • printするだけなら勝手にコンソールの文字コードエンコードされる
    • リダイレクト(python getword_setlink.py りんご > r とか)したいときなどは明示的にencodeする必要がある
  • APIに日本語を食わせるときはunicodeのままではなく、何らかの文字コードにencodeする必要がある(だけど何故かutf8だと化けることがある)
    • ファイルの先頭で -*- coding: euc-jp -*- と書いて、同じ文字コードで保存すると、ファイル中の日本語は指定した文字コード(この場合はeuc-jp)であると認識される
    • sys.argvで日本語を受け取った場合、コンソールの文字コードになる

SimpleXMLRPCServer

SimpleXMLRPCServerを使ってみます。

  • 引数で (アドレス, ポート) というタプルを与えてSimpleXMLRPCServerのインスタンスを作る
  • register_instance、register_functionで公開するAPIインスタンスに登録
  • serve_foreverでlisten

基本的にはこれだけです。簡単ですね。

# server.py
class Foo:
  def add(self, a, b):
    return a+b

  def get_list(self, *a):
    return [str(i)+'!' for i in a]

  def get_dict(self):
    return {'a': 'apple', 'b': 'banana', 'o': 'orange', }

  def hello(self):
    print 'hello'
    return True

def sub(a, b):
  return a-b

import SimpleXMLRPCServer
SimpleXMLRPCServer.SimpleXMLRPCServer.allow_reuse_address=True
s = SimpleXMLRPCServer.SimpleXMLRPCServer(('localhost', 12345))
s.register_instance(Foo())
s.register_function(sub)
s.register_function(lambda x, y: x*y, 'mul')
s.serve_forever()
# client.py
import xmlrpclib

s = xmlrpclib.ServerProxy('http://localhost:12345')
print s.add(1, 2)
print s.sub(10, 9)
print s.mul(11, 11)
print s.get_list(1, 2, 3, 4, 5, 6, 'foo', 'bar')
print s.get_dict()
s.hello()

実行結果は以下のようになります

> python server.py
> python client.py
3
1
121
['1!', '2!', '3!', '4!', '5!', '6!', 'foo!', 'bar!']
{'a': 'apple', 'b': 'banana', 'o': 'orange'}
> python server.py
localhost - - [22/Oct/2006 23:22:33] "POST /RPC2 HTTP/1.0" 200 -
localhost - - [22/Oct/2006 23:22:33] "POST /RPC2 HTTP/1.0" 200 -
localhost - - [22/Oct/2006 23:22:33] "POST /RPC2 HTTP/1.0" 200 -
localhost - - [22/Oct/2006 23:22:33] "POST /RPC2 HTTP/1.0" 200 -
localhost - - [22/Oct/2006 23:22:33] "POST /RPC2 HTTP/1.0" 200 -
hello
localhost - - [22/Oct/2006 23:22:33] "POST /RPC2 HTTP/1.0" 200 -

localhost - -... というのはアクセスログです。鬱陶しい場合はサーバインスタンス生成時に

s = SimpleXMLRPCServer.SimpleXMLRPCServer(('localhost', 12345), logRequests=False)

とすることでログを表示しないようになります。helloと表示されているのはクライアントがメソッド hello() を呼び出した結果です。
また、

SimpleXMLRPCServer.SimpleXMLRPCServer.allow_reuse_address=True

とすることで サーバ起動 -> 停止 -> サーバ起動 を短い時間で行ったときに出がちなエラー、Address already in use を殺しています。


気がついたことなど

  • サーバに登録するインスタンスメソッド/関数はNone以外の値を返す必要がある
    • hello()で返り値を書かなかったら呼び出し時に例外が起きた
  • 文字列や数値だけではなく、リストや辞書なども結果として返すことができる

isinstance

isinstanceを使うと、オブジェクトが任意のクラスのインスタンスかどうかを調べることができます。

def exclam(o):
  if isinstance(o, str):
    return o + '!'
  elif isinstance(o, int):
    return reduce(lambda x, y: x*y, range(1, o+1), 1)

print exclam('hello')  # => hello!
print exclam(10)       # => 3628800
class Foo(object):
  pass

class Bar(Foo):
  pass

b = Bar()
print type(b)  # => <class '__main__.Bar'>

print isinstance(b, object)  # => True
print isinstance(b, Bar)     # => True
print isinstance(b, Foo)     # => True

親の親まで見に行ってくれるみたいです。


変数とクラスのシーケンスを渡して、変数のクラスがシーケンス中に一つでもあったらTrueを返す関数を書いてみました。

def type_check(var, types):
  return reduce(lambda x, y: x or isinstance(var, y), types, False)

a = 'apple'
print type_check(a, (int, long))  # => False

b = 123
print type_check(b, (str, int, long))  # => True

しかし、これには落とし穴があったのです……。

a = 'apple'
print isinstance(a, (int, long))  # => False

b = 123
print isinstance(b, (str, int, long))  # => True

isinstanceは第二引数でシーケンスを受け取って、上で書いた関数と同じ結果を返すのでした……。

二次元リストを縦に足す

l = [[1, 2, 3],
     [4, 5, 6],
     [7, 8, 9]]

print l  # => [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# 転置
print zip(*l)  # => [(1, 4, 7), (2, 5, 8), (3, 6, 9)]

# 転置した後にタプルになるのが嫌?
print map(list, zip(*l))  # => [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

# 転置した各行を足し合わせる
print [reduce(lambda x, y: [x[0]+y], i, [0]) for i in zip(*l)]  # => [[12], [15], [18]]

# 要素一つならリストじゃなくてもいい?
print [reduce(lambda x, y: x+y, i, 0) for i in zip(*l)]  # => [12, 15, 18]

文字列から二次元リストに変換する。

a = '''1,2,3
       4,5,6
       7,8,9'''

# 改行でsplitして得られたリストを','でsplitして、得られたリストの各要素をintに変換
l = [[int(i) for i in j.split(',')] for j in a.split('\n')]

print l  # => [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print [reduce(lambda x, y: [x[0]+y], i, [0]) for i in zip(*l)]  # => [[12], [15], [18]]

intは意外と柔軟性があるみたいですね……。

a = '''11,        12,   13
    14,  15   ,  16   
          17,18 ,  19'''

print [[int(i) for i in j.split(',')] for j in a.split('\n')]
# => [[11, 12, 13], [14, 15, 16], [17, 18, 19]]
>>> int('            12          \r               \n\n')
12
>>> int('\r\n   33\n')
33
>>> int('\r\n\n\n         -            \r\n\n            13 \n\n')
-13
>>> int('2 7')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ValueError: invalid literal for int(): 2 7