Pojęcie dekoratora w języku Python to coś więcej niż wzorzec projektowy. To elegancki sposób na zwiększenie możliwości danej funkcji czy metody doskonale wykorzystujący unikalne cechy języka Python. Mechanizm działania dekoratorów oraz sposób ich użycia doskonale opisał Kent S Johnson (opracowanie to można znaleźć także w języku polskim).
Prostym i użytecznym przykładem dekoratora może być np. funkcja mierząca czas wykonania funkcji udekorowanej.
import time def timeit(method): """Mierzy czas wykonania funkcji""" def check_time(*args, **kw): ts = time.time() result = method(*args, **kw) te = time.time() log(method.__name__, args, kw, ts, te) return result def log(name, args, kw, time_start, time_end): msg = '%r (%r, %r) %2.10f sec' % (name, args, kw, time_end - time_start) print msg check_time.__name__ = method.__name__ return check_time |
Użycie dekoratora w Pythonie to syntaktyczna perełka.
@timeit def say_hello(param): print param time.sleep(0.5) |
W PHP osiągnięcie czegoś takiego jest po prostu niemożliwe. Od wersji PHP 5.3 język został wzbogacony o nowe cechy takie jak domknięcia, funkcje anonimowe i przestrzenie nazw. Czy owe mechanizmy umożliwiają implementację czegoś co choćby przypominało pythonowy dekorator?
W PHP nie da się zdefiniować funkcji, a potem przypisać jej inną nazwę.
def foo(): print 'OK' faa = foo faa() # w wyniku otrzymamy: OK |
no chyba że użyjemy funkcji anonimowej (ang. anonymous function)
$foo = function() {echo 'OK';}; $faa = $foo; $faa(); // w wyniku otrzymamy: OK |
Dekorowanie funkcji anonimowych przy użyciu domknięć
Możemy zapomnieć (na razie) o dekorowaniu tradycyjnych funkcji ale udekorowanie funkcji anonimowej staje się potencjalnie możliwe.
$foo = function() {echo 'OK';}; $foo = function() use ($foo) { echo 'Jest '; return $foo(); }; $foo(); // w wyniku otrzymamy: Jest OK |
Domknięcia (ang. closures) stoją u podstaw programowania w Pythonie, są też od zawsze obecne i powszechnie używane np. w Javascript-cie.
var Example = function() { this.public = function() { return "This is a public method"; }; var private = function() { return "This is a private method"; }; }; Example.public() // returns "This is a public method" Example.private() // error - doesn't work |
Ich implementacja w PHP to kwestia dyskusyjna podobnie jak ich użyteczność. Z uwagi na określone cechy PHP domknięcia w tym języku wydają się nie być odpowiednikiem tego mechanizmu znanym z innych języków programowania. W przeprowadzanym przeze mnie teście przydały się jednak i osobiście dostrzegam jeszcze kilka innych ich praktycznych zastosowań. Wracając jednak do głównego wątku tego wpisu.
W miarę prosty sposób udało się udekorować funkcję „foo”. Domknięcie można zamknąć w funkcji dzięki czemu nadaje się do wielokrotnego użytku.
function timeit($func) { return function() use ($func) { // pobranie wszystkich argumentów funkcji $args = func_get_args(); // utworzenie funkcji pomocniczej $microtime_float = function() { list($usec, $sec) = explode(" ", microtime()); return ((float)$usec + (float)$sec); }; // rozpoczęcie pomiaru czasu $time_start = $microtime_float(); // wywolanie funkcji $result = call_user_func_array($func, $args); // zakończenie pomiaru czasu i wyświetlenie wyników $time_end = $microtime_float(); $time = $time_end - $time_start; printf ("\nfunction was executed during %01.6f seconds\n", $time); // zwrócenie resultatu działania funkcji return $result; }; } |
Zaprezentuje może użycie „dekoratora” timeit na funkcji wymagającej parametrów.
$func_say_hello = function(param) { return 'Hello '. $param; }; $func_say_hello = timeit($func_say_hello); echo $func_say_hello('World'); |
Funkcje anonimowe są, a raczej będą stosunkowo rzadko używane, podobnie zresztą jak funkcje lambda w Pythonie, których są odpowiednikiem. Pomiar czasu wykonania np. funkcji sortującej to z pewnością przydatna sprawa, ale wzbogacanie funkcji anonimowych o nowe możliwości przy pomocy „dekoratorów” wydaje się być mało przydatne. Czy zatem można udekorować zwykłą funkcję?
Czy przestrzenie nazw pomagają w dekorowaniu funkcji?
Po co nam przestrzenie nazw (ang. namespaces)? Załóżmy, że mamy w pliku functions.php zdefiniowaną przestrzeń nazw „lib” i funkcję „say_hello”
<?php namespace lib { function say_hello($param) { return 'Hello '. $param; } } |
Gdybyśmy w pliku index.php dołączyli plik functions.php moglibyśmy używać zdefiniowanych w nim funkcji.
<?php include './functions.php'; echo \lib\say_hello('World'); |
Wywołanie say_hello(‚World’); bez podania poprzedzającego go namespace-a zakończyłoby się błędem. Niestety PHP nie wspiera jak na razie możliwości aliasowania funkcji czy stałych zamkniętych w przestrzeni nazw dlatego też chęć odwoływania się do funkcji „say_hello” bezpośrednio musi zostać poprzedzona definicją nazwijmy to „atrapy”, która przy okazji pozwoli udekorować opakowywowaną funkcję.
include './functions.php'; function timeit($func) {...} function say_hello($param) { $func_say_hello = '\lib\say_hello'; $func_say_hello = timeit($func_say_hello); $result = $func_say_hello($param); } echo say_hello('World'); |
Zwróćmy uwagę na to, że funkcje „say_hello” i „\lib\say_hello” to nie to samo i równie dobrze moglibyśmy nie używać przestrzeni nazw i osiągnęlibyśmy to samo (tylko nazwy funkcji byłyby inne)
function timeit($func) {...} function say_hello($param) {...} function decorated_say_hello($param) { $func_say_hello = 'say_hello'; $func_say_hello = timeit($func_say_hello); $result = $func_say_hello($param); } |
Z punktu widzenia wygody użycia to czy użyjemy przestrzeni nazw czy nie to mamy to samo. I tak chcąc zmierzyć czas wykonania funkcji „say_hello” musimy zmienić jej wywołanie w kodzie na „decorated_say_hello”. Nie da się udekorować zdefiniowanych funkcji w pythoniczny sposób.
Aliasy – nowe możliwości dekorowania klas
W przypadku funkcji moje rozważania to jedynie gimnastyka mózgu, jednak w przypadku klas teoretyczne dywagacje – jak się zaraz okaże – przybiorą praktyczną formę.
Wspominałem wyżej o aliasach. Tego czego nie da się zrobić z funkcjami i stałymi w PHP > 5.3 da się uczynić z klasami.
Załóżmy, że plik functions.php wygląda tak
namespace lib { class Say { public static function hello($param) { return 'Hello '. $param; } public function goodbye($param) { return 'Goodbye '.$param; } } } |
Do pliku index.php możemy dołączyć functions.php i przypisać klasie „/lib/Say” alias Say
include './functions.php'; function timeit($func) {...} use \lib\Say as Say; echo Say::hello('Adam'); $say = new Say(); echo $say->goodbye('Ewa'); |
Nadanie aliasu sprawia, że możemy używać klasy tak jakby była zdefiniowana bez użycia przestrzeni nazw. Jeśli w tym momencie przyjdzie nam ochota zmierzenia czasu wykonania metod klasy „Say” (a właściwie „\lib\Say”) bez zmiany wywołań musimy zrobić dwie rzeczy. Zakomentować linijkę nadającą alias oraz udekorować klasę.
include './functions.php'; function timeit($func) {...} //use \lib\Say as Say; class Say { private $_obj; public function __construct() { $this->_obj = new \lib\Say(); } public static function hello($param) { $func = '\lib\Say::hello'; $func = \deco\timeit($func); return $func($param); } public function goodbye($param) { $func = function($obj, $param) { return $obj->goodbye($param); }; $func = \deco\timeit($func); return $func($this->_obj, $param); } } echo Say::hello('Adam'); $say = new Say(); echo $say->goodbye('Ewa'); |
Wywołanie metod się nie zmieniło, ale wzbogaciliśmy naszą klasę o funkcjonalność pozwalającą nam zmierzyć czas wykonania zarówno metody statycznej klasy, jak i metody instancji. Do wygody użycia dekoratora „@timeit” w Pythonie jest jeszcze bardzo daleko. Udekorowaliśmy zaledwie dwie metody, a trzeba było napisać tyle kodu. Z każdą kolejną metodą przybędzie dodatkowego kodu w dekoratorze. Czy da się ten przyrost ograniczyć?. Dzięki metodom magicznym da się zrobić bardziej uniwersalny dekorator.
include './functions.php'; function timeit($func) {...} //use \lib\Say as Say; class Say { private $_obj; public function __construct() { $this->_obj = new \lib\Say(); } public static function __callStatic($name, $arguments) { $func = '\lib\Say::'.$name; return self::_addDecorators($func, $arguments); } public function __call($name, $arguments) { $func = function() { $params = func_get_args(); $obj = array_shift($params); $method = array_shift($params); return call_user_func_array(array($obj, $method), $params); }; array_unshift($arguments, $name); array_unshift($arguments, $this->_obj); return self::_addDecorators($func, $arguments); } private static function _addDecorators($func, $arguments) { $func = \deco\timeit($func); return call_user_func_array($func, $arguments); } } echo Say::hello('Adam'); $say = new Say(); echo $say->goodbye('Ewa'); |
Konkluzje
1. To naprawdę działa – doznałem intelektualnego orgazmu.
2. Nie stosujcie takich rozwiązań w prawdziwym projekcie
Próba debugowania takiego kodu skazana jest z góry na porażkę. Przeprowadzony przeze mnie eksperyment dostarczył mi najlepszego dowodu na to, że nie ma sensu implementować na siłę mechanizmów znanych z innych języków. PHP ma swoją naturę, która czasami jest ograniczeniem, ale często stanowi też jego zaletę.