|
1
|
|
|
import abc |
|
2
|
|
|
import inspect |
|
3
|
|
|
import types |
|
4
|
|
|
from functools import wraps |
|
5
|
|
|
|
|
6
|
|
|
__all__ = ['Transformer'] |
|
7
|
|
|
|
|
8
|
|
|
class TransformerInterface(abc.ABC): |
|
9
|
|
|
"""The interface defining a method to transform structured data. Anyone, implementing this has the ability to receive |
|
10
|
|
|
some kind of data and return some kind of transformed version of them. |
|
11
|
|
|
""" |
|
12
|
|
|
@abc.abstractmethod |
|
13
|
|
|
def transform(self, data, **kwargs): |
|
14
|
|
|
"""Takes data and optional keyword arguments and transforms them. |
|
15
|
|
|
Input data can represent either a single variable of an observation (scalar) |
|
16
|
|
|
or a vector of observations of the same variable (if N observations then returns a [N x 1] array-like). |
|
17
|
|
|
|
|
18
|
|
|
Example 1: |
|
19
|
|
|
obs1 = [x1, y1, z1] |
|
20
|
|
|
fa = f_a(x) |
|
21
|
|
|
fb = f_b(x) |
|
22
|
|
|
fc = f_c(x) |
|
23
|
|
|
feature_vector1 = [fa(x1), fb(y1), fc(z1)] |
|
24
|
|
|
|
|
25
|
|
|
So, each of fa, fb and fc can implement the Transformer interface. |
|
26
|
|
|
|
|
27
|
|
|
Example 2: |
|
28
|
|
|
obs1 = [x1, y1, z1] |
|
29
|
|
|
obs2 = [x2, y2, z2] |
|
30
|
|
|
obs3 = [x3, y3, z3] |
|
31
|
|
|
obs4 = [x4, y4, z4] |
|
32
|
|
|
data = [obs1; |
|
33
|
|
|
obs2; |
|
34
|
|
|
obs3; |
|
35
|
|
|
obs4] shape = (4,3) |
|
36
|
|
|
fa = f_a(x) |
|
37
|
|
|
fb = f_b(x) |
|
38
|
|
|
fc = f_c(x) |
|
39
|
|
|
feature_vectors = [fa(data[:0], fb(data[:1], fc(data[:2])] - shape = (4,3) |
|
40
|
|
|
|
|
41
|
|
|
Again each of fa, fb and fc can implement the Transformer interface. |
|
42
|
|
|
|
|
43
|
|
|
Args: |
|
44
|
|
|
data (object): the input data to transform; the x in an f(x) invocation |
|
45
|
|
|
|
|
46
|
|
|
Raises: |
|
47
|
|
|
NotImplementedError: [description] |
|
48
|
|
|
""" |
|
49
|
|
|
raise NotImplementedError |
|
50
|
|
|
|
|
51
|
|
|
|
|
52
|
|
|
class RuntimeTransformer(TransformerInterface, abc.ABC): |
|
53
|
|
|
"""Examines whether the input object is callable, if it can receive at least one input argument and also |
|
54
|
|
|
whether it can accept kwargs. Depending on the kwargs check, "configures" the '_transform' method to process |
|
55
|
|
|
any kwargs at runtime or to ignore them. |
|
56
|
|
|
|
|
57
|
|
|
Delegates all the transformation operation to its '_transform' method provided by its '_callable' field. |
|
58
|
|
|
|
|
59
|
|
|
Args: |
|
60
|
|
|
a_callable (callable): a callable object used to delegate the transformation operation |
|
61
|
|
|
""" |
|
62
|
|
|
def __new__(cls, *args, **kwargs): |
|
63
|
|
|
x = super().__new__(cls) |
|
64
|
|
|
a_callable = args[0] |
|
65
|
|
|
if not callable(a_callable): |
|
66
|
|
|
raise ValueError(f"Expected a callable as argument; instead got '{type(a_callable)}'") |
|
67
|
|
|
nb_mandatory_arguments = a_callable.__code__.co_argcount # this counts sums both *args and **kwargs |
|
68
|
|
|
# use syntax like 'def a(b, *, c=1, d=2): .. to separate pos args from kwargs and to inform 'inspect' lib about it |
|
69
|
|
|
if nb_mandatory_arguments < 1: |
|
70
|
|
|
raise ValueError(f"Expected a callable that receives at least one positional argument; instead got a callable that " |
|
71
|
|
|
f"receives '{nb_mandatory_arguments}'") |
|
72
|
|
|
signature = inspect.signature(a_callable) |
|
73
|
|
|
parameters = [param for param in signature.parameters.values()] |
|
74
|
|
|
|
|
75
|
|
|
if 1 < nb_mandatory_arguments: |
|
76
|
|
|
def _transform(self, data, **keyword_args): |
|
77
|
|
|
return a_callable(data, **keyword_args) |
|
78
|
|
|
x._transform = types.MethodType(_transform, x) |
|
79
|
|
|
elif nb_mandatory_arguments == len(parameters): |
|
80
|
|
|
def _transform(self, data, **keyword_args): |
|
81
|
|
|
return a_callable(data) |
|
82
|
|
|
x._transform = types.MethodType(_transform, x) |
|
83
|
|
|
else: |
|
84
|
|
|
raise Exception(f"Something went really bad. Check code above. Parameters: [{', '.join(str(_) for _ in parameters)}]") |
|
85
|
|
|
x._callable = a_callable |
|
86
|
|
|
return x |
|
87
|
|
|
|
|
88
|
|
|
def transform(self, data, **kwargs): |
|
89
|
|
|
return self._transform(data, **kwargs) |
|
90
|
|
|
|
|
91
|
|
|
class Transformer(RuntimeTransformer): pass |