Domknięcia, funkcje anonimowe przestrzenie nazw i dekoratory

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ę.