Edited at

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

2019/4/8: 誤植を1か所修正しました。


はじめに

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

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

__クラスでは、__str__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))