2021年6月13日日曜日

10: 任意のPythonモジュールを任意の出所からダイナミックにインポートする

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

出所は、OSファイルでなくてもよく、HTTP、FTP、等のリソース、データベース項目、プログラム、等でもよく、モジュールコンテンツがダイナミックでもよい。

話題


About: Pythonプログラミング言語

この記事の目次


開始コンテキスト


  • 読者は、Pythonプログラミング言語の基本的知識を持っている。

ターゲットコンテキスト



  • 読者は、任意の、ダイナミックでもよいモジュールを、任意の出所(オペレーティングシステムファイルでなくてもよく、HTTP、FTP、等リソース、データベース項目、プログラム、等)から、ダイナミックにインポートする方法を知る。

オリエンテーション


ここで紹介されるコードは、mypyアノテーションを含んでいます。もしも、それらに馴染みがなければ、ある以前の記事が充分な説明になるでしょう、または、それらは無視しても問題ありません。


本体


1: もしも、あるPythonモジュールファイルが、想定されたパスのものでなかったら、どうすればよいか?


Hypothesizer 7
通常、Pythonモジュールファイルは、ある想定されたオペレーティングシステムディレクトリ内に、想定されたファイル名で、存在します。

例えば、あるモジュール、'theBiasPlanet.coreUtilities.pipes.StringPipe'は、ファイル、'theBiasPlanet/coreUtilities/pipes/StringPipe.py'内にあり、その'theBiasPlanet'ディレクトリは、'PYTHONPATH'に登録されているあるディレクトリ内にあります。

もしも、そのモジュールが、'StringPipe.txt'などという名前のファイルにあったり、別のディレクトリ構造内にあったら、どうすればよいのか?


2: もしも、あるPythonモジュールが、オペレーティングシステムファイル内にあるのでさえなかったら、どうするか?


Hypothesizer 7
もしも、あるPythonモジュールが、オペレーティングシステムファイル内にあるのでさえなかったら、どうすればよいのか?

それは、どういうことか?

えーと、それは、あるHTTP、FTP、等リソース、あるデータベース項目、プログラム、等内にあるかもしれない。

そのモジュールは、特定のアルゴリズムによって生成された、ダイナミックなものでさえあるかもしれない。


3: 一つのわかりきったオプション


Hypothesizer 7
一つのわかりきったオプションは、当該モジュールコンテンツを、ある想定されたパスのファイル内に保存することだ。

言い換えれば、モジュールコンテンツはそのファイル内にキャッシュされるということだ。

何でいけない?

えーと、一部の人々は、反対する理由を持っているかもしれないし、他の人々は、そうでないかもしれない。

私は、個人的には、そのオプションを否定しないが、それを具体的にどう実装しようかと思い惑う: いつ、いかにして、そのキャッシングは呼び出されるべきか、いかにして、キャッシュされるべきモジュールが選択されるべきか(不要なモジュールを私はキャッシュしたくない)、いつ、いかにして、期限切れになったキャッシュは削除されるべきか、等。 . . . 不可能ではない、しかし、さほど苦痛なくというのでもない、と思われる。


4: ある、よりよいかもしれないオプション


Hypothesizer 7
ある、よりよいかもしれない方法は、'importlib'パッケージを使用することだ。

そのアドバンテージは、必要なモジュールのみが必要な時に取り出され、あとに何の残存物も残されないことで、私が上記に挙げた懸念が払拭される。


4-1: メカニズム


Hypothesizer 7
1つのソースローダーが使われ、それが、1つのモジュール名を受け取り、コンテンツバイト配列をリターンする。

以下が、それが動作する仕組みだ('HttpPythonSourceLoader'が、ソースローダークラス(私が作成した)、'theBiasPlanet_coreUtilities_pipes_StringPipe'は、モジュールオブジェクト(モジュール名は、'theBiasPlanet.coreUtilities.pipes.StringPipe'で、モジュールは、'http://localhost:8080/pythonSource/theBiasPlanet/coreUtilities/pipes/StringPipe.py'に存在する)。

@Python ソースコード
		l_httpPythonSourceLoader: HttpPythonSourceLoader = HttpPythonSourceLoader ("http://localhost:8080/pythonSource/")
		l_pythonModuleName = "theBiasPlanet.coreUtilities.pipes.StringPipe"
		theBiasPlanet_coreUtilities_pipes_StringPipe = ModuleType (l_pythonModuleName)
		l_httpPythonSourceLoader.exec_module (theBiasPlanet_coreUtilities_pipes_StringPipe)
		sys.modules [l_pythonModuleName] = theBiasPlanet_coreUtilities_pipes_StringPipe

そのモジュールを'theBiasPlanet.coreUtilities.pipes.StringPipe'としてアクセスしたいですが、'theBiasPlanet_coreUtilities_pipes_StringPipe'としてではなく?

えーと、上記コード自体は、'theBiasPlanet'パッケージオブジェクト、等を自動的に作成しない、したがって、当該パッケージオブジェクト群をあなたが特に作成しなければならないだろう、もしも、あなたが本当にそれを必要するならば(私は必要としない)。

もしも、'from theBiasPlanet.coreUtilities.pipes.StringPipe import StringPipe'('StringPipe'クラスはそのモジュール内にある)のようにしたければ、以下を行なえばよい、上記コードの後に。

@Python ソースコード
		StringPipe = theBiasPlanet_coreUtilities_pipes_StringPipe.StringPipe


4-2: ソースローダークラスを作成する


Hypothesizer 7
したがって、問題は、そのソースローダークラスをいかに作成するかということだ。

実のところ、以下が、そのソースローダークラスだ。

@Python ソースコード
from typing import Union
from typing import cast
from http.client import HTTPConnection
from http.client import HTTPResponse
from http.client import HTTPSConnection
from importlib.abc import SourceLoader
import urllib.parse
from urllib.parse import ParseResult

class HttpPythonSourceLoader (SourceLoader):
	c_readingBlockSize: int = 1024
	
	def __init__ (a_this: "HttpPythonSourceLoader ", a_urlPrefix: str) -> None:
		a_this.i_urlPrefix: str = a_urlPrefix
		a_this.i_httpConnection: HTTPConnection = None
		
		l_parsedUrl: ParseResult = urllib.parse.urlparse (a_this.i_urlPrefix)
		if l_parsedUrl.scheme == "https":
			a_this.i_httpConnection = HTTPSConnection (l_parsedUrl.hostname, l_parsedUrl.port)
		else:
			a_this.i_httpConnection = HTTPConnection (l_parsedUrl.hostname, l_parsedUrl.port)
	
	def get_filename (a_this: "HttpPythonSourceLoader", a_pythonModuleName: str) -> str:
		return "{0:s}{1:s}.{2:s}".format (a_this.i_urlPrefix, a_pythonModuleName.replace (".", "/"), "py")
	
	def get_data (a_this: "HttpPythonSourceLoader", a_pythonModuleUrl: Union [bytes, str]) -> bytes:
		l_httpResponseBody: bytes = b""
		try:
			l_parsedUrl: ParseResult = urllib.parse.urlparse (cast (str, a_pythonModuleUrl))
			a_this.i_httpConnection.request ("GET", l_parsedUrl.path)
			l_httpResponse: HTTPResponse = a_this.i_httpConnection.getresponse ()
			if l_httpResponse.status == 200:
				l_httpResponseBodyBuffer: bytes = None
				while True:
					l_httpResponseBodyBuffer = l_httpResponse.read (HttpPythonSourceLoader.c_readingBlockSize)
					if len (l_httpResponseBodyBuffer) == 0:
						break
					l_httpResponseBody = l_httpResponseBody + l_httpResponseBodyBuffer
			else:
				raise Exception ("The Python module source could not be accessed.")
		finally:
			a_this.i_httpConnection.close ()
			
		return l_httpResponseBody

それは、'importlib.abc.SourceLoader'抽象クラスの実装であり、2つのメソッド、'get_filename'および'get_data'を実装している。

'get_filename'メソッドは、モジュール名を受け取り、モジュールのURLをリターンするが、そのURLが'get_data'メソッドに渡され、後者メソッドがそのURLのコンテンツをリターンする。

データベース等にアクセスするソースローダーを書くのも、直線的に行なえる。


4-3: ビルトインのファイルソースローダークラスがあるが . . .


Hypothesizer 7
もしも、出所がオペレーティングシステムファイルであれば、ビルトインの'importlib.machinery.SourceFileLoader'クラスを使うことも可能ではある。

しかしながら、そのクラスは、モジュール名とファイルパスをコンストラクタで受け取る。

はあ?モジュール名とファイルパスが、そのクラスインスタンスに対して固定される?モジュール毎にインスタンスを作成しなければならないということか? . . . そういうことのようだ。

それは、親クラスの意図に反した奇妙な設計である: そのやり方では、'get_filename'メソッドが完全に無意味にされてしまっている。

モジュール毎にインスタンス化される必要のないものが欲しいのであれば、それをご自分で作成されるのは容易だろう。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>