summaryrefslogtreecommitdiff
path: root/es/05_oop.md
blob: 81fa3af52bd4c2b55de8a287f46f4e677bbe02c4 (plain)
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
# Orientación a Objetos

La *programación orientada a objetos* u *object oriented programming* (OOP) es
un paradigma de programación que envuelve python de pies a cabeza. A pesar de
que python se define como un lenguaje de programación multiparadigma, la
programación orientada a objetos es el paradigma principal de éste. Aunque
varias de las características que tratamos en el apartado anterior se
corresponden más con un lenguaje de programación funcional, en python todo
(o casi todo) es una clase.

Python usa una programación orientada a objetos basada en clases, a diferencia
de otros lenguajes como JavaScript, donde la orientación a objetos está basada
en prototipos. No es el objetivo de este documento el de contarte cuáles son
las diferencias entre ambas, pero es interesante que sepas de su existencia, ya
que es una de las pocas diferencias que existen entre estos dos lenguajes de
amplio uso en la actualidad.

## Programación basada en clases

Tras haber hecho una afirmación tan categórica como que en python todo son
clases, es nuestra obligación entrar a definir lo que son y qué implica la
programación basada en clases.

Los objetos, *objects*, son entidades que encapsulan un estado, un
comportamiento y una identidad capaz de separarlos de otras entidades. Una
clase, *class*, es la descripción de estos objetos.

Saliendo de la definición filosófica y trayéndola a un nivel de andar por casa,
puedes aclararte sabiendo que las clases son la definición enciclopédica de
algo, mientras que los objetos son el propio objeto, persona o animal descrito.

Llevándolo al ejemplo de un perro, la clase es la definición de qué es un perro
y los objetos son los distintos perros que te puedes encontrar en el mundo. La
definición de perro indica qué características ha de tener un ente para ser un
perro, como ser un animal, concretamente doméstico, qué anatomía debe tener,
cómo debe comportarse, etc. Mientras que el propio perro es uno de los casos de
esa definición.

Cada perro tiene una **identidad propia** y es independiente de los otros,
tiene un **comportamiento** concreto (corre, salta, ladra...) y tiene un
**estado** (está despierto o dormido, tiene una edad determinada...).

La diferencia entre una clase y un objeto tiene lógica si lo piensas desde la
perspectiva de que python no tiene ni idea de lo que es un perro y tú tienes
que explicárselo. Una vez lo haces, declarando tu clase, puedes crear
diferentes perros y ponerlos a jugar. Lo bonito de programar es que tu programa
es tu mundo y tú decides lo que es para ti (o para tu programa) un perro.

A nivel práctico, los objetos son grupos de datos (el *estado*) y funciones (la
*funcionalidad*). Estas funciones son capaces de alterar los datos del propio
objeto y no de otro (se intuye el concepto de *identidad*). Analizándolo desde
el conocimiento que ya tienes, es lógico pensar que un objeto es, por tanto,
una combinación de valores y funciones accesible a modo de elemento único.
Exactamente de eso se trata.

Existe una terminología técnica, eso sí, para referirse a esos valores y a esas
funciones. Normalmente los valores se conocen como *propiedades* del objeto y
las funciones se conocen como *métodos*. Así que siempre que hagamos referencia
a cualquiera de estas dos palabras clave debes recordar que hacen referencia a
la programación orientada a objetos.

### Fundamento teórico

La programación basada en clases se basa en tres conceptos fundamentales que
repasaremos aquí de forma rápida para razonar el interés de la programación
orientada a objetos sobre otros paradigmas.

La **encapsulación** trata de crear datos que restrinjan el acceso directo su
contenido con el fin de asegurar una coherencia o robustez interna. Puedes
entender esto como una forma de esconder información o como mi profesor de
Programación II en la universidad solía decir: «Las patatas se pelan en la
cocina del restaurante, no en el comedor». La utilidad de la encapsulación es
la de aislar secciones del programa para tener total control sobre su contenido
gracias a tener total control de la vía de acceso a estos datos. A nivel
práctico este concepto puede usarse para, por ejemplo, obligar a que un objeto
sólo pueda ser alterado en incrementos controlados en lugar de poder pisarse
con un valor arbitrario.

La **herencia** es un truco para reutilizar código de forma agresiva que,
casualmente, sirve como una buena forma de razonar. Aporta la posibilidad de
crear nuevas *clases* a partir de clases ya existentes.  Volviendo a la
simplificación anterior, si una clase es una definición enciclopédica de un
concepto, como un perro, puede estar basada en otra descripción para evitar
contar todo lo relacionado con ella. En el caso del perro, el perro es un
animal. Animal podría ser otra clase definida previamente de la que el perro
heredara y recibiera gran parte de su descripción genérica para sólo cubrir
puntos que necesite especificar como el tamaño, la forma, el tipo de animal, el
comportamiento concreto, etc. Existe la posibilidad de hacer herencias
múltiples también ya que algunos conceptos pueden describirse en dos
superclases distintas: un perro es un animal (vive, muere, se alimenta, se
reproduce) y también es terrestre (camina sobre una superficie, etc). Ambos
conceptos son independientes: los coches también son terrestres pero no son
animales y los peces también son animales pero no terrestres.

Y, finalmente, el **polimorfismo**. La propia etimología de la palabra define
con bastante precisión el concepto, pero aplicarlo a la programación orientada
a objetos no es tan evidente. Existen varios tipos de polimorfismo pero el más
sencillo es entender el *subtyping*. Una vez lo comprendas el resto será
evidente. Si volvemos al ejemplo del perro, para ciertos comportamientos, nos
da igual que tratemos de perros, de peces o de pájaros, todos son animales y
todos los animales se comportan de la misma forma. Es decir, todas las
subclases señaladas comparten el comportamiento de la superclase animal. Si
esto es cierto, puede suponerse que en cualquier caso en el que se espere un
objeto de la clase animal es seguro usar una subclase de ésta.

Visto desde otra perspectiva, las subclases comparten comportamiento porque
reutilizan las funciones de la clase principal o las redefinen (*herencia*),
pero podemos asegurar que todas las subclases tienen un conjunto de funciones
con la misma estructura, independientemente de lo que hagan, que aseguran que
siempre van a ser compatibles. El nombre de esta cualidad viene a que un perro
puede tomar la forma de un animal.

Los otros tipos de polimorfismo explotan el mismo comportamiento de diferentes
maneras, mientras que recuerdes que es posible programar de modo que el tipo de
los datos que trates sea indiferente o pueda variar es suficiente. Otro ejemplo
de esto son los operadores matemáticos, que son capaces de funcionar en
cualquier tipo de número (integer, float, complex, etc.) de la misma manera, ya
que todos son números, al fin y al cabo.

Entender estos conceptos a nivel intuitivo, sin necesidad de entrar en los
detalles específicos de cada uno, es interesante para a la hora de diseñar
programas y facilita de forma radical la comprensión de muchas de las
decisiones de diseño tomadas en python y en proyectos relacionados aunque
también, por supuesto, de otros lenguajes y herramientas.


## Sintaxis

En el siguiente ejemplo se muestra la sintaxis básica a la hora de crear una
clase y después instanciar dos nuevos objetos `bobby` y `beltza`. Los puntos
(`.`) se utilizan para indicar a quién pertenece el método o propiedad al que
se hace referencia (*identidad*). De este modo, no ocurrirá lo mismo cuando el
perro (`Dog`) `bobby` ladre (`bark`) que cuando lo haga el perro `beltza`.

Los métodos describen la *funcionalidad* asociada a los perros en general, pero
además, la función `bark` los describe en particular, haciendo que cada perro
tome su nombre (`name`), una propiedad, es decir: su *estado*.

``` python
class Dog:
    type = "canine"
    def __init__(self, name):
        self.name = name
    def bark(self):
        print("Woof! My name is " + self.name)

bobby  = Dog("Bobby")   # Nuevo Dog llamado "Bobby"
beltza = Dog("Beltza")  # Nuevo Dog llamado "Beltza"

bobby.name    # Bobby
beltza.name   # Beltza

bobby.type    # canine
beltza.type   # canine

bobby.bark()  # "Woof! My name is Bobby"
beltza.bark() # "Woof! My name is Beltza"
```

### Creación de objetos

El ejemplo muestra cómo crear nuevos *objetos* de la clase `Dog`. Las llamadas
a `Dog("Bobby")` y `Dog("Beltza")` crean las diferentes instancias de la clase.

Llamar a los nombres de clase como si de funciones se tratara crea una
instancia de éstas. Los argumentos de entrada de la llamada se envían como
argumentos de la función `__init__` declarada también en el propio ejemplo.
Entiende de momento que los argumentos posicionales se introducen a partir de
la segunda posición, dejando el argumento llamado `self` en el ejemplo para un
concepto que más adelante entenderás.

En el ejemplo, por tanto, se introduce el nombre (`name`) de cada `Dog` en su
creación y la función `__init__` se encarga de asignárselo a la instancia
recién creada mediante una metodología que se explica más adelante en este
mismo capítulo. De momento no es necesario comentar en más profundidad estos
detalles, con lo que sabes es suficiente para entender el funcionamiento
general.

Queda por aclarar, sin embargo, qué es la función `__init__` y por qué tiene un
nombre tan extraño y qué es `type = canine`, que lo trataremos en próximos
apartados de este capítulo.

### Herencia

Antes de entrar en los detalles propuestos en el apartado anterior, que tratan
conceptos algo más avanzados, es interesante ver cómo definir clases mediante
la herencia. Basta con introducir una lista de clases de las que heredar en la
definición de la clase, entre paréntesis, como si de argumentos de entrada de
una función se tratara, tal y como se muestra en la clase `Dog` del siguiente
ejemplo ejecutado en la REPL:

``` python
>>> class Animal:
...     def live(self):
...             print("I'm living")
...
>>> class Terrestrial:
...     def move(self):
...             print("I'm moving on the surface")
...
>>> class Dog(Animal, Terrestrial):
...     def bark(self):
...             print("woof!")
...     def move(self):
...             print("I'm walking on the surface")
...
>>> bobby = Dog()
>>> bobby.bark()
woof!
>>> bobby.live()
"I'm living"
>>> bobby.move()
"I'm walking on the surface"
```

El ejemplo muestra un claro uso de la herencia. La clase `Dog` hereda
automáticamente las funciones asociadas a las superclases, pero es capaz de
definir las propias e incluso redefinir algunas. Independientemente de la
redefinición del método `move`, cualquier perro (`Dog`) va a ser capaz de
moverse por la superficie, porque la superclase `Terrestrial` ya le da los
métodos necesarios para hacerlo. Lo que ocurre es que cualquier subclase de
`Terrestrial` tiene la ocasión moverse (`move`) a su manera: en el caso del
perro, caminando.

>  La herencia es interesante, pero tampoco debe caerse en la psicosis de
> añadir demasiadas superclases. En ocasiones las superclases son necesarias,
> sobre todo cuando aprovechar el polimorfismo facilita el trabajo, pero
> usarlas de forma agresiva genera código extremadamente complejo sin razón.


### Métodos de objeto o funciones de clase

Los métodos reciben un parámetro de entrada llamado `self` que no se utiliza a
la hora de llamarlos: al hacer `bobby.bark()` no se introduce ningún argumento
de entrada a la función `bark`.

Sin embargo, si no se añade el argumento de entrada a la definición del método
`bark` y se llama a `bobby.bark()` pasa lo siguiente:

``` python
>>> class Dog:
...     def bark():
...             pass
...
>>> bobby = Dog()
>>> bobby.bark()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bark() takes 0 positional arguments but 1 was given
```

Python dice que `bark` espera `0` argumentos posicionales pero se le ha
entregado `1`, que nosotros no hemos metido en la llamada, claro está. Así que
ha debido de ser él.

Efectivamente, python introduce un argumento de entrada en los métodos, el
argumento de entrada que por convención se suele llamar `self`. Este parámetro
es el propio `bobby` en este caso.

>  Por convención se le denomina `self`. Tú le puedes llamar como te apetezca
>  pero, si pretendes que los demás te entiendan, mejor `self`.

Para explicar por qué ocurre esto es necesario diferenciar bien entre clase y
objeto. Tal y como hemos hecho antes con las definiciones enciclopédicas
(*clase*) y los conceptos del mundo real que encajan en la definición
(*objeto*). Los objetos también se conocen como instancias, son piezas de
información independiente que han sido creadas a partir de la definición que la
clase aportaba.

En python las clases tienen la posibilidad de tener funciones, que definen el
comportamiento de la clase y no el de los objetos que se crean desde ellas.
Ten en cuenta que las clases también deben procesarse y ocupan un espacio en la
memoria, igual que te ocurre a ti, puedes conocer un concepto y su
comportamiento y luego muchos casos que cumplan ese concepto y ambas cosas
son independientes. Esta posibilidad aporta mucha flexibilidad y permite
definir clases complejas.

Ahora bien, para python las funciones de clase y los métodos (de los objetos,
si no no se llamarían métodos), se implementan de la misma manera. Para la
clase ambas cosas son lo mismo. Sin embargo, el comportamiento del operador
punto (`.`), que dice a quién pertenece la función o método, es diferente si el
valor de la izquierda es una clase o un objeto, introduciendo en el segundo
caso el propio objeto como primer parámetro de entrada, el `self` del que
hablamos, para que la clase sepa qué objeto tiene que alterar. Este es el
mecanismo de la *identidad* del que antes hablamos y no llegamos a definir en
detalle. Cada objeto es único, y a través del `self` se accede a él.

Es un truco interesante para no almacenar las funciones en cada uno de los
objetos como método. En lugar de eso, se mantienen en la definición de la clase
y cuando se llama al método, se busca de qué clase es el objeto y se llama a la
función de la clase con el objeto como argumento de entrada.

Dicho de otra forma, `bobby.bark()` es equivalente a `Dog.bark( bobby )`.

Ilustrado en un ejemplo más agresivo, puedes comprobar que en función de a
través de qué elemento se acceda a la función `bark` python la interpreta de
forma distinta. A veces como función (*function*) y otras veces como método
(*method*), en función de si se accede desde la clase o desde el objeto:

``` python
>>> class Dog:
...     def bark(self):
...             pass
...
>>> type ( Dog.bark)
<class 'function'>
>>> type ( bobby.bark )
<class 'method'>
```

>  También te habrás fijado, y si no lo has hecho es momento de hacerlo,
> que los nombres de las clases empiezan por mayúscula en los ejemplos (`Dog`)
> mientras que los objetos comienzan en minúscula (`bobby`). Se trata de otra
> convención ampliamente utilizada para saber diferenciar entre uno y otro de
> forma sencilla. Es evidente cuál es la clase y el objeto con los nombres que
> hemos tratado en los ejemplos, pero en otros casos puede no serlo y con este
> sencillo truco facilitas la lectura de tu código. Hay muchas ocasiones en las
> que esta convención se ignora, así que cuidado.  
> Prueba a hacer `type(int)` en la terminal.

### Variables de clase

En el primer ejemplo del capítulo hemos postergado la explicación de `type =
canine` y ahora que ya manejas la mayor parte de la terminología y dominas la
diferencia entre una clase y una instancia de ésta (un *objeto*) es momento de
recogerla. A continuación se recupera la sección del ejemplo para facilitar la
consulta, fíjate en la línea 2.

``` {.python .numberLines}
class Dog:
    type = "canine"
    def __init__(self, name):
        self.name = name
    def bark(self):
        print("Woof! My name is " + self.name)
```

`type` es lo que se conoce como una *variable de clase* (*class variable*).

>  En este documento se ha evitado de forma premeditada usar la palabra
>  *variable* para referirse a los valores y sus referencias con la intención
>  de marcar la diferencia entre ambos conceptos. En este apartado, sin
>  embargo, a pesar de que se siga tratando de una referencia, se usa el nombre
>  *class variable* porque es como se le llama en la documentación y así será
>  más fácil que lo encuentres si en algún momento necesitas buscar información
>  al respecto. De esto ya hemos discutido en el capítulo sobre datos, donde
>  decimos que *todo es una referencia*.


Previamente hemos hablado de que los objetos pueden tener propiedades
asociadas, y cada objeto tendrá las suyas. Es decir, que cada instancia de la
clase puede tener sus propias propiedades independientes. El caso que tratamos
en este momento es el contrario, el `type` es un valor que comparten **todas**
las instancias de `Dog`. Cualquier cambio en esos valores los verán todos los
objetos de la clase, así que hay que tener cuidado.

El acceso es idéntico al que ocurriría en un valor asociado al objeto, como en
el caso `name` del ejemplo, pero en este caso observas que en su declaración en
la clase no es necesario indicar `self`. No es necesario decir cuál es la
instancia concreta a la que se le asigna el valor: se le asigna a todas.

Aparte de poder acceder a través de los objetos de la clase, es posible
acceder directamente desde la clase a través de su nombre, como a la hora de
acceder a las funciones de clase: `Dog.type` resultaría en `"canine"`.

>  Si en algún caso python viera que un objeto tiene propiedades y
> variables de clase definidas con el mismo nombre, cosa que no debería ocurrir
> a menudo, tendrán preferencia las propiedades.

### Encapsulación explícita

Es posible que te encuentres en alguna ocasión con métodos o propiedades,
*campos* en general, cuyo nombre comience por `_` o por `__`. Se trata de casos
en los que esas propiedades o métodos quieren ocultarse del exterior.

El uso de `_` al inicio del nombre de un campo es una convención que avisa de
que este campo no debe accederse desde el exterior de la clase y su objetivo es
usarlo desde el interior de ésta.

Esta convención se llevó al extremo en algún momento y se decidió crear un caso
en el que esta convención inicial tuviera cierta funcionalidad añadida para el
doble guión bajo (`__`) que impidiera un acceso accidental a esos campos
conocido como *name mangling*.

#### Campos privados: *name mangling*

El *name mangling* es un truco que hace python para asegurarse de que no se
entra por accidente a las secciones que empiezan por `__`. Añade
`_nombredeclase` al inicio de los campos, transformando su nombre final y
dificultando el acceso por accidente.

Ese acceso accidental no sólo es para que quien programa no acceda, ya que, si
se esfuerza la suficiente, va a poder hacerlo de igual modo, sino para que el
propio python no acceda al campo que no corresponde. El hecho de añadir el
nombre de la clase al campo crea una brecha en la herencia, haciendo que los
campos no se hereden de la forma esperada.

En una subclase en la que los campos de la clase madre han sido marcados con
`__`, la herencia hace que estos campos se hereden con el nombre cambiado que
contiene el nombre de la superclase. De este modo, es difícil para la subclase
pisar estos campos ya que tendría que definirlos manualmente con el nombre
cambiado. Crear nuevos campos con `__` no funcionaría, ya que, al haber
cambiado de clase, el nombre generado será distinto.

Este mecanismo es un truco para crear *campos privados*, concepto bastante
común en otros lenguajes como Java o C++, que en python es inexistente.

El concepto de los *campos privados* es interesante en la programación
orientada a objetos. Pensando en la *encapsulación*, es lógico que a veces las
clases definan métodos o propiedades que sólo los objetos creados a partir de
ellas conozcan y que los objetos creados de clases heredadas no. Este es el
método que python tiene para aportar esta funcionalidad.

Es interesante añadir, por otro lado, que python es un lenguaje de programación
muy dinámico por lo que la propia definición de las clases, y muchas cosas más,
puede alterarse una vez creadas. Esto significa que el hecho de ocultar campos
no es más que un acuerdo tácito entre quienes programan porque, si quisieran,
podrían definir todo de nuevo. Trucos como este sirven para que seamos
conscientes de que estamos haciendo cosas que se supone que no deberíamos
hacer.  Cuando programes en python, tómate esto como pistas que te indican cómo
se supone que deberías estar usando las clases.

### Acceso a la superclase

A pesar de la herencia, no siempre se desea eliminar por completo la
funcionalidad de un método o pisar una propiedad. A veces es interesante
simplemente añadir funcionalidad sobre un método o recordar algún valor
definido en la superclase.

Python soporta la posibilidad de llamar a la superclase mediante la función
`super`, que permite el acceso a cualquier campo definido en la superclase.

``` python
class Clase( SuperClase ):
    def metodo(self, arg):
        super().metodo(arg)     # Llama a la definición de
                                # `metodo` de `SuperClase`
```

>  `super` busca la clase previa por preferencia, si usas herencias
> múltiples y pisas los campos puede complicarse.


## Duck Typing

Una de las razones principales para usar programación orientada a objetos es
que, si se eligen los métodos con precisión, pueden crearse estructuras de
datos que se comporten de similar forma pero que tengan cualidades diferentes.
Independientemente de cómo estén definidas sus clases, si dos objetos disponen
de los mismos métodos podrán ser sustituidos el uno por el otro en el programa
y seguirá funcionando aunque su funcionalidad cambie.

Dicho de otra forma, dos objetos (o dos cosas, en general) podrán ser
intercambiados si disponen de la misma *interfaz*. *Interfaz*, de *inter*:
entre; y *faz*: cara, viene a significar algo así como «superficie de contacto»
y es la palabra que se usa principalmente para definir la frontera compartida
entre dos componentes o, centrándonos en el caso que nos ocupa, su conexión
funcional.

Si recuerdas la *herencia* y la combinas con estos conceptos, puedes
interpretar que además de una metodología para reutilizar código es una forma
de crear nuevas definiciones que soporten la misma interfaz.

En otros lenguajes de programación, Java, por ejemplo, existe el concepto
*interfaz* que serían una especie de pequeñas clases que definen qué funciones
debe cumplir una clase para ser compatible con la interfaz. A la hora de crear
las clases se les puede indicar qué interfaces implementan y el lenguaje se
encarga de asegurarse de que quien programa ha hecho todo como debe.

El dinamismo de python hace que esto sea mucho más flexible. Debido a que
python no hace casi comprobaciones antes de ejecutarse, necesita un método
mucho más directo. Python aplica lo que se conoce como *duck typing* (el tipado
del pato), es decir, *si anda como un pato, vuela como un pato y nada como un
pato: es un pato*.

Para que los objetos creados manualmente puedan comportarse como los que el
propio sistema aporta, python define unos nombres de métodos especiales
(*special method names*). Estos métodos tienen infinidad de utilidades: que sea
posible utilizarlos como iterable en un `for`, que el sistema pueda cerrarlos
de forma automática, buscar en ellos usando el operador `in`, que puedan
sumarse y restarse con los operadores de suma y resta, etc. Simplemente,
el sistema define qué funciones se deben cumplir en cada uno de esos casos y
cuando se encuentre con ellos intentará llamarlas automáticamente. Si el
elemento no dispone de esas funciones lanzará una excepción como la que lanza
cuando intentamos acceder a un método que no existe (que es básicamente lo que
estamos haciendo en este caso).

En general, python, con el fin de diferenciar claramente qué nombres se eligen
manualmente y cuales han sido seleccionados por el lenguaje, suele utilizar una
convención para la nomenclatura: comienzan y terminan por: `__`

A continuación se describen algunos de los nombres especiales más comunes.
Algunos ya han aparecido a lo largo de los ejemplos del documento, otros las
verás por primera vez ahora. Existen muchos más, y todos están extremadamente
bien documentados. Si en algún momento necesitas crear algunos nuevos, la
documentación de python es una buena fuente donde empezar.

Todos los nombres de métodos especiales agrupan un conjunto de características
que se presentan con una palabra, en muchos casos inventada, terminada en
*-able*. Python utiliza también este tipo de nombres, como el ya aparecido
*llamable*, o *callable* en inglés, que se refiere a cualquier cosa que puede
ser llamada. Representar las capacidades de esta manera sirve para expresar el
interés de los nombres de métodos especiales. Si en algún momento necesitas
crear una clase que defina un objeto en el que se puede buscar necesitas que
sea un *buscable*, es decir, que soporte el nombre de método especial que
define ese comportamiento.

### *Representable*

Un objeto representable es aquél que puede representarse automáticamente en
modo texto.  Al ejecutar la función `print` o al exponer valores en la REPL
(recuerda que la P significa print), python trata de visualizarlos.

El método `__repr__` se ejecuta justo antes de imprimirse el objeto, de
forma automática. La función requiere que se devuelva un elemento de tipo
string, que será el que después se visualice.

En el ejemplo a continuación se comienza con la clase `Dog` vacía y se
visualiza una de sus instancias. Posteriormente, se reasigna la función
`__repr__` de `Dog` con una función que devuelve un string. Al volver a mostrar
a `bobby` el resultado cambia.

Como se ve en el ejemplo, es interesante tener una buena función de
representación si lo que se pretende es entender el contenido de los objetos.

>  Python ya aporta una forma estándar de representar los objetos, si la
> función `__repr__` no se define simplemente se usará la forma estándar.

``` python
>>> class Dog:
...     pass
...
>>> bobby = Dog()
>>> bobby
<__main__.Dog object at 0x7fb7fba1b908>

>>> Dog.__repr__ = lambda self: "Dog called " + self.name
>>> bobby.name = "Bobby"
>>> bobby
Dog called Bobby
>>>
```

### *Contable*

En python se utiliza la función `len` para comprobar la longitud de cualquier
elemento contable. Por ejemplo:

``` python
>>> len( (1,2,3) )
3
```

Las objetos que soporten esta función podrán contarse para conocer su longitud
mediante la función `len`. Python llamará al método  `__len__` del objeto (que
se espera que devuelva un número entero) y ésta será su longitud. Siguiendo con
el ejemplo anterior:

``` python
>>> Dog.__len__ = lambda self: 12     # Siempre devuelve 12
>>> len(bobby)
12
```

Este método permite crear elementos contables, en lugar de los típicos
diccionario, tupla y lista. Como por ejemplo los ya existentes `NamedTuple`,
`OrderedDict` y otros. Los métodos *buscable* e *iterable* también son muy
interesantes para esta labor.

### *Buscable*

El método `__contains__` debe devolver `True` o `False` y recibir un argumento
de entrada. Con esto el objeto será capaz de comprobarse con sentencias que
hagan uso del operador `in` (y `not in`). Las dos llamadas del ejemplo son
equivalentes. La segunda es lo que python realiza internamente al encontrarse
el operador `in` o el operador `not in`.

``` python
>>> 1 in [1,2,3]
True
>>> [1,2,3].__contains__(1)
True
```

### *Hasheable*

Los objetos *hasheables*, pueden convertirse a un valor numérico mediante una
función *hash*. Estas funciones habilitan la existencia de los diccionarios,
siendo el mecanismo principal para obtener una mejora en el rendimiento en el
acceso y la inserción aunque también sirven para infinidad de aplicaciones,
como la comparación de objetos, agrupaciones, etc.

La función `__hash__` será ejecutada siempre que se intente aplicar `hash()` a
un objeto, cosa que ocurre de forma automática en varios escenarios. Su único
requerimiento es que retorne un número, y que el *hash* de dos objetos
idénticos sea el mismo.

Con el fin de que las comparaciones entre objetos puedan realizarse como es
debido, es necesario implementar al menos la función `__eq__`, función que será
llamada al realizar comparaciones como `a == b`. Ésta sirve para poder realizar
comparaciones complejas (*rich comparisons*) en objetos que en principio no
pueden compararse.

Los objetos básicos de python son *hasheables*.


### *Iterable*

Estos métodos permiten crear objetos con los que es posible iterar en bucles
`for` y otras estructuras. Por ejemplo, los archivos de texto en python
soportan este protocolo, por lo que pueden leerse línea a línea en un bucle
`for`.

Igual que en el caso de `__len__`, que servía para habilitar la llamada a la
función `len`, `__iter__` y `__next__` sirven, respectivamente, para habilitar
las llamadas a `iter` y `next`.

La función `iter` sirve para convertir obtener un *iterador* de un *iterable*.
Este *iterador* es un objeto que soporta el funcionamiento de la función
`next`. Y `next` sirve para pasar al siguiente elemento de la iteración.
Ejemplificado:

``` python
>>> l = [1,2,3]
>>> next(l)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator
>>> it = iter(l)
>>> it
<list_iterator object at 0x7ff745723908>
>>>
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
```

La función `__next__` tiene un comportamiento muy sencillo. Si hay un próximo
elemento, lo devuelve. Si no lo hay lanza la excepción `StopIteration`, para
que la capa superior la capture.

Fíjate que la lista, que es un elemento sobre el que se puede iterar, esto es,
un *iterable*, no soporta `next` sino que necesita obtener un *iterador* a
partir de ella mediante `iter` y es éste el que soporta el `next`. Esto se debe
a que la función `iter` está pensada para restaurar la posición del cursor en
el primer elemento y poder volver a iniciar la iteración.

Sorprendentemente, éste es el procedimiento de cualquier `for` en python. El
`for` es una estructura creada sobre un `while` que obtiene el *iterador* e
itera sobre él automáticamente.

Este bucle `for`:

``` python
for el in iterable:
    # hace algo con `el`
```

Realmente se implementa de la siguiente manera:

```  python
# Construye un iterador desde la secuencia
iterador = iter(secuencia)

# Bucle infinito que se rompe cuando `next` lanza una
# excepción de tipo `StopIteration`
while True:
    try:
        el = next(iterador)
        # hace algo con `el`
    except StopIteration:
        break
```

Así que, si necesitas una clase con capacidad para iterarse sobre ella, puedes
crear un pequeño *iterador* que soporte el método `__next__` y devolver una
instancia nueva de éste en el método `__iter__` de tu clase.

La diferencia entre el *iterable* y el *iterador* es importante: el *iterable*
es un objeto sobre el que se puede iterar, y el *iterador* el objeto que se
utiliza para iterar sobre el *iterable*. Es decir, el *iterador* es la
herramienta que se usa para iterar sobre algo sobre lo que se puede iterar (un
*iterable*). Esto permite separar conceptos de forma clara, permitiendo, como
se introdujo antes, reiniciar el iterador cada vez que se crea un bucle nuevo,
pero también permitiendo asignar diferentes modos de iteración para el mismo
tipo de objeto. Imagina, por ejemplo, in iterador que salta los elementos
impares de una colección.

En la práctica, en casos sencillos, el propio *iterable* implementa la interfaz
de su *iterador* y se devuelve a sí mismo en el método `__iter__`. De esta
manera el programador no necesita escribir dos clases y se puede salir con la
suya haciendo la mitad del trabajo, pero la realidad es que es interesante
separar los conceptos, sobre todo para el caso más general.

### *Inicializable*

El método `__init__` es uno de los más usados e interesantes de esta lista, esa
es la razón por la que ha aparecido en más de una ocasión durante este
capítulo.

El método `__init__` es a quien se llama al crear nuevas instancias de una
clase y sirve para *ini*cializar las propiedades del recién creado objeto.

Cuando se crean nuevos objetos, python construye su estructura en memoria,
pidiéndole al sistema operativo el espacio necesario. Una vez la tiene, envía
esa estructura vacía a la función `__init__` como primer argumento para que sea
ésta la encargada de rellenarla.

Como se ha visto en algún ejemplo previo, el método `__init__` (es un método,
porque el objeto, aunque vacío, ya está creado) puede recibir argumentos de
entrada adicionales, que serán los que la llamada al nombre de la clase reciba,
a la hora de crear los nuevos objetos. Es muy habitual que el inicializador
reciba argumentos de entrada, sobre todo argumentos con nombre, para que quien
cree las instancias tenga la opción de inicializar los campos que le interesen.

Volviendo a un ejemplo previo:

``` python
class Dog:
    type = "canine"
    def __init__(self, name):
        self.name = name
    def bark(self):
        print("Woof! My name is " + self.name)

bobby = Dog("Bobby")  # Aquí se llama a __init__
```

El nombre del perro, `"Bobby"` será recibido por `__init__` en el argumento
`name` e insertado al `self` mediante `self.name = name`. De este modo, esa
instancia de `Dog`, `bobby`, tomará el nombre `Bobby`.

>  En muchas ocasiones, el método `__init__` inicializa a valores vacíos
> todas las posibles propiedades del objeto con el fin de que quien lea el
> código de la clase sea capaz de ver cuáles son los campos que se utilizan en
> un primer vistazo. Es una buena práctica listar todos los campos posibles en
> `__init__`, a pesar de que no se necesite inicializarlos aún, con el fin de
> facilitar la lectura.

>  Quien tenga experiencia con C++ puede equivocarse pensando que
> `__init__` es un constructor. Tal y como se ha explicado anteriormente, al
> método `__init__` ya llega un objeto construido. El objetivo de `__init__` es
> inicializar. En python el constructor, que se encarga de crear las instancias
> de la clase, es la función `__new__`.

>  Si creas una clase a partir de la herencia y sobreescribes su método
> `__init__` es posible que tengas que llamar al método `__init__` de la
> superclase para inicializar los campos asociados a la superclase. Recuerda
> que puedes acceder a la superclase usando `super`.

Los tipos básicos de python están definidos en clases también, pero su nombre
puede usarse para hacer conversiones.

```python
>>> int("1")
1
```

Para entender el funcionamiento de esa llamada, no hay más que recordar el que
el aplicar el nombre de la clase como una llamada a función sirve para crear un
objeto del tipo indicado, enviando los argumentos a su inicializador. Es decir,
simplemente soportan el método `__init__` y construyen un nuevo objeto del tipo
indicado a partir de lo que se les envía como argumento.

### *Abrible* y *cerrable*

Los objetos *abribles* y *cerrables* puedan ser abiertos y cerrados de forma
segura y con una sintaxis eficiente. Aunque no se van a detallar en
profundidad, el objetivo de este punto es mostrar la sentencia `with` que se
habilita gracias a estos métodos y mostrar cómo facilitan la apertura y cierre.

El PEP 343[^pep343] muestra en detalle la implementación de la sentencia
`with`. Simplificándolo y resumiéndolo, `with` sirve para abrir elementos y
cerrarlos de forma automática.

>  Los PEP (*Python Enhancement Proposals*) son propuestas de mejora para
> el lenguaje. Puedes consultar todos en la web de python. Son una fuente
> interesante de información y conocimiento del lenguaje y de programación en
> general.  
> <https://www.python.org/dev/peps/>

Pensando en, por ejemplo, la lectura de un archivo, se requieren varias etapas
para tratar con él, por ejemplo:

``` python
f = open("file.txt")    # apertura del fichero
f.read()                # lectura
f.close()               # cierre
```

Este método es un poco arcaico y peligroso. Si durante la lectura del fichero
ocurriera alguna excepción el fichero no se cerraría, ya que la excepción
bloquearía la ejecución del programa. Para evitar estos problemas, lo lógico
sería hacer una estructura `try-except` y añadir el cierre del fichero en un
`finally`.

La sentencia `with` se encarga básicamente de hacer eso y facilita la escritura
de todo el proceso quedándose así:

``` python
with f as open("file.txt"): # apertura
    f.read()                # en este cuerpo `f` está abierto

# Al terminar el cuerpo, de forma normal o forzada,
# `f` se cierra.
```

Ahora bien, para que el fichero pueda ser abierto y cerrado automáticamente,
deberá tener implementados los métodos `__enter__` y `__exit__`. En el PEP 343
se muestra la equivalencia entre la sentencia `with` y el uso de `__enter__`,
`__close__` y el `try-except`.

[^pep343]: Puedes leer el contenido completo del PEP en:  
<https://www.python.org/dev/peps/pep-0343/>

### *Llamable*

Queda pendiente desde el capítulo sobre funciones responder a qué es un
*callable* o *llamable*. Una vez llegados a este punto, tiene una respuesta
fácil: un *llamable* es un objeto que soporta el método `__call__`.

Aunque pueda parecer sorprendente, las funciones en python también se llaman de
este modo, así que realmente son objetos que se llaman porque soportan este
método. Es lógico, porque las funciones, recuerda el capítulo previo, pueden
guardar valores, como el contexto en el que se crean (la *closure*). Las
funciones son meros *llamables* y como tales se comportan.

``` python
>>> class Dog:
...     def __call__(self):
...         print("Dog called")
...
>>> dog = Dog()
>>> dog()
Dog called
```

Ten en cuenta que el método `__call__` puede recibir cualquier cantidad de
argumentos como ya hemos visto en apartados anteriores, pero el primero será el
propio objeto que está siendo llamado, el `self` que ya conocemos.

Resumiendo, el método `__call__` describe cómo se comporta el objeto cuando se
le aplican los paréntesis.

### *Subscriptable*

Tal y como el método anterior describía cómo se aplican los paréntesis a un
objeto, los métodos que se muestran en este apartado describen el
comportamiento del objeto cuando se le aplican los corchetes. Recordando el
capítulo sobre datos, los corchetes sirven para acceder a valores de las
listas, tuplas y diccionarios.

Cuando python encuentra que se está tratando de acceder a un campo de un objeto
mediante los corchetes llama automáticamente al método `__getitem__` y cuando
se intenta asociar un campo a un valor llama al método `__setitem__` del
objeto. Al pedir la eliminación de un campo del objeto con la sentencia `del`,
se llama al método `__delitem__`.

Aunque en otros conjuntos de métodos aquí descritos hemos inventado un nombre
para este documento, Python denomina a este comportamiento *subscriptable* así
que cuando intentes acceder usando corchetes a un objeto que no soporta esta
funcionalidad, el error que saltará utilizará la misma nomenclatura que
nosotros.

El siguiente ejemplo muestra el funcionamiento en una clase que en lugar de
usar los métodos, imprime en pantalla. Lo lógico y funcional sería utilizar
estos dos métodos para facilitar el acceso a campos de estas clases o para
crear clases que pudiesen sustituir a listas, tuplas o diccionarios de
forma sencilla, pero puede hacerse cualquier porque son métodos normales.

``` python
>>> class Dog:
...     def __getitem__(self, k):
...             print(k)
...     def __setitem__(self, k, v):
...             print(k, v)
...
>>> bobby = Dog()
>>> bobby["field"]
field
>>> bobby["field"] = 10
field 10
```

Fíjate en que reciben diferente cantidad de argumentos de entrada cada uno de
los métodos. El método `__setitem__` necesita indicar no sólo qué *item* desea
alterarse, sino su también su valor.

#### *Slice notation*

Se trata de una forma avanzada de seleccionar las posiciones de un objeto, el
nombre viene de *slice*, rebanada, y significa que puede coger secciones del
objeto en lugar de valores únicos. Piénsalo como en una barra de pan cortada en
rebanadas de la que quieres seleccionar qué rebanadas te interesan en bloque.

No todos los objetos soportan *slicing*, pero los que lo hacen permiten acceder
a grupos de valores en el orden en el que están indicando el inicio del grupo
(inclusive), el final (no inclusive) y el salto de un elemento al siguiente.

Además, los valores del *slice* pueden ser negativos. Añadir un número negativo
al salto implica que el salto se hace hacia atrás. Añadirlo en cualquier de los
otros dos valores, inicio o final de grupo, implica que se cuenta el elemento
desde el final de la colección en dirección opuesta a la normal.

La sintaxis de los *slice*s es la siguiente: `[inicio:fin:salto]`.
Cada uno de los valores es opcional y si no se añaden se comportan de la
siguiente manera:

- Inicio: primer elemento
- Fin: último elemento inclusive
- Salto: un único elemento en orden de cabeza a cola

>  El índice para representar el último elemento es -1, pero si se quiere
> indicar como final, usar -1 descartará el último elemento porque el final no
> es inclusivo. Para que sea inclusivo es necesario dejar el campo fin vacío.

Dada una lista de los números naturales del 1 al 99, ambos incluidos, de
nombre `l` se muestran unos casos de *slicing*.

``` python
>>> l[-5:]
[95, 96, 97, 98, 99]
>>> l[6:80:5]
[6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61, 66, 71, 76]
>>> l[60:0:-5]
[60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5]
```

La sintaxis de los *slice*s mostrada sólo tiene sentido a la hora de acceder a
los campos de un objeto, si se trata de escribir suelta lanza un error de
sintaxis. Para crear *slice*s de forma separada se construyen mediante la
clase `slice` de la siguiente manera: `slice(inicio, fin, salto)`.

En los métodos que permiten a un objeto ser *subscriptable* (`__getitem__`,
`__setitem__` y `__delitem__`) a la hora de elegir un *slice* se recibe una
instancia del tipo *slice* en lugar de una selección única como en el ejemplo
previo:

``` python
>>> class Dog:
...     def __getitem__(self, item):
...             print(item)
...
>>> bobby = Dog()
>>> bobby[1:100]
slice(1, 100, None)
>>> bobby[1:100:9]
slice(1, 100, 9)
>>> bobby[1:100:-9]
slice(1, 100, -9)
```

Por complicarlo todavía más, los campos del *slice* creado desde la clase
`slice` pueden ser del tipo que se quiera. El formato de los `:` es únicamente
*sintactic sugar* para crear *slices* de tipo integer o string.  Aunque después
es responsabilidad del quien implemente soportar el tipo de *slice* definido,
es posible crear *slices* de lo que sea, incluso anidarlos.

Como ejemplo de un caso que utiliza *slices* no integer, los tipos de datos
como los que te puedes encontrar en la librería `pandas` soportan *slicing*
basado en claves, como si de un diccionario se tratara.

### Ejemplo de uso

Para ejemplificar varios de estos métodos especiales, tomamos como ejemplo una
pieza de código fuente que quien escribe este documento ha usado en alguna
ocasión en su trabajo como desarrollador.

Se trata de un iterable que es capaz de iterar en un sistema de ficheros
estructurado en carpetas *año-mes-día* con la estructura `AAAA/MM/DD`. Este
código se creó para analizar datos que se almacenaban de forma diaria en
carpetas con esta estructura. Diariamente se insertaban fichero a fichero por
un proceso previo y después se realizaban análisis semanales y mensuales de los
datos. Esta clase permitía buscar por las carpetas de forma sencilla y obtener
rápidamente un conjunto de carpetas que procesar.

El ejemplo hace uso del módulo `datetime`, un módulo de la librería estándar
que sirve para procesar fechas y horas. Por ahora, puedes ver la forma de
importarlo como una receta y en el siguiente capítulo la entenderás a fondo. El
funcionamiento del módulo es sencillo y puedes usar la ayuda para comprobar las
funciones que no conozcas.

Te animo a que analices el comportamiento del ejemplo, viendo en detalle cómo
se comporta. Como referencia, fuera de la estructura de la clase, en las
últimas líneas, tienes disponible un bucle que puedes probar a ejecutar para
ver su comportamiento.

``` {.python .numberLines}
from datetime import timedelta
from datetime import date

class dateFileSystemIterator:

    """
    Iterate over YYYY/MM/DD filesystems or similar.
    """
    def __init__( self, start = date.today(), end = date.today(),
                  days_step = 1, separator = '/'):
        self.start = start
        self.current = start
        self.end = end
        self.separator = separator
        self.step = timedelta( days = days_step )

    def __iter__( self ):
        self.current = self.start
        return self

    def __next__( self ):
        if self.current >= self.end:
            raise StopIteration
        else:
            self.current += self.step
            datestring = self.current - self.step
            datestring = datestring.strftime( "%Y" \
                                + self.separator \
                                + "%m"+self.separator \
                                +"%d")
            return datestring

    def __repr__( self ):
        out = self.current - self.step
        tostring = lambda x: x.strftime("%Y" \
                        + self.separator \
                        + "%m" \
                        + self.separator + "%d")
        return "<dateFileSystemIterator: <Current: " \
                + tostring(self.current) + ">" \
                + ",<Start: " + tostring(self.start)  + ">" \
                + ",<End: " + tostring(self.end)  + ">" \
                + ",<Step: " + str(self.step) +  ">"


it = dateFileSystemIterator(start = date.today() - timedelta(days=30))
print(it)
for i in it:
    print(i)
```

> Esta clase implementa tanto el iterable como su propio iterador, pero fíjate
> que cuando se llama a `__iter__` reinicia la cuenta. Otra forma de hacer esto
> podría ser con dos clases que diferencien bien los conceptos (*iterable* e
> *iterador*), pero es quizás demasiado escribir para un caso tan sencillo.
>
> El método `__repr__` hace mucho uso de la concatenación de strings y de la
> conversión de valores a string. Históricamente en python siempre ha habido
> formas más elegantes de hacer esto. La más reciente (a partir de python 3.6)
> es utilizar lo que se conoce como *formatted string literals* o *f-strings*.
> Puedes leer más sobre ellos en el PEP 498[^pep498]

[^pep498]: Puedes leer el contenido completo del PEP en:  
<https://peps.python.org/pep-0498/>

####  Ejercicio libre: generadores

La parte de la iteración del ejemplo previo puede realizarse forma más breve
mediante el uso de la sentencia `yield`. Aunque no la trataremos, `yield`
habilita muchos conceptos interesantes, entre ellos los *generadores*.

A continuación tienes un ejemplo de cómo resolver el problema anterior mediante
el uso de esta sentencia. Te propongo como ejercicio que investigues cómo
funciona buscando información sobre los *generadores* (*generator*) y la
propia sentencia `yield`.

``` python
from datetime import datetime, timedelta

def iterate_dates( date_start, date_end=datetime.today(),
                    separator='/', step=timedelta(days=1) ):
    date = date_start
    while date < date_end:
        yield date.strftime('%Y'+separator+'%m'+separator+'%d')
        date += step
```

`yield` tiene mucha relación con las *corrutinas* (*coroutine*) que, aunque no
se tratarán en este documento, son un concepto muy interesante que te animo a
investigar. Si lo haces, verás que los generadores son un caso simple de una
corrutina.

## Lo que has aprendido

Este capítulo también ha sido intenso como el anterior, pero te prometo que no
volverá a pasar. El interés principal de este capítulo es el de hacerte conocer
la programación orientada a objetos y enseñarte que en python lo inunda todo.
Todo son objetos.

Para entenderlo has comenzado aprendiendo lo que es la programación orientada a
objetos, concretamente la orientada a clases, donde has visto por primera vez
los conceptos de identidad propia, comportamiento y estado.

Desde ahí has saltado al fundamento teórico de la programación orientada a
objetos y has visitado la encapsulación, la herencia y el polimorfismo para
luego, una vez comprendidos, comenzar a definir clases en python.

Esto te ha llevado a necesitar conocer qué es el argumento que suele llamarse
`self`, una excusa perfecta para definir qué son las variables y funciones de
clase y en qué se diferencian de las propiedades y métodos.

Como la encapsulación no se había tratado en detalle aún, lo próximo que has
hecho ha sido zambullirte en los campos privados viendo cómo python los crea
mediante un truco llamado *name mangling* y su impacto en la herencia.

Aunque en este punto conocías el comportamiento general de la herencia hacia
abajo, necesitabas conocerlo hacia arriba. Por eso, ha tocado visitar la
función `super` en este punto, función que te permite acceder a la superclase
de la clase en la que te encuentras. En lugar de contártela en detalle, se te
ha dado una pincelada sobre ella para que tú investigues cuando lo veas
necesario, pero que sepas por dónde empezar.

Para describir más en detalle lo calado que está python de programación
orientada a objetos necesitabas un ejemplo mucho más agresivo: los métodos con
nombres especiales.  A través de ellos has visto cómo python recoge las
funcionalidades estándar y te permite crear objetos que las cumplan. Además, te
ha servido para ver que **todo** en python es un objeto (hasta las clases lo
son[^objects]) y para ver formas elegantes de resolver problemas comunes, como
los iteradores, `with` y otros.

También, te recuerdo que, aunque sea de forma colateral y sin prestarle
demasiada atención, se te ha sugerido que cuando programamos no lo hacemos
únicamente para nosotros mismos y que la facilidad de lectura del código y la
preparación de éste para que otros lo usen es primordial. Los próximos
capítulos tratan en parte de ésto: de hacer uso del patrimonio tecnológico de
la humanidad, y de ser parte de él.

[^objects]: Puedes preguntárselo a python:  
    ``` python
    >>> class C: pass
    ...

    >>> isinstance(C, object)
    True
    ```