2008年9月22日月曜日

Python のジェネレータ (3) - 数字にコンマを振る

Python のジェネレータ (2) のつづき

1. ジェネレータを使う方法

前回と同じく、ジェネレータを使う例を考える。

Python のチュートリアル 5.5.7 レシピ に、moneyfmt() という関数が書かれている。とりあえず、これは横に置いておく。

「数字にコンマを振る」ために、ジェネレータを使うことをイメージする。

  1. 数をリストに見たて、末尾から 3 つずつ要素を返すジェネレータを作成。
  2. そのジェネレータからリストを生成し、
  3. 要素を逆順にして、
  4. 最後にコンマで要素をくっつける。

080922-002

実装。

def gCommaStr(num):
    result = ""
    for i,e in enumerate(reversed(str(num))):
        if not i == 0 and i % 3 == 0:
            yield result
            result = ""
        result = e + result
    yield result

print ",".join(list(reversed([x for x in gCommaStr(1234567890)])))

一応、コンマを打つ位置を可変にしておくと、

def gCommaStr2(num,n):
    result = ""
    for i,e in enumerate(reversed(str(num))):
        if not i == 0 and i % n == 0:
            yield result
            result = ""
        result = e + result
    yield result

print ",".join(list(reversed([x for x in gCommaStr2(1234567890,3)])))

 

2. 再帰を使った方法

リスト操作なので、ついでに再帰の練習もしておこう。

数が小さいときは、

1 --> 1
12 --> 12
123 --> 123

そのまま返す。

コンマがつけられる最小の数値のリストは、

1234 --> 1,234

これが再帰をするときの基本的なコンポーネントになりそうだ。

続きを考えると、

12345 --> 12,345
123456 --> 123,456
1234567 --> 1,234,567
12345678 --> 12,345,678

ぼ~っとながめていると、何となく繰返しになっている構造が見えてくる。 (@_@;) 上記の最後のパターンを見ていたら、 「2,345」 と 「5,678」 というように分割して考えれば、先ほど考えた基本的なコンポーネントが結合した形と見ることができることに気がついた。  「5」  が重複しているが、これは再帰呼出しの後に削除すればいいような感じ。 「12345」 なら、 「12」 と 「2,345」 。 「123456」 なら、 「123」 と 「3,456」 というような部分に分割できると考える。

080922-006

これで数値が大きくなっても、基本的なコンポーネントとそれ以外の部分に分けて考え、関数を再帰的に適用すればいいような気がしてきた。

080922-005

さて、実装~ (+_+)

def recCommaStr(L):
    if len(L) <= 3:
        return L
    elif len(L) == 4:
        L.insert(1, ","); return L
    else:
        return recCommaStr(L[:-3])[:-1] + recCommaStr(L[-4:])

num = 1234567890
print "recCommaStr(L): " + "".join(recCommaStr(list(str(num))))

引数 L は、数値を文字列にした後、リストにしたものを渡すようにした。

何も知らずにこのコードを見たら、読む気が失せるけれど ^^; 、わかっていれば再帰的な実装はやり方を素直に実装しているように感じる。

これも同じくコンマが打たれる位置を指定できるように拡張すると、

def recCommaStr2(L,i):
    if len(L) <= i:
        return L
    elif len(L) == i+1:
        L.insert(1, ","); return L
    else:
        return recCommaStr2(L[:-i],i)[:-1] + recCommaStr2(L[-(i+1):],i)

num = 1234567890
print "recCommaStr2(L,i): " + "".join(recCommaStr2(list(str(num)),3))

引数 i がコンマの位置を現わす。

 

3. 割り算で分割する方法

あ~、そうだ。数値を 1000 で割っていけば、コンマの打ちたい分の数値だけ取得できるかぁ。

def divCommaStr(num):
    result = str(num) if num < 1000 else ""
    while num > 1000:
        result = "," + str(num % 1000) + result
        num /= 1000
        if num < 1000:
            result = str(num) + result
    return result

print "divCommaStr(1234567890): " + divCommaStr(1234567890)

あれ?何かややこしい。 (+_+) こういうの考えるの苦手。脳みそ混乱してきた。 パタッ(o_ _)o~†

 

コンマの位置を可変にすると、

def divCommaStr2(num, i):
    assert i > 0
    divNum = 10 ** i
    if num < divNum: return str(num)

    result = ""
    while num > divNum:
        result = "," + str(num % divNum) + result
        num /= divNum
        if num < divNum:
            result = str(num) + result
    return result


print "divCommaStr2(1234567890, 3): " + divCommaStr2(1234567890, 3)

 

4. クラスを使った方法

クラスを使って実装してみる。コンマ付きの数字を表わす CommaStr クラスを作成。コンマを付けるのは、文字列としてオブジェクトの情報を出力するときに、末尾から数値を 3 つ置きに走査する時点でつけることにした。

class CommaStr:
    def __init__(self, num, i):
        self.i = i
        self.nums = str(num)
            
    def __str__(self):
        result = ""
        for i,e in enumerate(reversed(self.nums)):
            if not i == 0 and i % self.i == 0:
                result = e + "," + result
            else:
                result = e + result
        return result

print "CommaStr(1234567890, 3):", CommaStr(1234567890, 3)

これだとクラスを使う意味があまりないなぁ。 ^^; グローバル変数と関数を組み合わせる方法と大差なし。でも、クラスで考えるというのは何となくイメージしやすい。

 

自分の好みとしては、同様にコンマ付きの数字全体を CommaStr クラスで表わし、各数字を Num クラスとする。 Num クラスには、インスタンス変数として数値とコンマをつけることができるフィールドを用意しておく。

080922-008

実装。

class CommaStr2:
    def __init__(self, num, i):
        self.i = i
        self._createNums(num)

    def _createNums(self, num):
        self.nums = []
        for e in str(num):
            self.nums.append(Num(e))
        self.setComma(",")
            
    def setComma(self, comma):
        for i,e in enumerate(reversed(self.nums)):
            if not i == 0 and i % self.i == 0:
                e.comma = comma

    def __str__(self):
        return "".join(str(x) for x in self.nums)

class Num:
    def __init__(self, num, comma=""):
        self.num = num
        self.comma = comma
    
    def __str__(self):
        return str(self.num) + self.comma

print "CommaStr2(1234567890, 3):", CommaStr2(1234567890, 3)

全体は長くて効率も悪いけれど、全体の構造の見通しがよく、各メソッドはシンプルでええわぁ~。 ^^

Python のジェネレータ (4) - 無限リスト につづく…