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