PythonといえばCのように波括弧を使わず、インデントによってスコープを表現するところが特徴です。
int main() {
printf("Hello, world!\n");
}
def main():
print("Hello, world!")
しかし、関数定義 main() や関数呼び出し print() では依然として括弧を使用しています。これらの括弧も不要ではないでしょうか?
ルールの説明
括弧類 (( ) [ ] { }) をソースコード内に一切記述することなく汎用的なPythonプログラミングを実現する方法を「カッコつけないPythonプログラミング」と呼ぶことにします。
この記事に記載のコードを検証した環境はCPython 3.14.0b4、macOS 15.6.1 (Apple Silicon) です。
ファイルエンコーディング
もしあなたの脳内にbase64エンコーダ・デコーダがインストール済みであれば、今すぐ括弧なしPythonプログラミングを始めることができます。
# coding: utf_7
print+ACg-'Hello, world!'+ACk-
Pythonはファイルの先頭2行にcoding指定コメントを記述することでutf-8やshift_jisなどのファイルエンコーディングを選択することができますが、その選択肢の一つがutf-7です。utf-7は修正base64エンコーディングによる文字のエスケープを採用したエンコーディングで、 + と - に囲まれた部分がエンコードされた文字列として解釈されます。
しかし一般の人類は脳みその中にbase64エンコーダ・デコーダを持っていませんしすべての文字のUnicodeコードポイントも記憶していないので、より簡単な方法を見てきましょう1。
exec("ソースコード")
多くの方がまず思いつくのは、Pythonコードを文字列リテラルの中に記述し、それを exec() で実行することだと思います。文字列リテラルの中では任意の文字を "\x28" や "\u005b" のように文字コードでエスケープできるので簡単に括弧を使わずプログラムを記述できます。
しかしexecを実行するための丸括弧は省略できません2。なんとか括弧を使わずに関数を実行することはできないでしょうか?
可能です。デコレータを使います。
get_value = lambda cls: cls.value
@exec
@get_value
class Code:
value = """print\x28"Hello, world!"\x29"""
デコレータは対象であるクラスや関数を第一引数に取り、改変した結果を同じ名前に再束縛します。また、デコレータは複数適用することが可能なので、事前に定義したヘルパー関数 get_value を使うことで任意のコードをexecの第一引数に渡すことができました。
しかし全てのコードを文字列リテラルの中に書くとなると、コード補完もシンタックスハイライトも効かず、何よりエレガントではありません。そこで、このデコレータを使用した関数呼び出しを更に深堀りしてみることとします。
カッコつけないPython
まず、デコレータによる関数呼び出しの最大の弱点は、引数の数が1つに限定されてしまう点です。0個でもなく2つ以上でもなく、ちょうど1つの引数でしか呼び出しができません。これを解決する良い方法はないでしょうか?
0個の場合については、単項演算子のオーバーロードによって実現できます。
import operator
# operator.call によりクラスをインスタンス化
@operator.call
class call_hoge:
__neg__ = hoge.__call__
# hoge() と等価
-call_hoge
そして任意の数の引数を使用できるようにする最後のピースが functools モジュールにある partial です。 partialオブジェクトには3つの属性 func , args , keywords があり、partialオブジェクトを0引数で実行した場合、 args と keywords を引数として func を呼び出したのと等価の結果が得られます。
# 注意: このコードは動作しません(後で解説します)
from functools import partial
p = partial(print)
p.args = ("Hello", ", world")
p.keywords = {"end": "!\n"}
# print("Hello", ", world", end="!\n") と等価
p()
上記のコードは動作しません。なぜかというとpartialオブジェクトの属性は読み取り専用であり、代入しようとするとエラーになるからです。これはpartialオブジェクトの本体がCで実装された _functools モジュールにあるためで、partialを継承したクラスを作ってオーバーライドする方法では回避できません。
しかしこれを回避する方法があります。 functools モジュールのソースコードを見てみると、Pythonでpartialが実装されたあとに _functools からpartialをimportすることで上書きしています。つまり _functools モジュールが消え去れば純粋Python実装のpartialを使うことができるようになります。 sys.modules["_functools"] = None により _functools モジュールを消去し、 importlib.reload(functools) によってモジュールをリロードすることで実現します。
import functools
import operator
import importlib
import sys
get_value = lambda cls: cls.value
@operator.call
class sys_modules:
__setattr__ = sys.modules.__setitem__
# sys.modules["_functools"] = None と等価
sys_modules._functools = None
@importlib.reload
@get_value
class functools:
value = functools
functools.partial.__neg__ = functools.partial.__call__
これにより括弧を使わず任意の関数を呼び出すことができるようになりました。
@operator.call
class create_function:
__add__ = functools.partial
f = create_function + print
f.args = "Hello", "world" # tupleの括弧は省略できます
@get_value
class keyword_1:
value = "end", "!\n"
@dict
@get_value
class keywords:
value = keyword_1, # ((key1, val1), (key2, val2), ...)からdictが構成できます
f.keywords = keywords
# print("Hello", ", world", end="!\n") と等価
-f
関数の定義
任意の関数の実行ができるようになったところで、次は関数の定義を行う方法について考えてみます。関数は def キーワードを使う関数及びメソッドと lambda の2つに大きく別れますが、lambdaは式を1つしか記述できず文を含めることもできないという非常に強い制約を受けています。そのためtry-exceptやyieldなどを含めることもできません。
defによる関数定義は文法レベルで ( , ) を使用することが定められており、Pythonのパーサーに文字列が与えられるよりも前に文字を改変するという方法を取るか、defを使わずに関数を定義することになります。前者の文字を改変する方法としては前述したエンコーディング指定のほか、Pythonのimportシステムをカスタマイズする方法もありますが似たような話なので割愛します。
defを使わずに関数を定義する方法の1つはバイトコードから types.FunctionType オブジェクトをインスタンス化する方法です。Pythonバイトコードを書くことができれば色々なフラグの設定などはそう難しいものでは無いはずです。私は挫折しました。
もう1つの方法はASTで関数定義を記述して compile() して exec() する方法です。文字列をexecする方法と違ってちゃんとシンタックスハイライトしてくれます。実用性は皆無です。
まとめ
カッコつけずにPythonを書くのはとても大変なようです。みなさんはぜひカッコつけながらPythonプログラミングを楽しんでください。
参考
-
http://herumi.in.coocan.jp/prog/futsu.pdf
- ネタ被り
- C言語によるカッコつけないプログラミングの実践例です
-
https://web.archive.org/web/20151121185814/http://e-arrows.sakura.ne.jp/2010/08/is-lisp-really-has-too-many-parenthesis.html
- 上記記事の元ネタ