有効数字で浮動小数点数を丸める

浮動小数点数の時系列データを圧縮するのに xor encoding という手法があるそうだ。

値が時系列に 15.5, 14.0625, 3.25, 8.625 というように並んでいたとする。

それぞれを倍精度浮動小数点で表すと、ビット列は下のようになる。

decimail double precision
15.5 01000000 00101111 00000000 00000000 00000000 00000000 00000000 00000000
14.0625 01000000 00101100 00100000 00000000 00000000 00000000 00000000 00000000
3.25 01000000 00001010 00000000 00000000 00000000 00000000 00000000 00000000
8.625 01000000 00100001 01000000 00000000 00000000 00000000 00000000 00000000

時系列データというのは、同じ値が続いたり、前後の値が近いという特徴がある。そのとき、近い数字はビット列の並びも近いとすると、前の値ビット列と XOR を取ると、ビットが立っているところが集中するようになる。

00000000 00000011 00100000 00000000 00000000 00000000 00000000 00000000
00000000 00100110 00100000 00000000 00000000 00000000 00000000 00000000
00000000 00101011 01000000 00000000 00000000 00000000 00000000 00000000

ビットが立っているところのデータだけ記録すれば、保存に必要なディスク容量が少なくなるだろうというアイデアです。ビットが立っているところだけ記録する方法は Gorilla の論文を参照してください。

しかし、この手法は本当にいいのだろうか?

浮動小数点数のバイナリデータをダラダラと吐き出すシステムもないわけではないのだろうが、ほとんどの場合10進数のテキストデータが渡されるのではなかろうか。10進数を浮動小数点数に厳密に変換することはできません。例えば、 3.14 を倍精度浮動小数点にしたときのビット列は

01000000 00001001 00011110 10111000 01010001 11101011 10000101 00011111

となります。これを10進数に戻すと 3.14000000000000012434… となります。

3.13 を倍精度浮動小数点にしたら、

01000000 00001001 00001010 00111101 01110000 10100011 11010111 00001010

となるのですが、3.14 と XOR をとったら

00000000 00000000 00010100 10000101 00100001 01000000 01010010 00010101

です。2進数でキリが悪い数字となっているので、値は近いにも関わらず、ビット列はそこまで一致していません。

そもそも、3.14, 3.13 は有効数字は3桁であり、倍精度浮動小数点は10進数で約16桁の精度なので、10進数をそのまま浮動小数点数に直すと無効な桁も表現してしまっていることになります。有効数字だけ残して、無効な桁を表現するビットを丸めてしまえば、もっと効果的にデータを圧縮できそうです。

3.14 の場合は、下のように有効数字を示す右側のビットを落としてしまってよいでしょう。

丸めなし 01000000 00001001 00011110 10111000 01010001 11101011 10000101 00011111
丸めあり 01000000 00001001 00100000 00000000 00000000 00000000 00000000 00000000

有効数字3桁で丸めた浮動小数点を10進数に戻すと、 3.140625 となります。これが2進数ではキリのよい数ということです。記事の最後にこのような丸めを実現するプログラム例を載せておきます。

テキストの 10進数 → 有効数字で丸めた浮動小数点 → xor encoding

という風に変換していけば有効数字を維持したまま時系列データを圧縮できるのではないかと考えました。

実際にやってみました。Gorilla の論文に載っている符号化は実装が面倒すぎたので、代わりに前の値との xor 値を並べたバイト列を圧縮してどの程度効果があるのか試した。

sar でとった 1秒おきの PC の CPU 使用率のデータで試してみたら、以下のような結果になりました。

サイズ [bytes]
もとのテキストデータ 70062
倍精度浮動小数点数に変換後 XOR 値をとって gzip 圧縮 29357
有効数字で丸めた浮動小数点数に変換後 XOR 値をとって gzip 圧縮 26887

一応、効果はあるようです。しかし、普通にテキストファイルを gzip 圧縮したら 18431 bytes となって、最も有効だった。PC の CPU の使用率は変動が激しすぎて、そもそも xor encoding が有効でないのだろう。滑らかな変動をするようなデータで試した方が良かったのかもしれない。


import argparse
import math
import re
import struct
import sys


class DoublePrecisionFloat:
    def __init__(self):
        self.bits = None

    @staticmethod
    def from_float(f):
        instance = DoublePrecisionFloat()
        instance.bits = struct.unpack('>Q', struct.pack('>d', f))[0]
        return instance

    @staticmethod
    def from_string(string):
        f = float(string)
        return DoublePrecisionFloat.from_float(f)

    @staticmethod
    def from_bits(bits):
        instance = DoublePrecisionFloat()
        instance.bits = bits
        return instance

    @property
    def raw_float(self):
        return struct.unpack('>d', self.bits.to_bytes(8, byteorder='big'))[0]

    def to_binstr(self):
        bb = self.bits.to_bytes(8, byteorder='big')
        return ' '.join(['{:08b}'.format(b) for b in bb])

    def to_hexstr(self):
        bb = self.bits.to_bytes(8, byteorder='big')
        hex2bytes = ['{:02x}{:02x}'.format(bb[i], bb[i + 1])
                     for i in range(0, len(bb), 2)]
        return ' '.join(hex2bytes)

    @staticmethod
    def __significant_bits(significant_figures, base):
        return int(math.ceil(
            significant_figures * math.log2(base)))

    # 11111111 11110000 00000000 ... 00000000
    # |<----------><------  fraction  ------>
    # |  exponent
    # sign
    __FILTER = int.from_bytes(
        b'\xFF\xF0\x00\x00\x00\x00\x00\x00',
        byteorder='big',
        signed=True)

    def round_off(self, significant_figures, base=10):
        # 0         0.1         1
        # |<-------------------||
        #        round off
        sig_bits = self.__significant_bits(significant_figures, base)
        # left side is padded with 1
        filt = self.__FILTER >> sig_bits
        new_bits = self.bits & filt
        return DoublePrecisionFloat.from_bits(new_bits)

    def round_up(self, significant_figures, base=10):
        # 0         0.1         1
        # ||------------------->|
        #        round up

        sig_bits = self.__significant_bits(significant_figures, base)
        # left side is padded with 1
        filt = self.__FILTER >> sig_bits

        # TODO: the case of overflow
        new_bits = (self.bits + (~filt)) & filt
        return DoublePrecisionFloat.from_bits(new_bits)

    def round(self, significant_figures, base=10):
        # 0         0.1         1
        # |<--------||--------->|
        #  round off   round up

        sig_bits = self.__significant_bits(significant_figures, base)
        filt = self.__FILTER >> sig_bits
        round_up_bit = 0x0010000000000000 >> (sig_bits + 1)
        new_bits = (self.bits + round_up_bit) & filt
        return DoublePrecisionFloat.from_bits(new_bits)

    def xor(self, other):
        return DoublePrecisionFloat.from_bits(self.bits ^ other.bits)


class FloatString:
    def __init__(self, string):
        self.string = string

    @property
    def significant_figures(self):
        # TODO: This code is ugly

        string = self.string

        # 0.0
        f = float(string)
        if f == 0.0:
            return 0

        # 0.0123
        pattern1 = r'[+-]?0\.0*(\d+)'
        match1 = re.search(pattern1, string)
        if match1:
            return len(match1.group(1))

        # 123.4 or 123.40
        pattern2 = r'[+-]?0*([1-9]+\d*\.\d+)'
        match2 = re.search(pattern2, string)
        if match2:
            return len(match2.group(1)) - 1

        # +1E+10 or -1e-10
        pattern3 = r'[+-]?(\d+)[eE][+-]\d+'
        match3 = re.search(pattern3, string)
        if match3:
            return len(match3.group(1))

        # +1.23E+10 or -1.23e-10
        pattern4 = r'[+-]?0*(\d\.\d+)[eE][+-]\d+'
        match4 = re.search(pattern4, string)
        if match4:
            return len(match4.group(1)) - 1

        # 1234
        pattern5 = r'[+-]?0*(\d+)'
        match5 = re.search(pattern5, string)
        if match5:
            return len(match5.group(1))

        raise ValueError("could not get significant figures: '%s'" % string)

    def to_rounded_float(self):
        d = DoublePrecisionFloat.from_string(self.string)
        sigfig = self.significant_figures
        return d.round(sigfig)

    def to_nonrounded_float(self):
        d = DoublePrecisionFloat.from_string(self.string)
        return d


def main():
    parser = argparse.ArgumentParser()
    out_formats = parser.add_mutually_exclusive_group()
    out_formats.add_argument('-b', action='store_true',
                             help='print results with binary strings')
    out_formats.add_argument('-x', action='store_true',
                             help='print results with hex strings')
    out_formats.add_argument('-f', action='store_true',
                             help='print results with decimal number')
    out_formats.add_argument('-n', action='store_true',
                             help='not round results')
    args = parser.parse_args()

    if args.b is True:
        write_func = lambda f: print(f.to_binstr())
    elif args.x is True:
        write_func = lambda f: print(f.to_hexstr())
    elif args.f is True:
        write_func = lambda f: print(str(f.raw_float))
    else:
        write_func = lambda f: sys.stdout.buffer.write(
            struct.pack('>d', f.raw_float))

    def stdin():
        try:
            while True:
                yield input().strip()
        except EOFError:
                pass

    for line in stdin():
        if args.n is not True:
            rounded = FloatString(line).to_rounded_float()
        else:
            rounded = FloatString(line).to_nonrounded_float()
        write_func(rounded)

if __name__ == '__main__':
    main()

プログラミングと英語

英語ができなかったら、プログラミングはできるとは言えないのではないか。英語ができなければ近いうちにソフトウェアエンジニアと名乗れなくなるのではないかと思う。私は英語ができない側の人間だから、こんなことを考えるのだと思いますが。

日本社会として今は空前の英語ブームです。本屋に行けば英語教材の棚の占める面積は大きい。昇進や入社に TOEIC テストの点数を課している企業も多いので、多くの人々が英語の勉強をするべく、教材を買っている。従業員に英語力を課す企業は英語が必要になる方に賭けるという経営判断をしたのだろう。日本人の会社員の皆が英語を読んだり書いたり話したりできる必要があるのかは、私には分からない。英語を課した企業が賭けに勝つのかどうかは、おいおい結果が出るだろう。

企業として英語化が良いのかは分からないが、個人としては良いことなのは間違いないと思っている。これだけ英語ブームだったら、英語ができたら給料が上がる。従業員に英語力を課した企業は当然英語ができる人の給料を上げなければならない。*1一方で英語なんてどうでもいいと思っている企業は安い給料で英語ができない人を雇えるのだから、人件費の削減になる。企業として英語ができる人を比較的高給で雇うか、できない人を安く雇うかのどっちが得かは、私には分からないが、個人としては英語を覚えた方が得だろう。

上記は会社員一般についての議論だが、ソフトウェアエンジニアもしくはプログラマに関しては、英語ができなかったらそもそも仕事にならないのではないかと思ったりする。理由は二つで、一つは英語が読めなかったらマニュアルが読めない;もう一つは英語の読み書きができなかったらソースコードの読み書きができないことです。

インターネットがこれだけ普及した現在、我々日本人がアクセス可能な情報であっても、ほとんどの情報は英語で書かれている。知っているか知らないか――エンジニアは専門職なのだから専門知識を有していることが職業的な優位性を担保している。同じ問題に直面している人は世の中には多いものなので、既に世の中には問題の解決策が存在していることがしばしばある。普通にマニュアルを読めば答えが書いてあったりするし、Stack Overflow にヒントが載っていたりすることもある。それを知っていれば問題は素早く解決できる。エンジニアってそのために雇われているのでしょう? もちろん、Stack Overflow や Blog ポストに載っていることが正しいこととは限らないので、裏取りが必要なのは言うまでもない。隣に座っている人の情報の信憑性も同程度ではあるので、正しい情報源としては結局マニュアルを当たる必要があり、マニュアルが英語だった場合はそれが読めなければ困ったことになってしまう。いわゆるオープン系のシステムは、知らない人が作った部品を使うことがその定義であり、ほとんどの場合英語で書かれているマニュアルを読めなければ、仕事にならない。

マニュアルに限らず、ソースコードだって英語で書くものだ。英語ができなくても coding つまり機械に読めるような符号を書くことはできるが、例えば整数に全て i をつけるようなことはできるが、それはプログラミングではない。符号化はコンパイラの仕事であって、プログラマがやることではない。私は、「設計=名付け」と考えていて、名付けができていなければ、設計をしていないと思っている。英語ができなければ名付けができないので、英語ができないならプログラミングができないという考えになる。ただ動くだけのものだったら、別に英語力なんて要らないが、ただ動くだけのソフトウェアが極端に組織の足を引っ張るのは、誰しも経験して知るところだろう。

私がソフトウェアエンジニアを雇うなら、英語ができることを課す。自分が英語を出来なきゃ、英語ができる人のマネジメントなんてできないので、ちょっとは勉強しなければならないなと強く思う今日このごろ。

*1:英語のできないおじさんの給料は下げられないが、若い人には語学力を求めるような、おかしな会社もあるかもしれないが、そんな会社はさっさと辞めた方がいいと思う。

Bloom Filter を作ってみた

Bloom Filter を実装してみた。簡単な実装なので、速度や空間効率は悪いです。

Bloom Filter というのは、確率的データ構造の一つで、ある要素が集合に含まれるかどうかを試験するものです。空間効率が非常に良いのが利点で、偽陽性、つまり集合に含まれない要素もあるとしてしまう場合があるという欠点があります。偽陰性はありません。

以下が実装例です。

import hashlib
import math

class BloomFilter:

    def __init__(self, num_item, false_positive):
        assert 0 <= false_positive <= 1
        # Optimal number of bit array size and hash function
        # https://en.wikipedia.org/wiki/Bloom_filter#Optimal_number_of_hash_functions
        log2 = math.log(2)
        logp = math.log(false_positive)
        size = - num_item * logp / (log2 * log2)
        num_hash = size / num_item * log2

        self.size = int(math.ceil(size))
        self.num_hash = int(math.ceil(num_hash))

        self.array = self.BitArray(self.size)
        self.hash_functions = [self.HashFunction(i, self.size)
                               for i in range(self.num_hash)]

    def add(self, item):
        for hash_func in self.hash_functions:
            i = hash_func(item)
            self.array[i] = True

    def __contains__(self, item):
        return self.contains(item)

    def contains(self, item):
        for hash_func in self.hash_functions:
            i = hash_func(item)
            if self.array[i] is False:
                return False
        return True

    class BitArray:

        def __init__(self, length):
            # Allocate an object per a bit for simplicity
            self.array = [False for _ in range(length)]

        def __getitem__(self, index):
            return self.get(index)

        def get(self, index):
            return self.array[index]

        def __setitem__(self, index, bit):
            return self.set(index, bit)

        def set(self, index, bit):
            if bit > 0 or bit is True:
                self.array[index] = True
            else:
                self.array[index] = False

    class HashFunction:

        def __init__(self, seed, maximum):
            self.seed = seed.to_bytes(
                seed.bit_length() // 8 + 1, byteorder='big')
            self.maximum = maximum

        def __call__(self, byte_buffer):
            return self.call(byte_buffer)

        def call(self, byte_buffer):
            # MD5 is enough because I don't want to oneway hash function
            b = hashlib.md5(byte_buffer + self.seed).digest()
            i = int.from_bytes(b, byteorder='big', signed=False)
            return i % self.maximum

要素の数と偽陽性の確率を指定します。

>>> bf = BloomFilter(num_item=1000, false_positive=0.01)

アイテム数 1000 で偽陽性率が 1% で、たったの 9856 bit = 1232 bytes にデータが収まります。

>>> bf.size
9586

要素を入れて、入っているかどうかが判定できる。

>>> bf.add(b'1')
>>> b'1' in bf         # 必ず正しい
True
>>> b'2' in bf         # たまに間違う
False

面白いですね。

異常時にちゃんと止まるシェルスクリプト

自動化は大切ですが、自動化するときは異常時にちゃんと止まるようにしなければなりません。トヨタ自動車でいうところのニンベンのついた「自働化」である。なぜ自働化が必要かというと、エラー時に止まってくれない機械は稼働中に人が張り付いて見守っていなければならないからです。また、エラーが起こったのちに機械が動き続けるならば、異常なアウトプットが出力され続けることになり、異常なアウトプットを処理しなければならないというムダが発生します。自動化のためのスクリプトを書くときは、エラー時には止まるように作らなければなりません。

しかしながら、シェルスクリプトは恐ろしいことにエラーが起きても既定ではそのまま処理を続けていきます。異常時にちゃんと止まる自働化スクリプトとするためには以下をスクリプトの先頭に書くと良いでしょう。

set -eu -o pipefail
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
set -e
set -e
echo 1
false
echo 2

上記のシェルスクリプトを実行すると、以下のように false のところ、つまり 0 以外で exit したところで止まります。

$ bash sete.sh
1
$ echo $?
1
trap

しかし、上記ではどこで止まったのかが分からないので、どこで止まったのかを trap を用いて表示するようにします。

set -e
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
echo 1
false
echo 2

trap に第1引数で指定されたコマンドがエラー発生に実行されます。 LINENO 変数には現在の行番号が格納されています。実行と以下のようにエラー発生時の行番号と exit status が標準エラー出力に表示されて止まります。

$ bash traperr.sh 
1
ERROR: line no = 4, exit status = 1
$ echo $?
1
pipefail

set -e だけだとコマンドをパイプでつないだ場合に上手く止まらない場合があります。

set -e
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
echo 1
false | true
echo 2

を実行すると、 false コマンドが失敗しているはずなのに、止まりません。

$ bash pipefail1.sh 
1
2
$ echo $?
0

set -o pipefail をつけるとパイプの途中のコマンドが失敗しても、止まります。

$ cat pipefail2.sh 
set -e -o pipefail
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
echo 1
false | true
echo 2
$ bash pipefail2.sh 
1
ERROR: line no = 4, exit status = 1
$ echo $?
1
set -u

set -u を指定すると宣言してない変数が使用されるとエラーになります。

$ cat setu.sh 
set -u
echo 1
echo $AAA
echo 2
$ bash setu.sh 
1
setu.sh: line 3: AAA: unbound variable

自働化は運用時の話なので、タイポなんてのは運用前になくなっているはずなので、運用時にはなくてもいいですが、間違っているのに動いちゃっているという状態にならないためにつけておくべきでしょう。

プログラムのレイヤーは降りれても登れない

プログラムにはレイヤーというものがあります。レベルとも呼びます。プログラムというのは人間の理解を超えて複雑なものなので、全容を知るということは人間には出来ません。理解できない複雑なものを理解するために、人間には便利な思考法が備わっています:抽象化という思考法です。抽象思考がどういうものなのかは、説明が長くなるので、ここでは省きますが、プログラムの世界もレイヤー化という一種の抽象思考によって驚くほど複雑なものが作られてきました。一般に、カーネルなど生の機械を触るプログラムを低レイヤー(低レベル)と言い、アプリケーションの画面など人間に近いプログラムを高レイヤー(高レベル)と言います。

ソフトウェアエンジニアとざっくり呼ばれますが、レイヤーごとに仕事をしている人が異なります。高レイヤーのエンジニアはアプリケーションエンジニアと募集されていることが多いですし、比較的低レイヤーのプログラミングは最近ではシステムプログラミングと呼ばれます*1。同じプログラミングではあるけれども、求められている知識やスキルが微妙に異なるので、レイヤー毎にエンジニアの棲み分けが行われています。

低レイヤーのエンジニアは、高レイヤーのエンジニアより自分は偉いと思っていることがしばしばあります。私が知っているカーネルの開発をしている人は、職業欄に “Kernel Developer” とわざわざ書いています。俺は特別だと思っているのでしょう。鼻には付きますが、自分の仕事に誇りを持つことは悪いことではないです。

しかしながら、そのよく分からない高慢がエンジニアのキャリアや企業の収益の可能性を制限してしまっているのではないかと思います。

低レイヤープログラマは「低レイヤーができたら高レイヤーもできるが、逆はできない」と言うのですが、これは嘘です。使う人の気持ちになれないからです。作ったけど何に使うかは分からないと、恥ずかしげもなく言う程度の低い人がいる。そういう人々は、自分の仕事は難しいのだと偉そうにしているけれども、低レイヤーのプログラミングすら実はまともにできないのだ。

さらに、現在では、低レイヤーのソフトウェアを作ることが大して金にならなくなっている。Linux のように OSS として公開されているものが、選ばれるようになっているからだ。それに、例えば Google みたいな低レイヤーの製品で利益を得ていない企業が、自分らが使うために作ったが、隠し持っていても競争力に繋がらないし、広く使われるほうが有利だと OSS を公開している。

使い道を分かっている人が作って無償で公開しているものがあるのに、どうして使い道も考えずになんとなく作っている製品を買う人がいるのだろうか。無償で配布されているものにすら及ばないものしか作れないなら、その仕事に1円の価値もないのではないかと思う。

そんな事業辞めて、もっと高いレイヤーに移れば良いのにと思うが、それができない。売るレベルのものが作れないとしても、一回覚えたことにこだわってしまうものだ。それに程度の低いプログラマが高レイヤーのプログラムを書くとそれはそれはひどいものができる。なんとなく動くし、性能はいいのかもしれないが、抽象度が整っていない難読でメンテ不可能な代物になる。また、使い道が分かっていないというのは変わらないので、作ったものに商品性はない。レイヤーを移るのは無理なので、仕事を維持するための仕事をして1円の価値もないものを作り続けるしかないのだろう。

お前らはそれしかできないかもしれないが、俺は他のことができるのに、どうしてお前らの仕事を守るためだけのことをしなければならないのか――と思っている人は、さっさと金になるレイヤーに移った方がいいと思う。使い道が分かるようになったら、再びレイヤーを降りてプロダクトマネージャー的なことをやったら良い。その方がキャリアもそうだし、社会のためにもなると思う。

*1:システムプログラミングをする人のことは何と呼ぶのだろう?インフラエンジニア?

正しい人間になりたい

このところずっと心に思っていることがある、ちょっとはマシな人間になりたい。恥の多い生涯を送って来ましたが、そろそろお天道様に顔向けできるような人生を歩みたいです。

マシな人間とは正しいことをする人のことです。正義を信じ、正義を貫ける人に私はなりたい。私は絶対に正しいのだという自信を持った行いをしたい。

何をもって正義とするか。もちろん世界には多様な価値観があって、それぞれがそこでの生活体系の中では正しかろうということは認めている。私の正義は、私が持っている価値観の体系で正しいことである。私が正しいと思うことが私の暮らす共同体の中でも正しいかどうかを疑ったら話が進まないので、私は道徳を知っていると信じる。

むしろ年を取ると、正しいことを知っているはずなのに、分からなくなってしまうのが怖い。世の中には「馬鹿には見えない服」が多い。裸の王様はやっぱり裸であると、自分の感性には自信を持つべきだ。私も含めて言われた後に「そんなの当たり前だ」みたいなことを口にするのだが、経験だとか知識だとかが付いてしまうと当たり前のことにさえ言われるまで気付かなくなってしまう。

すべての正義を認識できていないのかもしれないが、現状知りうる正義をもってして、私はそれに従っているのか。恥ずかしながら不正義な行いをしている。悪行はしていないが、今日お前は悔いのない生を過ごしたかと尋ねられれば、確信をもって肯定することはできない。遅くに起きたし、頭ボサボサで、部屋は散らかったままだし、やろうと決めていたこともできていない。こんなちっぽけなことだけではなくて、一生後悔するようなこともこれまで多くした。10年近く無駄に過ごしている。生きているからいいのだけれど、余生もこのまま過ごすは嫌だわ。

正しい行いをしたい。私は絶対に正しいと確信をもって、日々を過ごしたい。お天道様にも世間様にも、恥ずかしくない生き方をしたいものです。

不正義な行いの何が良くないかというと、それを一旦してしまうと、どんどん悪い方に向かっていくところだ。ひとつ後ろめたいことがあったら、ふたつめもみっつめも不正義が気にならなくなってくる。そして常に良くない行いを選択するようになってしまう。また、ちょっとでも後ろめたいことがあったら、正しいことをする際の行動力まで下がる。私は断じて間違っていない・私は正しいと信じている人間は強い。

不正義というのは軽微であっても許すことができない割れ窓なのです。常に正しいことをするという、ゼロ・トレランスの精神で戦わなければならない。戦うといって、誰と戦うかというともちろん自身とである。まあ、そんな気張るようなことでもなくて、迷いだとか躊躇だとか怠けだとかをせずに、ただ自分の正しいと思うこと・当たり前だと思うことをするだけです――それが出来ないわけですが。

また、周囲とも戦わなければならない。自分の正義を貫くなんて、わがままな生き方です。お前らが間違ったことをしていようが俺は正しいことしかしない、そしてお前らも正しくあれ、と言っているわけですから。後ろめたさがちょっとでもあったらそんなことも言えないので、自由が効く範囲では隙のない正義を実行している必要はあります。わがままだとは思うのだけれど、後悔はしたくないので正しいわがままは通そうと思った。

ほとんど全員がこれは正しくないのではないかと思っていても、なあなあで正しくないことを続けているところってあるじゃないですか。「言っていることは正しい、でも……」だとか、「正論を言っても仕方ないじゃないか」*1とか、金輪際聞かない。今後は怒るようにしたい、そして無視するようにしたい。面倒くさいみたいな理由で私もそういうこと言ったことがあるけれど、それで何の進歩もないような場所には居たくない。もう決して言わない。

正義を貫く――これを格率として、ちょっとはお天道様に顔向けできるような日々を過ごしたいと思う今日このごろ。

心の欲する所に従いて矩を踰えず

こんな意識高いようなことを考えなくても、自然と実行できる境地に達したら、立派な人間になれたと言えると思うが、それは無理かな。

*1:一般的には正しいが個別の場合には正しくないならそれは正論でない。

inline_table ― ソースコードに ASCII テーブルを埋め込むための Python モジュール

inline_table という名前のソースコードに ASCII テーブルを埋め込むための Python モジュールを作ってみました。

GitHub - fjkz/inline_table: Python module for embedding text tables into source-code

以下のように reStructuredText で書かれたテーブルをソースコードに埋め込むことが出来ます。

>>> import inline_table
>>> t1 = inline_table.compile('''
...     === === ====
...      A   B   AB
...     === === ====
...      1   1  '1'
...      1   2  '2'
...      2   1  '2'
...      2   2  '4'
...     === === ====
...     ''')
>>> t1.select(A=1, B=2)
Tuple(A=1, B=2, AB='2')

if 文を並べるよりコンパクトで読みやすいと思うのですが、どうでしょう?

テーブルに値を渡すこともできます。関数なども渡せるので便利かと思います。

>>> t2 = inline_table.compile('''
...     === =====
...     key value
...     === =====
...      1   a
...      2   b
...     === =====
...     ''',
...     a='A', b='B')
>>> t2.select(key=1)
Tuple(key=1, value='A')