はじめに
Pythonは今最も習得希望人口の多い言語である。
それはヘビーなデベロッパーからライトなオフィスコンピューティングまで多岐にわたる。
おそらく「Pythonを使えば○○ができる」と喧伝されているのを見聞きしているからだろう。
この記事ではそんなPythonを一から導入し開発できるようにすることを主にする。
OSのセンテイ
Pythonの開発でもっとも優れたOSはWindowsである。
単純にPython.orgの公式から対象のバージョンをいくつか選んでダウンロードして適当にインストールすればそれですべてを始められるので、MacやLinuxのようにシステムデフォルトと別verとのインストール共存やパッケージ管理ツールを意識する必要はない。
パスは通るし、複数のバージョンを同時に入れてもビルドバージョンですら分けて共存できるので簡単。
処理系
Pythonはインタプリタ界の変態言語なので処理系紹介のQiitaページが現れるくらいに複数の処理系が存在する。
Cで書かれ、実行時にキャッシュ上にコンパイルバイナリを保存する公式の処理系をCPythonと呼ぶほかに、
- ECMAScriptライクにJITなPyPy
- JavaなJython
- C#なIronPython
などがある。Cythonのような、C拡張を吐き出すためのものもある。Anacondaのようなエコシステム自体の統合環境もある。
各種処理系はPythonの拡張構造をシームレスに作れるのがC/C++であるが故なのだが、これらの言語はPytnonにしがみつくくらいなら実装元相当言語であるNodeやJavaやC#で作った方が手っ取り早い上ハイパフォーマンスかつ開発速度もそれほど変わらないことが多いし、そういった処理系を主体とするのはよほどのこだわりがあってのこと。
また、Python自体の環境を縛らないためには、付属品はできるだけそぎ落とした状態で後から追加するのが理想である。
なのでここではCPython一択とする。
どのバージョンか
公式のDownload→Source codeとたどると最新バージョンである3.10.a3(2020年12月現在)が確認できるのでこれのページの一番下にある64bitのmsiファイルをダウンロードしてインストールする。
もし都合で古いバージョンが必要になったらその都度足せばいいので、いろいろ気にしない。
ビルド環境をインストールする。
Pythonのライブラリの中には、C/C++でビルドすることを前提とした形で提供されているものがある。あるいは公式のPreBinaryが手元の最新版Pythonで動かない可能性がある。
その場合手元にC/C++のビルド環境が揃っていなければ当然エラー不可避である。
WindowsユーザーならほぼMicrosoftのVisual C++系一択と言える。MSYSでPacmanは半ばArch Linuxでハードルが上がるので今回は除く。
公式からVisual StudioかそのBuild toolsのどちらかをダウンロードしてインストールする。インストールの際は必ずC++が入るようにオプションを設定してやるようにする。C++と書いてある項目をどれか選んでおけば間違いない。
ふぃぼなっちする。
とりあえずPythonをインストールしたらコマンドプロンプトでpy
(python
ではない点に注意)と打ち込むとPythonの対話型実行環境(REPLと呼ばれるもの。PythonだとIDLEかな?)が立ち上がるのでそれで試すとよい。
まず以下のように入力しエンターする。
>>> def fibo(N):
... if (N < 2): return N
... return fibo(N-1) + fibo(N-2)
...
>>> fibo(50)
よくあるフィボナッチ数を求める関数である。数学が得意なPythonなのだし50くらい一瞬で解けるのではないか。
うん。
一瞬ではないどころか、実行完了までに一仕事終わらせられるくらいまである。
Pythonは数学系が得意だから、という話で入ったなら、まず現実を見るべきである。
このフィボナッチ数は$O(2^n)$の計算量を単純に作成できるので、よく言語の処理能力を図る物差しで使われるのだが、Pythonはfibo(50)を計算するのにすら苦戦する。
C++ほどの速いプログラム言語で同様のコードをコンパイルして50を宛がって実行してもすぐには結果が返ってこないのだから、当然といえば当然だろうか。
C++の本気
これがC++であれば、以下どちらかのコードをコンパイルして実行するとfibo(50)どころかfibo(100)付近までも瞬殺である。いわゆるメモ化等の工夫は必要ない。
(もっとも、fibo(100)あたりになると再帰回数の限界のほうが先に来てしまうのだが)
#include <iostream>
template<int N>
struct fibo {
static const long long value = fibo<N-2>::value + fibo<N-1>::value;
};
template<>
struct fibo<0> {
static const long long value = 0;
};
template<>
struct fibo<1> {
static const long long value = 1;
};
int main() {
std::cout << fibo<50>::value << std::endl;
}
#include <iostream>
constexpr long long fibo(const long long N) {
return N < 2 ? N : fibo(N - 1) + fibo(N - 2);
}
int main() {
std::cout << fibo(50) << std::endl;
}
どちらもコンパイル時に計算を終了してくれる上、再帰中の無駄を自動で最適化してくれるので、コンパイル時間もほぼかからない。
気を付けなければいけないのは、constexpr側のfibo関数については、設定する引数を定数かリテラルで設定する必要があることで、定数ではないlong long値かその互換な変数値を入れた場合通常の実行時計算をしてしまうので結局時間がかかる。
また、オンラインコンパイラでconstexpr版を処理させるととてもコンパイル時間がかかる。それでも通常関数の再起を最適化無しで行うのに比べたら格段に早いのだが。
それでもC++の処理速度はPythonの生で実行するそれを大きく凌駕する。
Pythonも早くなりたい
少なくともPythonはスクリプト言語としての枠内のままでは実行パフォーマンスに限界があるので、ここはC++先生の協力を仰ぐのが良い。(メモ化すればいいのは当然だが、言語の処理能力を計る主旨に反するため割愛)
いわゆる言語拡張という機能を利用してPythonのパワーを高めてやるのである。
CPythonがCで書かれているのも含めて、pythonの他言語拡張はCのほうがシームレスだが、C++先生にはpybind11という強力な武器がある。これを使うことで言語拡張同士の繋ぎ目を作る面倒さを極限まで減少させることができる。活用しない手はない。 1
手順
- cmakeをcmake公式から落としてインストールする。インストーラの設定でパスを通せる。Visual Studioのオプション品を使う場合は設定のため専用のbatファイルを読ませる必要があるので少し手間。
- 関数を実行するC++コード本体、ビルド準備のためのCMakeLists.txt、Python環境のpipに認識させるためのsetup.pyとpyproject.tomlの4つを同じフォルダに並べる。作成するエディタツールは何でもよいがVisual Studio Codeをお勧めする。
- pybind11のプロジェクトを
git clone
等して入手し、フォルダ上に置く。 - それぞれ以下のように記述する。
#include <iostream>
#include <pybind11/pybind11.h>
constexpr long long fibo_con(const long N=50) {
return N < 2 ? N : fibo_con(N-1) + fibo_con(N-2);
}
template<int N>
struct fibo_ten {
static const long long value = fibo_ten<N-2>::value + fibo_ten<N-1>::value;
};
template<>
struct fibo_ten<0> {
static const long long value = 0;
};
template<>
struct fibo_ten<1> {
static const long long value = 1;
};
namespace py = pybind11;
PYBIND11_MODULE(pythoncpp, m) {
m.doc() = "pybind fibo";
py::class_<fibo_ten<50>>(m, "fibo_ten", "").def_readonly_static("value", &fibo_ten<50>::value, "");
m.def("fibo_con", &fibo_con, "", py::arg("N") = 50);
}
cmake_minimum_required(VERSION 3.4...3.18)
project(pythoncpp)
add_subdirectory(pybind11)
pybind11_add_module(pythoncpp fibo.cpp)
#参考コード: https://github.com/pybind/cmake_example/blob/master/setup.py
import os
import sys
import subprocess
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
PLAT_TO_CMAKE = {
"win32": "Win32",
"win-amd64": "x64",
"win-arm32": "ARM",
"win-arm64": "ARM64",
}
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=""):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
class CMakeBuild(build_ext):
def build_extension(self, ext):
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
if not extdir.endswith(os.path.sep):
extdir += os.path.sep
cfg = "Debug" if self.debug else "Release"
cmake_generator = os.environ.get("CMAKE_GENERATOR", "")
cmake_args = [
"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}".format(extdir),
"-DPYTHON_EXECUTABLE={}".format(sys.executable),
"-DEXAMPLE_VERSION_INFO={}".format(self.distribution.get_version()),
"-DCMAKE_BUILD_TYPE={}".format(cfg),
]
build_args = []
if self.compiler.compiler_type != "msvc":
if not cmake_generator:
cmake_args += ["-GNinja"]
else:
single_config = any(x in cmake_generator for x in {"NMake", "Ninja"})
contains_arch = any(x in cmake_generator for x in {"ARM", "ARM64"})
if not single_config and not contains_arch:
cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]
if not single_config:
cmake_args += [
"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(cfg.upper(), extdir)
]
build_args += ["--config", cfg]
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
if hasattr(self, "parallel") and self.parallel:
build_args += ["-j{}".format(self.parallel)]
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
subprocess.check_call(
["cmake", ext.sourcedir] + cmake_args, cwd=self.build_temp
)
subprocess.check_call(
["cmake", "--build", "."] + build_args, cwd=self.build_temp
)
setup(
name="pythoncpp",
version="0.0.1",
author="exli3141",
author_email="exli3141@gmail.com",
description="",
long_description="",
ext_modules=[CMakeExtension("pythoncpp")],
cmdclass={"build_ext": CMakeBuild},
zip_safe=False,
)
[build-system]
requires = [
"setuptools>=42",
"wheel",
"pybind11>=2.6.0",
"ninja; sys_platform != 'win32'",
"cmake>=3.12",
]
build-backend = "setuptools.build_meta"
- 4つのファイルを保存したフォルダを対象に
py -m pip install
する。
> py -m pip install ./pythoncpp
Processing c:\users\exli3\pythoncpp
Installing build dependencies ... done
Getting requirements to build wheel ... done
Preparing wheel metadata ... done
Building wheels for collected packages: pythoncpp
Building wheel for pythoncpp (PEP 517) ... done
Created wheel for pythoncpp: filename=pythoncpp-0.0.1-cp310-cp310-win_amd64.whl size=51389 sha256=a5dfa77623dbd0c68a464e0be2e6a5e2228926e9df0f387c4d9ddeec07ff5c37
Stored in directory: C:\Users\exli3\AppData\Local\Temp\pip-ephem-wheel-cache-n2m3fx5l\wheels\e0\3f\c8\070c22c6b57aef709405561e758661aa4d276e2da56a8f9556
Successfully built pythoncpp
Installing collected packages: pythoncpp
Attempting uninstall: pythoncpp
Found existing installation: pythoncpp 0.0.1
Uninstalling pythoncpp-0.0.1:
Successfully uninstalled pythoncpp-0.0.1
Successfully installed pythoncpp-0.0.1
その結果
これで先ほどと同じようにpy
を打ち込んで動作させたIDLEで試す。
今回は時間を計測して比較できるようにする。
>>> import pythoncpp
>>> import time
>>> def fibo(N):
... if N < 2: return N
... return fibo(N-2) + fibo(N-1)
...
>>> start = time.perf_counter(); pythoncpp.fibo_con(); time.perf_counter() - start
12586269025
55.67761182785034
>>> ...
対象関数 | Nの値 | 所要時間(単位:s) |
---|---|---|
pythoncpp.fibo_con | 40 | 0.4524 |
pythoncpp.fibo_con | 42 | 1.1970 |
pythoncpp.fibo_con | 50 | 55.6776 |
pythoncpp.fibo_ten | 50 | 0.0001316 |
fibo(python内) | 40 | 21.2826 |
fibo(python内) | 42 | 55.9794 |
fibo(python内) | 50 | (たぶん数十分かかる) |
Python定義のfibo関数による50算出は時間がかかりすぎると判断し打ち切った。
constexpr関数のほうは単純にモジュール化のコンパイル過程で計算結果を確定していないため、通常の関数定義で行ったものと同じような時間がかかっている。
テンプレートメタで記述したものは単純に計算結果の入った変数を表示しているだけなので当然の結果である。計算速度の比率は計算対象となるものによって異なるので一概には言えないが、これを見ただけでも**「Pythonはそれ単独で覚えずC++も一緒に覚える」**ことが重要といえる。
ランタイムをPythonで、ロジックをC++で書くように心がければ、これからのPythonプログラミング生活がより一層張り合いのあるものとなるだろう。
Pythonらしい環境と連携させる?
pyファイルを作成してコードを実行するのが本来の在り方であるし、Python環境といえばvirtualenvで仮想環境を切ったり、Jupiter Laboを入れてIDLEを代用させたり、NumpyやPandasなどの有用なライブラリを結びつけるもの。2
とりあえずvirtualenv環境を作って、そのカレントフォルダ上にC++ロジック用のフォルダを設けてそこに上記4つのファイルをベースとしたライブラリ(cpprojectなりとする)をくみ上げて、スタートアップのpyファイルを置くカレントでpy -m pip install ./cpproject
してからpy /path/to/main.py
なりで動作確認できるだろうか。
(コマンドプロンプトなら/を\に変更する)
終わりに
おそらく本記事を最後まで読めば、Pythonを問題なく導入し使用できるようになっていることだろう。
PythonはCMakeに代わるmesonというビルドシステムがPythonで実装されていることもあったり、他のプロジェクトのビルドにちょくちょくかかわることもあって、C++erにとって欠かすことのできない言語であるが、その逆においてもPythonに足りない処理速度を補う力をC++が持っているため欠かせないものである。
Pythonでできることを見たために作った記事
なんとなく言語比較紹介なんかを見ていると、Pythonは「数学とか機械学習とかできますよ、夢膨らむ。WEBでも大活躍してるし」のような印象を与えてくるのだけれど、そもそも「プログラム言語の紹介に"そのプログラム言語ができること"をアピールするのが的外れ」である。
機械学習系はC++などのハイパフォーマンス言語でロジックしてPythonはそれを動作させるトリガー的な感じになっているのが現状の実装である。
WEBのフロントでPythonするのはまだ機が熟しておらずほぼバックエンド限定である。
そのバックエンドでもWordPress、Laravel等の巨人を多く抱えているPHPや、駆動の仕組み的にリバースプロキシ向きで速度の出るNodeに、開発性でRails等によるバックボーンのあるRubyと比較するとPythonは当初、「環境はある」程度でしかなかった。
もっともそれは過去のこと、PythonのWEBサービス比率も上がっており、今はYoutube、Dropbox、Instagram、Pinterestなどの大手サービスのバックエンドで活躍している。もともとWEBのバックエンドはスクリプト言語たちの戦場であり、Pythonのようなスマートさが大きな武器になるのは当然である。
Pythonの長所は「シンプルな記述性とプログラミングへの造詣の浅深をポリシーに問わない器の大きさから、プログラマブルが専門ではない多くのサイエンティストによって形成されたライブラリ群のサポートを受けられる言語であること」で、あえて"Pythonができること"を語るなら、そういった長所に基づいて、サイエンスやマースやプログラマブルな場面での補助用に強力な支援ができること。
しかもそれ自体に既存のWEBスクリプト言語世界をひっくり返すポテンシャルを秘めているといえる。 3
Pythonは他にもう一つ
プログラミングとしてPythonを収めるなら、Python以外にもう一つふたつは別の言語を覚えるべきである。
本導入は私の好む言語に合わせた仕様であるから、Java系やC#をメインとするならJythonやIronPythonは良い選択であるし、PHPやRubyのフォローしきれない部分をPythonで補う選択は大いにあり。RustやGoするならもっと別のアプローチがあるだろう。
終わりの終わり
この記事はまず隗より始めよのノリなので、もっと良い記事が量産されてくれることを望む。
終わりの終わりの終わり
Advent Calender間に合いませんでした……