コードをもっとPythonic(ニシキヘビ的)に!任意のPythonコードをアンダースコアでワンライナー化
(この記事は、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)
_ = __()
変数 _
はクラス __
のインスタンスです。
モジュールを読み込むことで、インスタンス _
を読み込み、
_
のメソッドを使用して蛇コードを作っています。
蛇コードは以下の流れで実行されています。
- インスタンスは新しい文字の文字コード(int)とソース(str)を保持
- 存在しないインスタンス変数を呼び出して文字コードをセット
- インスタンスを関数呼び出しして文字コードを文字に変換しソースに追記
- printするとソースを表示
- 単項演算子
-
をつけるとソースを実行
これを特殊メソッドでどのように実装したかを紹介していきます。
__getattr__
メソッド: 存在しないインスタンス変数の参照時に呼び出される
__
クラスのインスタンス変数は、asciiコードを表す整数 n
とソース文字列 codes
です。
これ以外のインスタンス変数(hoge
、piyo
等)は存在しません。
__
クラスは __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.codes
を exec
で実行しています。
例えば、
- _.__.____._._().__.____._.___().__.___.___.__().__.___.____.___().__.____.__._().___.___._().___.__.____().__.___.___._().__.___.__.__().__.___.____._().__.___.____._().__.___.____.____().___.____._().___._._().__.____.__.____().__.___.____.____().__.____._.___().__.___.____._().__.___.__._().___._.__().___.__.____().___.___.__()
によってコード 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))