Python
ネタ

コードをもっとPythonic(ニシキヘビ的)に!任意のPythonコードをアンダースコアでワンライナー化

はじめに

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

fizzbuzz.py
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(ニシキヘビ的)に改造しましょう!

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

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

仕組み

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

___.py
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()で表示する等)ときに呼び出される

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

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

により

print('hello, world!')

が表示されます。

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

__isub__の戻り値が-をつけた結果となります。
__クラスでは、__isub__Noneを返し、副作用としてself.codesexecで実行しています。
例えば、

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

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

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

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

encode.py
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))