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 zStringIOi 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_NUMERale 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.