Decoración de funciones en Python usando functools

La decoración de funciones, usando functools, en Python nos permite eliminar código repetitivo que debemos escribir una y otra vez, y también nos permite modificar funciones existentes con unas pocas lineas de código. Como ejemplo, veamos la tubería que escribí hace tiempo atrás:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
import sys, re
 
def grep(expression):
    while True:
        text = (yield)
        if re.search(expression, text):
            print(text)
 
def cat(file, target):
    next(target)
    with open(file, "r") as fh:
        for line in fh.readlines():
            target.send(line.strip())
 
def main(args):
    cat(args[0], grep(args[1]))
 
if __name__ == "__main__":
    main(sys.argv[1:])

En la linea 11 pueden ver que hay que preparar el otro lado de la tubería antes de enviar datos, lo cual es un fastidio; Usando ‘functools’ podemos crear una anotación a la medida, la cual llamaremos ‘pipe’ (lineas 6-12):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
# Shows how to use a function decorator to create an annotation
# josevnz@kodegeek.com
import sys, re, functools
 
def pipe(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        target = function(*args, **kwargs)
        next(target) # Saves us the trouble of calling next on the pipe
        return target
    return wrapper
 
@pipe
def grep(expression):
    while True:
        text = (yield)
        if re.search(expression, text):
            print(text)
 
def cat(file, target):
    with open(file, "r") as fh:
        for line in fh.readlines():
            target.send(line.strip())
 
def main(args):
    cat(args[0], grep(args[1]))
 
if __name__ == "__main__":
    main(sys.argv[1:])

En la línea 14 agregamos la nueva anotación y note como eliminamos el ‘next(target)’ completamente de la función cat(file, target). No paree gran cosa, pero es una línea menos de código que poner en cada llamada.

¿Pero se pueden hacer otras cosas? Se me ocurren decoraciones como ‘count’ o ‘sorted’ para funciones que trabajan con listas, estoy aprendiendo como usar esta nueva herramienta, ya les diré como me va.

Usando ‘data pipes y coroutines’ en Python

El concepto es similar a usar ‘pipes’ en UNIX. Por ejemplo, en UNIX podemos combinar varias herramientas para filtrar los contenidos en un archivo de texto. ¿Que hace el siguiente comando?

1
2
# Contar cuantos 'root' hay en el archivo de passwords del servidor Linux
cat /etc/password| egrep -i root

En Python 3 podemos hacer algo como lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
# pipeline.py /etc/passwd root
import sys, re
 
def grep(expression):
    while True:
        text = (yield) # Espere por el texto enviados por la otra co-rutina
        if re.search(expression, text):
            print(text)
 
def cat(file, target):
    next(target) # Initialize el otro lado de la tuberia
    with open(file, "r") as fh:
        for line in fh.readlines():
            target.send(line.strip()) # envie la linea a la co-rutina
 
def main(args):
    cat(args[0], grep(args[1])) # La 'tuberia' se hace de afuera hacia adentro
 
if __name__ == "__main__":
    main(sys.argv[1:])

Otro ejemplo, vamos a modificar el programa que lee los datos del archivo de datos de DolarToday para filtrar por fecha de inicio, final, valores máximos y mínimos. Note como el uso de los filtros es opcional y si no se aplican entonces retornan todos los datos (para mantener el ejemplo sencillo, removí el código que convierte los datos al formato binario):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/usr/bin/env python3
# Simple program to save the 'Dolartoday' extra official dollar rates from CSV to a custom format
# Revisited to use data pipes, couroutines
# @author Jose Vicente Nunez, josevnz@kodegeek.com
import os, sys, datetime, re, gzip, struct
from optparse import OptionParser, OptionValueError
from datetime import datetime
 
class DollarToday:
 
    __slot__ = ("__date", "__value")
 
    def __init__(self, ddate, value):
        if isinstance(ddate, datetime):
            self.__date = ddate
        else:
            self.__date = datetime.strptime(ddate, "%m-%d-%Y")
        self.__value = float(value)
        assert self.__value > 0.0, "{} for date {} is invalid!".format(value, self.__date)
 
    @property
    def date(self):
        return self.__date
 
    @date.setter
    def date(self, date):
        assert isinstance(date, datetime), "Invalid date {}".format(date)
        self.__date = date
 
    @property
    def value(self):
        return self.__value
 
    def __str__(self):
        return "DollarToday[date={}, value={}]".format(self.__date, self.__value)
 
    def __hash__(self):
        return str(id(self))
 
class DollarCollection(dict):
 
    def values(self):
        for dateId in sorted(self.keys()):
            yield self[dateId]
 
    def items(self):
        for dateId in self.keys():
            yield (dateId, self[dateId])
 
    def __iter__(self):
        for dateId in sorted(super().keys()):
            yield dateId
 
    def __str__(self):
        return "Records={},\n{}".format(len(self), ",\n".join([str(date) for date in self.values()]))
 
# Pipe: Read from binary file
def readBinary(file, target):
    next(target)
    FILE_MAGIC = b"DLR\x00"
    FILE_VERSION = b"\x00\x01"
    dollarStruct = struct.Struct("<id ")
    try:
       with gzip.open(file, "rb") as fh:
        magic = fh.read(len(FILE_MAGIC))
        if magic != FILE_MAGIC:
          raise "File doesn't look like a KodeGeek.com binary file!"
        version = fh.read(len(FILE_VERSION))
        if version > FILE_VERSION:
          raise "Unsupported file version: {}, expected {}".format(version, FILE_VERSION)
        while True:
          data = fh.read(dollarStruct.size)
          if len(data) == 0:
           break
          numbers = dollarStruct.unpack(data)
          target.send(DollarToday(datetime.fromordinal(numbers[0]), numbers[1]))
    except (Exception) as err:
            raise
 
# Pipe: Filter elements >= minimum
def min(min, target):
    next(target)
    while True:
        dollar = (yield)
        if dollar.value >= min:
            target.send(dollar)
 
# Pipe: Filter elements < = max
def max(max, target):
    next(target)
    while True:
        dollar = (yield)
        if dollar.value <= max:
            target.send(dollar)
 
# Pipe: filter from date
def from_date(fromd, target):
        next(target)
        while True:
            dollar = (yield)
            if dollar.date >= fromd:
                target.send(dollar)
 
# Pipe: filter from date
def to_date(tod, target):
        next(target)
        while True:
            dollar = (yield)
            if dollar.date < = tod:
                target.send(dollar)
 
def adder(collection):
    while True:
        dollar = (yield)
        collection[dollar.date] = dollar
 
def main(options):
 
    dc = DollarCollection()
 
    pipeline = adder(dc) # Destination of the pipe
    # Add pipes (if needed)
    if options.min != 99999999:
        pipeline = min(options.min, pipeline)
 
    if options.max != -1:
        pipeline = max(options.min, pipeline)
 
    if options.fromd is not None:
        pipeline = from_date(datetime.strptime(options.fromd, "%Y-%m-%d"), pipeline)
 
    if options.tod is not None:
        pipeline = to_date(datetime.strptime(options.tod, "%Y-%m-%d"), pipeline)
 
    readBinary(options.report, pipeline) # Start of the pipe that reads the file contents
 
    print("{}".format(dc))
 
if __name__ == "__main__":
 
    usagetext = """
%prog --report binary.file [--from_date YYYYMMDD] [--to YYYYMMDD] [--min amount] [--max amount]
"""
 
    op = OptionParser(usage=usagetext)
    op.add_option(
                  "-p", "--report",
                  action="store",
                  dest="report",
                  help="Read the contents from the binary storage and generate a report.")
    op.add_option(
                  "-f", "--from",
                  action="store",
                  dest="fromd",
                  help="Optional filter. Start date yyyy-mm-dd")
    op.add_option(
                  "-t", "--to",
                  action="store",
                  dest="tod",
                  help="Optional filter. End date yyyy-mm-dd")
    op.add_option(
                  "-m", "--min",
                  action="store",
                  dest="min",
                  default=99999999,
                  type="float",
                  help="Optional filter. Minimal amount")
    op.add_option(
                  "-M", "--max",
                  action="store",
                  dest="max",
                  default=-1,
                  type="float",
                  help="Optional filter. Maximum amount")
 
    (options, values) = op.parse_args()
    main(options)

Una salida de ejemplo:

1
2
3
4
DolarTodayReader.py --report /Users/josevnz/Documents/dolartoday.jose --min 1050 --from 2016-02-23 --to 2016-02-24
Records=2,
DollarToday[date=2016-02-23 00:00:00, value=1060.0],
DollarToday[date=2016-02-24 00:00:00, value=1071.19]

Para cerrar, les recomiendo el siguiente tutorial: http://www.dabeaz.com/coroutines/index.html. La sintaxis es de Python 2.

‘Contexts’ en Python

En Python, un contexto (context) es una clase que implementa los métodos ‘__enter__’ y ‘__exit__’ los cuales son llamados si la clase en llamada con la palabra reservada ‘with’. Por ejemplo, los descriptores de archivo (file handle) en Python se pueden llamar con un contexto, ahorrando llamar ‘finally’ para cerrar archivos, sin importar si hay un error.

Les traigo de vuelta el programa de ‘DolarToday’ el cual escribe datos en un archivo binario, pero ahora utilizando contextos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python3
# Simple program to save the 'Dolartoday' extra official dollar rates from CSV to a custom format
# Revisited to use 'contexts' to avoid using finally on exception handling
# @author Jose Vicente Nunez, josevnz@kodegeek.com
import os, sys, datetime, re, gzip, struct
from optparse import OptionParser, OptionValueError
from datetime import datetime
 
class DollarToday:
 
    def __init__(self, ddate, value):
        if isinstance(ddate, datetime):
            self.__date = ddate
        else:
            self.__date = datetime.strptime(ddate, "%m-%d-%Y")
        self.__value = float(value)
        assert self.__value > 0.0, "{} for date {} is invalid!".format(value, self.__date)
 
    @property
    def date(self):
        return self.__date
 
    @date.setter
    def date(self, date):
        assert isinstance(date, datetime), "Invalid date {}".format(date)
        self.__date = date
 
    @property
    def value(self):
        return self.__value
 
    def __str__(self):
        return "DollarToday[date={}, value={}]".format(self.__date, self.__value)
 
    def __hash__(self):
        return str(id(self))
 
class DollarCollection(dict):
 
    # https://en.wikipedia.org/wiki/List_of_file_signatures
    __FILE_MAGIC = b"DLR\x00"
    __FILE_VERSION = b"\x00\x01"
    __dollarStruct = struct.Struct("<id ")
 
    def values(self):
        for dateId in sorted(self.keys()):
            yield self[dateId]
 
    def items(self):
        for dateId in self.keys():
            yield (dateId, self[dateId])
 
    def __iter__(self):
        for dateId in sorted(super().keys()):
            yield dateId
 
    def save(self, file):
        try:
            with gzip.open(file, "wb") as fh:
                fh.write(self.__FILE_MAGIC)
                fh.write(self.__FILE_VERSION)
                for dollar in self.values():
                    data = bytearray()
                    data.extend(
                           self.__dollarStruct.pack(
                                dollar.date.toordinal(),
                                dollar.value
                           )
                                )
                    fh.write(data)
        except (Exception) as err:
            raise
 
    def readBinary(self, file, verbose=False):
        try:
            with gzip.open(file, "rb") as fh:
                magic = fh.read(len(self.__FILE_MAGIC))
                if magic != self.__FILE_MAGIC:
                    raise "File doesn't look like a KodeGeek.com binary file!"
                version = fh.read(len(self.__FILE_VERSION))
                if version > self.__FILE_VERSION:
                    raise "Unsupported file version: {}, expected {}".format(version, self.__FILE_VERSION)
                self.clear()
                while True:
                    data = fh.read(self.__dollarStruct.size)
                    if len(data) == 0:
                        break
                    numbers = self.__dollarStruct.unpack(data)
                    #if verbose:
                    #    print("{}".format(",".join([str(x) for x in numbers])))
                    dolar = DollarToday(
                                    datetime.fromordinal(numbers[0]),
                                    numbers[1]
                                    )
                    self[dolar.date] = dolar
        except (Exception) as err:
            raise
 
    def readCsv(self, file):
        tempMap = {}
        try:
            with open(file, "r", encoding="UTF-8") as fh:
                for line in fh.readlines():
                    # 6-23-2010       9.92
                    (date, value) = line.strip().split("\t")
                    if re.match("Fecha", date):
                        continue
                    try:
                        dolVsBol = DollarToday(date, value)
                        tempMap[dolVsBol.date] = dolVsBol
                    except (Exception) as err:
                        print(err)
            self.clear()
            self.update(tempMap)
        except (Exception) as err:
            raise
 
    def __str__(self):
        return "Records={},\n{}".format(len(self), ",\n".join([str(date) for date in self.values()]))
 
def main(options):
 
    verbose = options.verbose
    dc = DollarCollection()
    if options.report == None:
        dc.readCsv(options.read)
        dc.save(options.write)
    else:
        dc.readBinary(options.report, verbose)
    if verbose:
        print("{}".format(dc))
 
if __name__ == "__main__":
 
    usagetext = """
%prog --read csv.file --write binary.file
 
Or:
 
%prog --report binary.file
 
"""
 
    op = OptionParser(usage=usagetext)
    op.add_option(
                  "-r", "--read",
                  action="store", 
                  dest="read", 
                  help="Full path to CSV file with date,value pairs per line")
    op.add_option(
                  "-w", "--write", 
                  action="store", 
                  dest="write", 
                  help="Full path to destination file in binary format")
    op.add_option(
                  "-p", "--report", 
                  action="store", 
                  dest="report",
                  help="Read the contents of the binary storage and generate a report. Incompatible with --read and --write")
    op.add_option(
                  "-v", "--verbose", 
                  action="store_true", 
                  default=False, 
                  dest="verbose", 
                  help="Enable verbose mode")
 
    (options, values) = op.parse_args()
 
    main(options)
</id>

Functors en Python

Un ‘functor’ es u objeto el cual puede ser llamado como si fuera una función. En Python simplemente hay que implementar el método ‘__call__’. En este ejemplo, vamos a tomar a los candidatos presidenciales del articulo anterior y vamos a escribir una pequeña clase (llamada SortKey) la cual nos va a permitir ordenar por cualquier attributo o combinación de attributos (en nuestro ejemplo vamos a ordendar por sexo y luego por edad):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python3
# A little fun with the candidates for the US presidency for 2016 election year
# Revisited versions with functors, slots, abstract classes
# @author josevnz@kodegeek.com
#
import abc
 
class SortKey:
 
    def __init__(self, *attributes):
        self.attributes = attributes
    def __call__(self, instance):
        return [getattr(instance, attribute) for attribute in self.attributes]
 
class Candidate(metaclass=abc.ABCMeta):
 
    __slots__ = ("__name", "__sex", "__age")
 
    @abc.abstractmethod    
    def __init__(self, name, sex="F", age=21):
        self.__name = name
        self.__sex = "F" if sex not in ["M", "F"] else sex
        self.__age = 21 if not (21 < = age <= 120) else age
 
    @property
    def name(self):
        return self.__name
 
    def get_party(self):
        raise NotImplemented()
 
    party = abc.abstractproperty(get_party)
 
    @property
    def sex(self):
        return self.__sex
 
    @property
    def age(self):
        return self.__age
 
    def __hash__(self):
        return hash(id(self))
 
    def __str__(self):
        return "{}=[name={}, sex={}, age={}]".format(self.__class__.__name__,
                                                            self.__name,
                                                            self.__sex,
                                                            self.__age
                                                            )
 
    def __repr__(self):
        return "{}({}{}{}{})".format(
                                 self.__class__.__name__, 
                                 self.__name, 
                                 self.__sex, 
                                 self.__age
                                )
 
    @abc.abstractmethod
    def __bool__(self):
        raise NotImplemented()
 
    def __lt__(self, other):
        return self.__age < other.__age
 
    def __gt__(self, other):
        return self.__age > other.__age
 
    def __eq__(self, other):
        return (
                self.__age == other.__age and 
                self.__name == other.__name and
                self.__sex == other.__sex
                )
 
class Republican(Candidate):
 
    def __init__(self, name, sex="F", age=21):
        return super().__init__(name, sex, age)
 
    party = "Replublican Party (GOP)"
 
    def __bool__(self):
        return False
 
class Democrat(Candidate):
 
    def __init__(self, name, sex="F", age=21):
        return super().__init__(name, sex, age)
 
    party = "Democratic Party"
 
    def __bool__(self):
        return True
 
if __name__ == "__main__":
        candidates = []
        candidates.append(Democrat("Hillary Clinton", "F", 68))
        candidates.append(Republican("Donald Trump", "M", 70))
        candidates.append(Democrat("Bernie Sanders", "M", 75))
        candidates.append(Republican("Marco Rubio", "M", 45))
        candidates.sort(key=SortKey("sex", "age"), reverse=False)
        for candidate in candidates:
            isDemocrat = "yes" if candidate else "no"
            print("Candidate: {}, democrat? {}".format(candidate, isDemocrat))

Y la salida (fíjense que definimos el orden en la linea 103, ademas de que ordenamos la lista primero para después utilizarla):

1
2
3
4
Candidate: Democrat=[name=Hillary Clinton, sex=F, age=68], democrat? yes
Candidate: Republican=[name=Marco Rubio, sex=M, age=45], democrat? no
Candidate: Republican=[name=Donald Trump, sex=M, age=70], democrat? no
Candidate: Democrat=[name=Bernie Sanders, sex=M, age=75], democrat? yes

Slots, Abstracts classes en Python 3

¿Se acuerdan del ejemplo anterior usando classes en Python? En este caso hice un par de cambios para aprender más sobre clases abstractas y slots (una manera de ahorrar memoria en Python cuando creamos objetos).

En este caso:

  • Uso un ‘slot’ para guardar sólo 3 atributos que son definidos en el constructor
    Declaro la propiedad ‘party’ como abstracta ya que quiero que el que extienda las clases se vea forzado a implementar un método adecuado que me diga de que partido viene
    También obligo al usuario a declarar el método ‘__bool__’ el cual utilizo para saber si es Democrata u otra clase (si, el ejemplo es forzado :-))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#!/usr/bin/env python3
# A little fun with the candidates for the US presidency for 2016 election year
# Revisited versions with slots, abstract classes
# @author josevnz@kodegeek.com
#
import abc
 
class Candidate(metaclass=abc.ABCMeta):
 
    __slots__ = ("__name", "__sex", "__age")
 
    @abc.abstractmethod    
    def __init__(self, name, sex="F", age=21):
        self.__name = name
        self.__sex = "F" if sex not in ["M", "F"] else sex
        self.__age = 21 if not (21 &lt; = age &lt;= 120) else age
 
    @property
    def name(self):
        return self.__name
 
    def get_party(self):
        raise NotImplemented()
 
    party = abc.abstractproperty(get_party)
 
    @property
    def sex(self):
        return self.__sex
 
    @property
    def age(self):
        return self.__age
 
    def __hash__(self):
        return hash(id(self))
 
    def __str__(self):
        return "{}=[name={}, sex={}, age={}]".format(self.__class__.__name__,
                                                            self.__name,
                                                            self.__sex,
                                                            self.__age
                                                            )
 
    def __repr__(self):
        return "{}({}{}{}{})".format(
                                 self.__class__.__name__, 
                                 self.__name, 
                                 self.__sex, 
                                 self.__age
                                )
 
    @abc.abstractmethod
    def __bool__(self):
        raise NotImplemented()
 
    def __lt__(self, other):
        return self.__age < other.__age 
 
     def __gt__(self, other): 
        return self.__age > other.__age
 
    def __eq__(self, other):
        return (
                self.__age == other.__age and 
                self.__name == other.__name and
                self.__sex == other.__sex
                )
 
class Republican(Candidate):
 
    def __init__(self, name, sex="F", age=21):
        return super().__init__(name, sex, age)
 
    party = "Republican Party (GOP)"
 
    def __bool__(self):
        return False
 
class Democrat(Candidate):
 
    def __init__(self, name, sex="F", age=21):
        return super().__init__(name, sex, age)
 
    party = "Democratic Party"
 
    def __bool__(self):
        return True
 
if __name__ == "__main__":
        candidates = []
        candidates.append(Democrat("Hillary Clinton", "F", 68))
        candidates.append(Republican("Donald Trump", "M", 70))
        candidates.append(Democrat("Bernie Sanders", "M", 75))
        candidates.append(Republican("Marco Rubio", "M", 45))
        for candidate in sorted(candidates, reverse=True):
            isDemocrat = "yes" if candidate else "no"
            print("Candidate: {}, democrat? {}".format(candidate, isDemocrat))

Recetas en Python, Java, Perl y otros lenguajes

Internet ofrece un montón de lugares en los cuales puedes buscar pedazos de código, para aprender como hacer algo rápidamente en su lenguaje de programación favorito. Por ejemplo, como las tiendas de ‘todo a un dolar’, tienes sitios con pedazos de código como http://code.activestate.com/recipes/langs/; en otros casos si quieres una discusión un poco más profunda de como se hizo algo, con opiniones a favor y en contra, entonces tienes http://stackoverflow.com/.

¿Cual es tu favorito? Yo aún recuerdo cuando la única forma de aprender algo en Unix era escribiendo “man $command” o “info $command”, ya que Google no existia…

Guardando datos usando formatos de archivo a la medida, en Python 3

En Venezuela una de las pocas formas de saber la paridad entre el dolar “paralelo” y el Bolivar fuerte es utilizando el portal “DolarToday”. El sitio web (https://dolartoday.com/historico-dolar/) ofrece datos que van desde el 2010 hasta el presente, en los cuales puede ver la paridad entre las dos monedas.

En un arranque de ociosidad, decidí bajarme el archivo de Excel con las tasas de conversión, lo exporte a CSV y de allí escribí un programa en Python 3 que hace lo siguiente:

  • Extendiende la clase ‘dict’ en Python para mantener un diccionario con claves ordenadas, el cual se pueda guardar y recuperar a si mismo desde el disco duro
  • Usar struct.Struct para guardar y leer data binaria (también muestro como leer un archivo comprimido con gzip).

 

El código a continuación:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python3
# Simple program to save the 'Dolartoday' extra official dollar rates from CSV to a custom format
# @author Jose Vicente Nunez, josevnz@kodegeek.com
import os, sys, datetime, re, gzip, struct
from optparse import OptionParser, OptionValueError
from datetime import datetime
 
class DollarToday:
 
    def __init__(self, ddate, value):
        if isinstance(ddate, datetime):
            self.__date = ddate
        else:
            self.__date = datetime.strptime(ddate, "%m-%d-%Y")
        self.__value = float(value)
        assert self.__value > 0.0, "¡{} para la fecha {} es invalida!".format(value, self.__date)
 
    @property
    def date(self):
        return self.__date
 
    @date.setter
    def date(self, date):
        assert isinstance(date, datetime), "Invalid date {}".format(date)
        self.__date = date
 
    @property
    def value(self):
        return self.__value
 
    def __str__(self):
        return "DollarToday[date={}, value={}]".format(self.__date, self.__value)
 
    def __hash__(self):
        return str(id(self))
 
class DollarCollection(dict):
 
    __FILE_MAGIC = b"DLR\x00" # Me invente este numero mágico...
    __FILE_VERSION = b"\x00\x01"
    __dollarStruct = struct.Struct("<id ")
 
    def values(self):
        for dateId in sorted(self.keys()):
            yield self[dateId]
 
    def items(self):
        for dateId in self.keys():
            yield (dateId, self[dateId])
 
    def __iter__(self):
        for dateId in sorted(super().keys()):
            yield dateId
 
    def save(self, file):
        fh = None
 
        try:
           fh = gzip.open(file, "wb")
           fh.write(self.__FILE_MAGIC)
           fh.write(self.__FILE_VERSION)
           for dollar in self.values():
               data = bytearray()
               data.extend(
                           self.__dollarStruct.pack(
                                dollar.date.toordinal(),
                                dollar.value
                           )
                )
               fh.write(data)
        except (Exception) as err:
            raise
        finally:
            if fh is not None:
                fh.close()
 
    def readBinary(self, file, verbose=False):
        fh = None
        try:
            fh = gzip.open(file, "rb")
            magic = fh.read(len(self.__FILE_MAGIC))
            if magic != self.__FILE_MAGIC:
                raise "File doesn't look like a KodeGeek.com binary file!"
            version = fh.read(len(self.__FILE_VERSION))
            if version > self.__FILE_VERSION:
                raise "Unsupported file version: {}, expected {}".format(version, self.__FILE_VERSION)
            self.clear()
            while True:
                data = fh.read(self.__dollarStruct.size)
                if len(data) == 0:
                    break
                numbers = self.__dollarStruct.unpack(data)
                #if verbose:
                #    print("{}".format(",".join([str(x) for x in numbers])))
                dolar = DollarToday(
                                    datetime.fromordinal(numbers[0]),
                                    numbers[1]
                                    )
                self[dolar.date] = dolar
 
        except (Exception) as err:
            raise
        finally:
            if (fh is not None):
                fh.close()
    def readCsv(self, file):
        fh = None
        tempMap = {}
        try:
            fh = open(file, "r", encoding="UTF-8")
            for line in fh.readlines():
                # 6-23-2010       9.92
                (date, value) = line.strip().split("\t")
                if re.match("Fecha", date):
                    continue
                try:
                    dolVsBol = DollarToday(date, value)
                    tempMap[dolVsBol.date] = dolVsBol
                except (Exception) as err:
                    print(err)
            self.clear()
            self.update(tempMap)
        except (Exception) as err:
            raise
        finally:
            if (fh is not None):
                fh.close()
 
    def __str__(self):
        return "Records={},\n{}".format(len(self), ",\n".join([str(date) for date in self.values()]))
 
def main(options):
 
    verbose = options.verbose
    dc = DollarCollection()
    if options.report == None:
        dc.readCsv(options.read)
        dc.save(options.write)
    else:
        dc.readBinary(options.report, verbose)
    if verbose:
        print("{}".format(dc))
 
if __name__ == "__main__":
 
    usagetext = """
%prog --read csv.file --write binary.file
 
Or:
 
%prog --report binary.file
 
"""
 
    op = OptionParser(usage=usagetext)
    op.add_option(
                  "-r", "--read",
                  action="store", 
                  dest="read", 
                  help="Ruta completa del archivo CSV fuente, cada linea tiene una clave=valor")
    op.add_option(
                  "-w", "--write", 
                  action="store", 
                  dest="write", 
                  help="Ruta completa para guardar el archivo en nuevo formato binario")
    op.add_option(
                  "-p", "--report", 
                  action="store", 
                  dest="report",
                  help="Lee el archivo binario en memoria. No es compatible con --read y --write")
    op.add_option(
                  "-v", "--verbose", 
                  action="store_true", 
                  default=False, 
                  dest="verbose", 
                  help="Activar impresión de valores por pantalla")
 
    (options, values) = op.parse_args()
 
    main(options)
</id>

Un ejemplo de como correrlo:

1
2
DolarToday.py --read /Users/josevnz/Documents/dolartoday.csv --write /Users/josevnz/Documents/dolartoday.jose --verbose
DolarToday.py --report /Users/josevnz/Documents/dolartoday.jose --verbose

Y la salida luce como esto:

1
2
3
4
5
6
7
8
9
10
Records=1917,
DollarToday[date=2010-06-23 00:00:00, value=9.92],
DollarToday[date=2010-06-25 00:00:00, value=8.05],
DollarToday[date=2010-06-26 00:00:00, value=8.05],
DollarToday[date=2010-06-27 00:00:00, value=7.91],
DollarToday[date=2010-06-28 00:00:00, value=7.91],
DollarToday[date=2010-06-29 00:00:00, value=7.92],
DollarToday[date=2010-06-30 00:00:00, value=7.97],
DollarToday[date=2010-07-01 00:00:00, value=7.97],
DollarToday[date=2010-07-02 00:00:00, value=7.98],

Si tan sólo el gobierno de Nicolás Maduro fuera tan transparente como mis programas :-)

Guardando y recuperando datos en Python usando Pickle

Muy fácil de usar. Aquí les muestro como grabar un objeto (Account) el cual tiene otros objetos adentro (lista de objetos tipo ‘Transaction’).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env python3
# @author Jose Vicente Nunez - josevnz@kodeek.com
import os, os.path, pickle, tempfile
 
class Transaction:
 
    def __init__(self, amount, date, currency="USD", conv_rate=1.0, description=None):
        self.__amount = amount
        self.__date = date
        self.__currency = currency
        if conv_rate < 0:
            raise ValueError("Invalid amount:{0}".format(conv_rate))
        self.__conv_rate = float(conv_rate)
        self.__description  = description
 
    @property
    def amount(self):
        return self.__amount
 
    @property
    def date(self):
        return self.__date
 
    @property
    def currency(self):
        return self.__currency
 
    @property
    def description(self):
        return self.__description
 
    @property
    def conv_rate(self):
        return self.__conv_rate
 
    @property
    def usd(self):
        return self.__conv_rate * self.__amount
 
class Account:
 
    def __init__(self, number, name, transactions = []):
        self.__number = number
        self.__name = name
        if transactions == None:
            self.__transactions = []
        else:
            self.__transactions = transactions
 
    @property
    def number(self):
        return self.__number
 
    @property
    def name(self, name):
        if name == None:
            return self.__name
        if len(name) < 6:
            raise ValueError("Account name too short!")
        self.__name = name
 
    def __len__(self):
        return len(self.__transactions)
 
    @property
    def name(self):
        return self.__name
 
    @property
    def balance(self):
        balance = 0
        for bal in self.__transactions:
            balance += bal.usd
        return balance
 
    @property
    def all_usd(self):
        return len([t for t in self.__transactions if t.currency == "USD"]) == len(self.__transactions)
 
    def apply(self, transaction):
        if not isinstance(transaction, Transaction):
            raise ValueError("Invalid argument, can only add transactions!")
        self.__transactions.append(transaction)
 
    def __getFilename(self):
        return os.path.join(tempfile.gettempdir(), str(self.number))
 
    def save(self):
        fn = self.__getFilename()
        fh = None
        try:
            fh = open(fn, 'wb')
            pickle.dump([self.__name, self.__number, self.__transactions], fh, pickle.HIGHEST_PROTOCOL)
        except (EnvironmentError, pickle.PicklingError) as Error:
            raise SaveError(str(err))
        finally:
            if fh is not None:
                fh.close()
 
    def load(self):
        fn = self.__getFilename()
        fh = None
        try:
            fh = open(fn, 'rb')
            (self.__name, self.__number, self.__transactions) = pickle.load(fh)
        except (EnvironmentError, pickle.UnpicklingError) as Error:
            raise LoadError(str(err))
        finally:
            if fh is not None:
                fh.close()
 
if __name__ == "__main__":
    transactions = []
    transactions.append(
                        Transaction(
                                    15.0, 
                                    "06-06-1966", 
                                    "USD", 
                                    1.0, 
                                    description="XXXX paid me some money"))
    transactions.append(
                        Transaction(
                                    -2, 
                                    "06-06-1966", 
                                    "USD", 
                                    1.0, 
                                    description="I had to pay YYYYY some cash"))
    transactions.append(
                        Transaction(
                                    10000, 
                                    "06-06-1966", 
                                    "BsF", 
                                    0.0001, 
                                    description="Maduro paid me some money in Venezuelan BS. LOL"))
    account = Account(666666, "Savings account", transactions)
    account.apply(Transaction(-3, "02-28-1016", "USD", 1.0, "Coffee time"))
    print(
          "'{0}' Balance: ${1}, all in USD: {2}".format(
                                                        account.name, 
                                                        account.balance, 
                                                        account.all_usd))
    account.save()
    account.load()
    print(
          "'{0}' Balance: ${1}, all in USD: {2}".format(
                                                        account.name, 
                                                        account.balance, 
                                                        account.all_usd))

Pickle no es seguro (ya que se lleva a cabo ninguna validación en el código leido desde el archivo), sin embargo es increíblemente conveniente para programas pequeños que requieren guardar datos rápidamente con estructuras de datos complejas, como por ejemplo objetos anidados.

¿Qué tienen en común los candidatos presidenciales y el lenguaje Python? Mucho más de lo que usted cree

Al menos Python es más fácil de entender :-). También me dió un excusa para mostrarles un poco de herencia y otros trucos de objetos en el lenguaje:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/env python3
# A little fun with the candidates for the US presidency for 2016 election year
# @author josevnz@kodegeek.com
#
class Candidate:
 
    def __init__(self, name, party, sex="F", age=18):
        self.__name = name
        self.__party = party
        self.__sex = "F" if sex not in ["M", "F"] else sex
        self.__age = 18 if not (18 < = age <= 120) else age
 
    @property
    def name(self):
        return self.__name
 
    @property
    def party(self):
        return self.__party
 
    @property
    def sex(self):
        return self.__sex
 
    @property
    def age(self):
        return self.__age
 
    def __hash__(self):
        return hash(id(self))
 
    def __str__(self):
        return "Candidate=[name={0}, party={1}, sex={2}, age={3}]".format(
                                                                      self.__name,
                                                                      self.__party,
                                                                      self.__sex,
                                                                      self.__age
                                                                      )
 
    def __repr__(self):
        return "{0}{1}{2}{3}{4}".format(
                                        self.__class__.__name__, 
                                        self.__name, 
                                        self.__party, 
                                        self.__sex, 
                                        self.__age
                                        )
 
    def __invert__(self):
        raise NotImplemented()
 
    # Are you a Democrat?    
    def __bool__(self):
        raise NotImplemented()
 
    def __lt__(self, other):
        return self.__age < other.__age
 
    def __gt__(self, other):
        return self.__age > other.__age
 
    def __eq__(self, other):
        return (
                self.__age == other.__age and 
                self.__name == other.__name and 
                self.__party == other.__party and 
                self.__sex == other.__sex
                )
 
class Republican(Candidate):
 
    def __init__(self, name, sex="F", age=21):
        return super().__init__(name, "Republican party", sex, age)
 
    def __bool__(self):
        return False
 
    def __invert__(self):
        raise NotImplementedError()
 
class Democrat(Candidate):
 
    def __init__(self, name, sex="F", age=21):
        return super().__init__(name, "Democratic party", sex, age)
 
    def __bool__(self):
        return True
 
    def __invert__(self):
        raise NotImplementedError()
 
if __name__ == "__main__":
        candidates = []
        candidates.append(Democrat("Hillary Clinton", "F", 68))
        candidates.append(Republican("Donald Trump", "M", 70))
        candidates.append(Democrat("Bernie Sanders", "M", 75))
        candidates.append(Republican("Marco Rubio", "M", 45))
        for candidate in sorted(candidates, reverse=True):
            isDemocrat = "yes" if candidate else "no"
            print("Candidate: {0}, democrat? {1}".format(candidate, isDemocrat))

And the output for this run:

1
2
3
4
Candidate: Candidate=[name=Bernie Sanders, party=Democratic party, sex=M, age=75], democrat? yes
Candidate: Candidate=[name=Donald Trump, party=Republican party, sex=M, age=70], democrat? no
Candidate: Candidate=[name=Hillary Clinton, party=Democratic party, sex=F, age=68], democrat? yes
Candidate: Candidate=[name=Marco Rubio, party=Republican party, sex=M, age=45], democrat? no

Ahora si sólo pudiera escribir algo tan sencillo como esto para saber los resultados de las elecciones del 2016 :-)

Escribiendo ‘ls’ en Python3

El programa a continuación es un ejemplo de las cosas que se pueden hacer con Python 3. Para mí fue una excusa para aprender lo siguiente:

  • Uso de ‘.format’ para mostrar contenido con formato (mucho mejor que interpolación de cadena de caracteres con ‘%’)
  • La librería ‘OptionParser’ (Mejor que Getoptions)
  • Trucos con ‘list comprehensions’ , ordenaciones
  • Referencias a funciones

(Les debo el manejo de recursividad, me dio algo de flojera escribirlo :-))
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python3
from optparse import OptionParser, OptionValueError
from collections import namedtuple
import locale, os, time
from _locale import LC_ALL
locale.setlocale(LC_ALL, "en_US.UTF-8") # locale -a
usage = '''
%prog [options] [path1 [path2] [... pathN]]]
The paths are optional; if not given '.' is used
@author: Jose Vicente Nunez (josevnz@kodegeek.com)
'''
Entry = namedtuple('Entry', 'name size modified')
 
def orderCheck(option, opt_str, value, parser):
    if value in ['n', 'name']:
        parser.values.order = "name"
    elif value in ['m', 'modified']:
        parser.values.order = "modified"
    elif value in ['s', 'size']:
        parser.values.order = "size"
    else:
        raise OptionValueError("Invalid value for --order received: {0}".format(value))
 
def getKeyByName(entry):
    return entry.name
 
def getKeyBySize(entry):
    return int(entry.size)
 
def getKeyByModif(entry):
    return int(entry.modified())
 
def createTuple(path):
    bits = os.stat(path)
    return Entry(path, bits.st_size, bits.st_mtime) 
 
def doLs(path, hidden, getKey):
    if (not os.path.isdir(path)):
        return path
    if hidden: # On Unix, hidden files start with '.'
        return sorted([ createTuple(os.path.join(path, entry)) for entry in os.listdir(path) ], key=getKey, reverse=False)
    return sorted([ createTuple(os.path.join(path, entry)) for entry in os.listdir(path) if entry[0] != "." ], key=getKey, reverse=False)
 
def doLsR(path, hidden, getKey):
    # TODO
    pass
 
def formatEntries(entries, modified, sizes):
    if entries == None:
        return
    dirs = 1
    files = 0
    for entry in entries:
        mod = time.ctime(entry.modified) if modified else ""
        size = entry.size if sizes else ""
        if os.path.isfile(entry.name):
            files += 1
        else:
            dirs += 1
        print("{modif}{theSize:>10,} bytes{name:>35}".format(modif=mod, theSize=size, name=entry.name))
    print("files={0}, directories={1}".format(files, dirs))
 
parser = OptionParser(usage=usage)
parser.add_option("-H", "--hidden", action="store_true", dest="hidden", default=False, help='Show hidden files [default: off]')
parser.add_option("-m", "--modified", action="store_true", dest="modified", default=False, help='Show last modified date/time [default: off]')
parser.add_option("-r", "--recursive", action="store_true", dest="recursive", default=False, help='Recurse into sub-directories [default: off]')
parser.add_option("-s", "--sizes", action="store_true", dest="sizes", default=False, help='Show sizes [default: off]')
parser.add_option("-o", "--order", action="callback", type="string",  callback=orderCheck, default="name", help='''Order by ('name', 'n', 'modified', 'm', 'size', 's') [default: name]''')
(options, args) = parser.parse_args()
hidden = options.hidden
modified = options.modified
recursive = options.recursive
sizes = options.sizes
order = options.order
getKey = getKeyByName
if order == 'size':
    getKey = getKeyBySize
elif order == 'modified':
    getKey = getKeyByModif
paths = args if len(args) > 0  else ["."]
lsCallback = doLs if not recursive else doLsR
 
for path in paths:
    formatEntries(lsCallback(path, hidden, getKey), modified, sizes)