(この記事は、2018年にQiitaに上げた記事の再投稿です。内容が古くなっている可能性がありますのでご了承ください。)

はじめに

Pythonのコードは、インデントスコープやイテレータにより読みやすくすっきりとした見た目を実現しています。

for i in range(100):
    if i % 15 == 0:
        print('fizzbuzz')
    elif i % 3 == 0:
        print('fizz')
    elif i % 5 == 0:
        print('buzz')
    else:
        print(i)

でも、ちょっと待ってください。

Python(ニシキヘビ)なのに蛇らしくないですよね…

蛇はにょろにょろと地面を這っていくものです。 あなたのコードもPythonic(ニシキヘビ的)に改造しましょう!

from ___ import _
-_.__.___.__.___().__.___.____.____().__.____._.___().___._._().__.___.___.__().___._._().__.___.___.__().__.___.____.___().___._._().__.____._.___().__.___._.__().__.___.____.___().__.___.__.____().__.___.__.__().___.___._().____._.__().____._._().____._._().___.___.__().____.___.___().___.___().___._._().___._._().___._._().___._._().__.___.___.__().__.___.__.___().___._._().__.___.___.__().___._._().___.__.__().___._._().____._.__().____.__.__().___._._().____.____.__().____.____.__().___._._().____._._().____.___.___().___.___().___._._().___._._().___._._().___._._().___._._().___._._().___._._().___._._().__.____._._().__.____._.___().__.___.___.__().__.___.____.___().__.____.__._().___.___._().___.__.____().__.___.__.___().__.___.___.__().__.____.___.___().__.____.___.___().__.___._.___().__.____.__.__().__.____.___.___().__.____.___.___().___.__.____().___.___.__().___.___().___._._().___._._().___._._().___._._().__.___.__.__().__.___.____._().__.___.___.__().__.___.__.___().___._._().__.___.___.__().___._._().___.__.__().___._._().____._.____().___._._().____.____.__().____.____.__().___._._().____._._().____.___.___().___.___().___._._().___._._().___._._().___._._().___._._().___._._().___._._().___._._().__.____._._().__.____._.___().__.___.___.__().__.___.____.___().__.____.__._().___.___._().___.__.____().__.___.__.___().__.___.___.__().__.____.___.___().__.____.___.___().___.__.____().___.___.__().___.___().___._._().___._._().___._._().___._._().__.___.__.__().__.___.____._().__.___.___.__().__.___.__.___().___._._().__.___.___.__().___._._().___.__.__().___._._().____.__.__().___._._().____.____.__().____.____.__().___._._().____._._().____.___.___().___.___().___._._().___._._().___._._().___._._().___._._().___._._().___._._().___._._().__.____._._().__.____._.___().__.___.___.__().__.___.____.___().__.____.__._().___.___._().___.__.____().__.___._.___().__.____.__.__().__.____.___.___().__.____.___.___().___.__.____().___.___.__().___.___().___._._().___._._().___._._().___._._().__.___.__.__().__.___.____._().__.____._.____().__.___.__.__().____.___.___().___.___().___._._().___._._().___._._().___._._().___._._().___._._().___._._().___._._().__.____._._().__.____._.___().__.___.___.__().__.___.____.___().__.____.__._().___.___._().__.___.___.__().___.___.__().___.___()

あなたの知らない超絶技巧プログラミングの世界」で紹介されている「アンダースコアだけでHello, world!」(言語はRuby)のコードに感動し、 Pythonでも似たようなことができないかと考え作成しました。

仕組み

蛇コードは特殊メソッドを悪用して作成しています。 モジュールの中身は以下の通りです。

class __:
    def __init__(self, n=0, codes=''):
        self.n = n
        self.codes = codes

    def __getattr__(self, name):
        return __(self.n * 4 + len(name) - 1, self.codes)

    def __call__(self):
        return __(0, self.codes + chr(self.n))

    def __str__(self):
        return self.codes

    def __neg__(self):
        exec(self.codes)


_ = __()

変数 _ はクラス __ のインスタンスです。 モジュールを読み込むことで、インスタンス _ を読み込み、 _ のメソッドを使用して蛇コードを作っています。

蛇コードは以下の流れで実行されています。

  1. インスタンスは新しい文字の文字コード(int)とソース(str)を保持
  2. 存在しないインスタンス変数を呼び出して文字コードをセット
  3. インスタンスを関数呼び出しして文字コードを文字に変換しソースに追記
  4. printするとソースを表示
  5. 単項演算子 - をつけるとソースを実行

これを特殊メソッドでどのように実装したかを紹介していきます。

__getattr__ メソッド: 存在しないインスタンス変数の参照時に呼び出される

__ クラスのインスタンス変数は、asciiコードを表す整数 n とソース文字列 codes です。 これ以外のインスタンス変数(hogepiyo等)は存在しません。

__ クラスは __getattr__ メソッドをもっているので、 ここで _.hoge を参照すると AttributeError が起きる代わりに __getattr__ メソッドが呼び出されます。

そのため、 __getattr__ メソッドの戻り値を __ クラスのインスタンスとすることで _.hoge__ クラスのインスタンスとなり、 _.hoge.foo.bar のようにいくらでも属性をつなげられるようになります。

さらに、 __getattr__ メソッドは存在しなかった属性名(ここでは hoge )を引数にとれるので、

def __getattr__(self, name):
    return __(self.n * 4 + len(name) - 1, self.codes)

とすることで、属性のチェーンを使って n を属性の文字数による4進数で表すことができます。

_.__.n # 1
_.__._.n # 4 (4*1 + 0)
_.__._.____.n # 19 (4^2*1 + 4*0 + 3)

つづいて、asciiコードnを文字列に変換します。

__call__ メソッド: インスタンスを関数として呼び出し

クラスが __call__ メソッドを持っているとき、そのインスタンスは関数呼び出しができるようになり、 このメソッドが実行されます。

__ クラスでは __call__ メソッドが呼ばれると、インスタンス変数 n をasciiコードに変換し インスタンス変数 codes に追記したものを返します(この際nの値は0に戻します)。

def __call__(self):
    return __(0, self.codes + chr(self.n))

こうすることで、インスタンス変数 codes に任意の文字列をセットできるようになります。

# 1文字目
_.__.___.___._.n # 104 (asciiコードの"h")
_.__.___.___._.codes # ""
# ()で__.__call__を呼び出しあらたなインスタンス作成
# nのasciiコードをcodesに追記
_.__.___.___._().n # 0 (リセット)
_.__.___.___._().codes # "h"
# 2文字目
_.__.___.___._().__.___.___.__.n # 105 (asciiコードの"i")
_.__.___.___._().__.___.___.__.codes #"h"
# __.__call__呼び出し
_.__.___.___._().__.___.___.__().n # 0
_.__.___.___._().__.___.___.__().codes # hi
# 3文字目…

あとはこの文字列を表示・実行すれば完成です。

__str__ メソッド: インスタンスをstrに変換する( print() で表示する等)ときに呼び出される

このメソッドを持っているインスタンスは、文字列に変換することができます。

__ クラスでは、 __str__codes インスタンスを返すので、

print(_.__.____._._().__.____._.___().__.___.___.__().__.___.____.___().__.____.__._().___.___._().___.__.____().__.___.___._().__.___.__.__().__.___.____._().__.___.____._().__.___.____.____().___.____._().___._._().__.____.__.____().__.___.____.____().__.____._.___().__.___.____._().__.___.__._().___._.__().___.__.____().___.___.__())

により

print('hello, world!')

が表示されます。

__isub__ メソッド: 単項演算子 - をオーバーロードする

__isub__ の戻り値が - をつけた結果となります。

__ クラスでは、 __isub__None を返し、副作用として self.codesexec で実行しています。

例えば、

- _.__.____._._().__.____._.___().__.___.___.__().__.___.____.___().__.____.__._().___.___._().___.__.____().__.___.___._().__.___.__.__().__.___.____._().__.___.____._().__.___.____.____().___.____._().___._._().__.____.__.____().__.___.____.____().__.____._.___().__.___.____._().__.___.__._().___._.__().___.__.____().___.___.__()

によってコード print('hello, world!') が実行されます。

最後に: 手持ちのコードを蛇コード化したい!

以下のコードで変換できます。 “Pythonic"な見た目をお楽しみください。

import sys


def to_base_n(num, base):
	convertedNums = []
	while num:
		convertedNums.append(num % base)
		num //= base
	#昇順になっているので降順に直す
	convertedNums.reverse()
	return convertedNums


def encode_to_underlines(programmingCodes):
	#インスタンス名(チェーンのはじめ)
	nameChains = ['_']
	for char in programmingCodes:
		#asciiコードを4進数展開
		charNumbers = to_base_n(ord(char), base=4)
		#チェーンは4進数 (0表すために1つ余分に長くする)
		#ex: 104 = 1 * 4**3 + 2 * 4**2 + 2 * 4 ** 1 + 0 * 4**0 -> ____.___.___._()
		methodNames = '.'.join(['_' * (n + 1) for n in charNumbers]) + '()'
		nameChains.append(methodNames)
	return '.'.join(nameChains)

if __name__ == '__main__':
	fileName = sys.argv[1]
	with open(fileName, 'r', encoding='utf-8') as f:
		codes = f.read()
	print(encode_to_underlines(codes))