Normalną metodą ładowania kodu pythonowego jest polecenie
import
i umieszczanie plików w standardowym drzewie modułów
zarządzanym zmienną PYTHONPATH
. To załatwia 99.9% przypadków.
W tym artykule o pozostałym 0.1% - kilka dziwnych ale czasem użytecznych technik ładowania kodu, od wczytywania pojedynczego pliku z nietypowego katalogu, po pisanie własnych ładowaczy modułów mogących generować kod, ściągać go ze zdalnej maszyny czy pobierać z bazy danych.
Moduł imp
Moduł imp
pozwala (między innymi) załadować moduł z nietypowej
lokalizacji. Prosty przykład:
import imp file_path = "/sciezka/do/pliku.py" mymodule = imp.load_module( "moja_nazwa", file(file_path), file_path, (".py", "r", imp.PY_SOURCE)) print mymodule.jakas_zmienna my_module.jakas_funkcja() import moja_nazwa print moja_nazwa.multistr
Przydaje się to głównie do ładowania pojedynczego nietypowo położonego
pliku (np. pliku konfiguracyjnego zapisanego w syntaksie pythona
leżącego gdzieś w /etc/
). Warto zwrócić uwagę, że import
moja_nazwa
niczego już nie ładuje, moduł jest.
Parametry funkcji load_module
to:
- Nazwa tworzonego modułu (tj. nazwa pod jaką moduł pojawi się w słowniku modułów). Można tu podać nawet dziwne teksty (np. zawierające spacje).
- Otwarty plik z którego ma być przeczytany kod (musi to być obiekt
file
, funkcja odmawia działania zStringIO
i innymi podobnymi obiektami). - Nazwa pliku - używana przy raportowaniu błędów (w trackbackach itp), nie musi to być prawdziwa ścieżka choć ... jest to sposób na utrudnienie sobie życia.
- Magiczna tupla informująca co jest ładowane (powyższy zapis oznacza kod źródłowy pythona, są też inne, np. zapis oznaczający skompilowany plik)
Funkcja exec
Funkcja exec
pozwala na wykonanie kodu w wstępnie przygotowanym środowisku
(np. już z zaimportowanymi potrzebnymi modułami czy predefiniowanymi zmiennymi), daje też kontrolę nad miejscem zapisu powstałych zmiennych, funkcji i klas.
Przykład:
def dodawacz(a, b): return a + b def odejmowacz(a, b): return a - b text = """ global a a = 1 b = 2 c = dodaj(a,b) d = odejm(a,b) def dziel(x, y): return x / y class Tabliczka(object): def __call__(self, x, y): return x * y """ globalne = dict(dodaj = dodawacz) lokalne = dict(odejm = odejmowacz) exec(text, globalne, lokalne) print lokalne['b'] print globalne['a'] print lokalne['dziel'](63,9) tabl = lokalne['Tabliczka']() print tabl(6, 4)
W pełni kontrolujemy tutaj miejsce odczytu/zapisu zmiennych. Słownik globalne
jest dla parsowanego
kodu przestrzenią zmiennych globalnych, słownik lokalne
przestrzenią zmiennych lokalnych. Cokolwiek
tam włożyliśmy (jak funkcje dodaj
i odejm
) jest widoczne dla wykonywanego kodu, cokolwiek kod
zdefiniował, znajdziemy w tych słownikach.
Podając jako słowniki globals()
i locals()
możemy wykonać kod w normalnym kontekście bieżącego programu:
# -*- coding: utf-8 -*- def dodawacz(a, b): return a + b def odejmowacz(a, b): return a - b code = compile(""" a = 1 b = 2 """, "nazwa do tracebacka", "exec") exec(code, globals(), locals()) print "a = %d, b = %d" % (a, b) a = 3 b = 4 print "a = %d, b = %d" % (a, b) exec(code, globals(), locals()) print "a = %d, b = %d" % (a, b)
Powyżej przy okazji dałem przykład funkcji compile
. Tworzy ona binarną reprezentację sparsowanego
kodu, co jest wydajniejsze, jeśli ten sam fragment chcemy wykonywać wielokrotnie. Funkcja ta niczego
nie wykonuje, jedynie parsuje. Parametry compile
to parsowany kod, tekst jakim będzie on opisywany
w informacjach o błędach (trackbackach), wreszcie stała informująca co jest kompilowane
(exec
to wieloliniowy kod, single
to pojedyncza instrukcja a eval
to wyrażenie).
Powyższe nie wyczerpuje sztuczek na które pozwala exec
. Można np. zadać jako słownik zmiennych
lokalnych przestrzeń danych jakiegoś modułu (jakis.modul.__dict__
) by zmodyfikować jego treść po
załadowaniu.
Moduł imputil
Moduł imputil
to maszyneria pozwalająca na pisanie własnych
loaderów kodu. Proste przykładowe zastosowanie to ładowanie kodu z
bazy danych czy innego storage, ściąganie go po sieci albo generowanie
z takich czy innych szablonów.
Użycie wygląda tak:
# -*- coding: utf-8 -*- import imputil, sys class MyImporter(imputil.Importer): numer = 0 def get_code(self, parent, modname, fqname): print "Próba ładowania %s" % fqname if fqname == "moje": # pakiet (namespace) return (1, "", dict()) if not fqname.startswith("moje."): return None code = """ who_am_i = 'Tu moduł %s' def kim_jestem(): return who_am_i + " (numer " + str(MOJ_NUMER) + ")" """ % fqname # moduł self.numer += 1 return (0, code, dict(MOJ_NUMER = self.numer)) imputil.ImportManager().install() sys.path.insert(0, imputil.BuiltinImporter()) sys.path.insert(0, MyImporter()) import re import moje.krokodyl print moje.krokodyl.who_am_i print moje.krokodyl.kim_jestem() from moje.zolw import kim_jestem print kim_jestem()
Co się tu dzieje?
Zainstalowanie obiektu ImportManager
sprawia, że zastępuje on standardowe mechanizmy
ładowania modułów. Następnie dodaję do sys.path
obiekty importerów (przy czym BuiltinImporter
reaktywuje normalne techniki importu, gdybym go nie dodał, import re
nie udałoby się). Kolejność
jest istotna (powyżej mój importer jest przed BuiltinImporter
-em, dlatego trafiają do niego wszystkie
żądania).
Mój importer to tak naprawdę funkcja get_code
, która ma zwrócić kod modułu o zadanej nazwie.
Kluczowy parametr to fqname
czyli pełna nazwa (moj.modul
).
Funkcja get_code
może zwrócić None - gdy nie znajdzie zadanego modułu (nastąpi wtedy przeszukiwanie
dalszych importerów). A w przeciwnym wypadku powinna zwrócić trójkę:
- flaga czy to jest namespace (1 gdy tak, tj. gdy moduł może mieć podmoduły, 0 gdy nie)
- kod modułu (stała tekstowa albo obiekt
code
, czyli wynik funkcjicompile
) - słownik zmiennych, które zostaną umieszczone w przestrzeni nazw modułu jeszcze zanim zacznie
on być uruchamiany (powyżej definiuję tak stałą
MOJ_NUMER
ale technika nadaje się ogólnie do wkładania predefiniowanych zmiennych, automatycznego importowania itd)
W powyższym przykładzie zwracam pusty kod dla modułu będzącego namespace. Oczywiście tak nie trzeba, można tu zwrócić normalny kod, stanowi on odpowiednik zawartości pliku
__init__.py
.
Typ ModuleType
Na koniec przykład jak można stworzyć obiekt modułu bez parsowania żadnego kodu:
import types mymodule = types.ModuleType("Dziwna nazwa nie zaszkodzi") mymodule.x = 3 mymodule.y = "Ala" def multistr(str, count): return str * count mymodule.n_razy = multistr ############################################ print mymodule.n_razy(mymodule.y, mymodule.x)
Posłowie
Z wszystkich tych sztuczek trzeba korzystać ostrożnie (łatwo mogą doprowadzić do niezrozumiałego zachowania programu) ale w szczególnych przypadkach bywają przydatne. I ilustrują elastyczność runtimeowego środowiska Pythona.