# Interfícies #### **Concepte** Dins del paradigma de programació orientada a objectes, les interfaces són elements molt semblants a les classes, però tenen una sèrie de peculiaritats que les fan diferents a aquestes com: * Les interfícies no defineixen atributs, o, dit d'una altra, forma variables d'instància. * Tots els seus mètodes han de ser abstractes, és a dir, definim la seva signatura però en cap cas definim cap instrucció en el cos del mètode. És a dir, la implementació del mètode. Utilitat? Poder ser implementades en subclasses, de tal forma que d'una interfície es dissenyen classes. Així, les classes que implementen interfícies el que fan és implementar els mètodes d'aquestes. Dues (o més) classes implementant una mateixa interfície poden fer servir els seus mètodes, però cada classe afegirà comportaments diferents a aquests mètodes. En termes de diagrames de classes, una interfície pot representar-se com segueix: ![inter.drawio](https://hackmd.io/_uploads/rkQkkOV9p.png) #### **Interfícies a python: Aproximació informal.** A diferència d'altres llenguatges com java o C++, Python no utilitza la paraula reservada "interface" per referir-se a les interfícies. Una manera informal de definir una interfície a python podria ser aquesta, on definirem una classe amb una sèrie de mètodes, els quals podran ser implementats de forma diferent per classes (l'override), però sense exercir un control estricte. ``` class VehicleInterface: def arrancar(self) -> str: pass def aturarse(self) -> str: pass def accelerar(self, speed: int) -> str: pass def frenar(self, speed: int) -> str: pass ``` Com veieu, VehicleInterface no deixa de ser una classe python normal amb 4 mètodes. Necessitarem definir ara classes concretes que implementin aquesta interfície. Per poder informar als usuaris que en efecte la classe VehicleInterface és una interfície fem servir el que s'anomena "Duck Typing": ![image](https://hackmd.io/_uploads/SJ_PpL4q6.png) *Font: [Udacity - Introduction to Computer Science](https://www.youtube.com/watch?v=WM1x2L1dFJk)* Fonamentalment el "Duck Typing" dóna més importància als mètodes (que implementa la classe) que al tipus d'objecte que s'instancia o al nom de la classe en si. En aquest cas són els mètodes que no tenen cap implementació el que ens dóna la pista del "Duck Typing". Ara implementem informalment en dues classes concretes: ``` class Cotxe(VehicleInterface): def arrencar(self) -> str: return "Arrenco en primera" def aturarse(self) -> str: return "Aturo en punt mort i amb el fre de mà posat" def accelerar(self, speed: int) -> str: return "Accelero fins a " + str(speed) + " Km/h" def frenar(self, speed: int) -> str: return "Redueixo la velocitat en " + str(speed) + " Km/h" ``` ``` class Patinet(VehicleInterface): def arrencar(self) -> str: return "Arrenco amb el manillar" def aturarse(self) -> str: return "Aturo amb el manillar i posant un peu a terra" def accelerar(self, speed: int) -> str: return "Accelero fins a " + str(speed) + " Km/h" def frenarEnSemafor(self, speed: int) -> str: return "Com a patinet no m'aturo al semafor i no reduiré la velocitat en " + str(speed) + " Km/h" ``` Fixeu-vos que per fer-ho s'ha referenciat a la classe que fa d'interfície posant-la entre parèntesi. Per comprovar que en efecte ambdues classes implementen la interfície es pot fer servir el mètode **issubclass()**: ``` issubclass(Cotxe,VehicleInterface) issubclass(Patinet,VehicleInterface) ``` En ambdós casos, la resposta es True i les dues classes implementen VehicleInterface Ara bé, si mireu la classe Patinet, veureu que té un petit problema. Per definició, una interfície no hauria de permetre que una subclasse implementi mètodes amb una signatura diferent als definits a la interfície. En aquest cas Patinet implementa frenarEnSemafor, el qual no apareix a VehicleInterface i contradiu obertament el que esperem d'una interfície. Per tant, aquest enfoc informal no sembla el més adequat quan un projecte creix en funcionalitats i en nombre de membres que hi col·laboren. Pensem en el fet que això complica el poder trobar errors en el codi. Una possible "solució" a aquest problema dins aquest enfoc informal passa per utilitzar **metaclasses**. D'aquesta forma el mètode **issubclass()** ens serà més útil per confirmar si la subclasse està implementant correctament la interfície. Definirem una metaclasse anomenada VehicleMeta com segueix: ``` class VehicleMeta(type): def __instancecheck__(cls, instance): return cls.__subclasscheck__(type(instance)) def __subclasscheck__(cls, subclass): return ( hasattr(subclass, 'arrencar') and callable(subclass.arrencar) and hasattr(subclass, 'aturarse') and callable(subclass.aturarse) and hasattr(subclass, 'accelerar') and callable(subclass.accelerar) and hasattr(subclass, 'frenar') and callable(subclass.frenar) ) ``` I ara tot seguit VehicleInterface veurem que és una classe "buida", la qual es construeix a partir de la metaclasse: ``` class VehicleInterface(metaclass=VehicleMeta): pass ``` Pel que fa a les subclasses que implementen la interfície, es construirien del següent mode: ``` class Cotxe: def arrencar(self) -> str: return "Arrenco en primera" def aturarse(self) -> str: return "Aturo en punt mort i amb el fre de mà posat" def accelerar(self, speed: int) -> str: return "Accelero fins a " + str(speed) + " Km/h" def frenar(self, speed: int) -> str: return "Redueixo la velocitat en " + str(speed) + " Km/h" ``` ``` class Patinet: def arrencar(self) -> str: return "Arrenco amb el manillar" def aturarse(self) -> str: return "Aturo amb el manillar i posant un peu a terra" def accelerar(self, speed: int) -> str: return "Accelero fins a " + str(speed) + " Km/h" def frenarEnSemafor(self, speed: int) -> str: return "Com a patinet no m'aturo al semafor i no reduiré la velocitat en " + str(speed) + " Km/h" ``` Respecte a l'aproximació sense metaclasses, veiem que el comportament dels mètodes no canvia quan instanciem els objectes i els executem: ``` Arrenco en primera Accelero fins a 120 Km/h Redueixo la velocitat en 120 Km/h Aturo en punt mort i amb el fre de mà posat Arrenco amb el manillar Accelero fins a 20 Km/h Com a patinet no m'aturo al semafor i no reduiré la velocitat en 20 Km/h Aturo amb el manillar i posant un peu a terra ``` La novetat és que d'aquesta forma, quan fem el **issubclass(Patinet,VehicleInterface)**, la resposta del mètode canvia i és **false** en comptes de true. Això és perquè al definir la metaclasse hem especificat els mètodes que esperem quan s'implementi la interfície (mireu el hasattr i el callable). Si aquests no hi són "fil per randa", el mètode retornarà false i ens identificarà l'error d'implementació. Ens podriem preguntar ara, com és que la classe Cotxe ha implementat els mètodes de la interfície si de forma explícita no hem establert una relació entre ambdues classes? La resposta està en la idea de classe virtual de base (***virtual base class***). En aquest cas, VehicleInterface és una classe virtual base de Cotxe. Internament s'estableix aquesta relació entre classe gràcies al mètode **subclasscheck** present a la metaclasse. ![image](https://hackmd.io/_uploads/ByARTWH56.png) #### **Interfícies a python: Aproximació formal** A diferència de l'aproximació informal, aquesta aproximació requereix utilitzar una biblioteca (abc), la qual ens ajudarà a controlar la implementació en les subclasses. Formes d'implementar aquesta aproximació hi ha unes quantes (podeu veure-les al web de realpython), ara bé, ens quedarem amb una en concret: l'Abstract Method Declaration (si és vol, declaració abstracte de mètode). Per fer-ho, emprarem la biblioteca abc (Abstract Base Classes), la qual ens permet definir mitjançant decoradors mètodes abstractes (@abc.abstractmethod). Així, la interfície Vehicle quedaria com segueix: ``` import abc class IVehicle(metaclass=abc.ABCMeta): @abc.abstractmethod def arrencar(self) -> str: pass @abc.abstractmethod def aturarse(self) -> str: pass @abc.abstractmethod def accelerar(self, speed: int) -> str: pass @abc.abstractmethod def frenar(self, speed: int) -> str: pass ``` Ara crearé la classe Cotxe per implementar la interfície: ``` import IVehicle class Cotxe(IVehicle.IVehicle): def arrencar(self) -> str: return "Arrenco en primera" def aturarse(self) -> str: return "Aturo en punt mort i amb el fre de mà posat" def accelerar(self, speed: int) -> str: return "Accelero fins a " + str(speed) + " Km/h" def frenar(self, speed: int) -> str: return "Redueixo la velocitat en " + str(speed) + " Km/h" ``` Per comprovar el seu funcionament, segregarem el main en un nou arxiu .py i importarem Cotxe: ``` from Cotxe import Cotxe def main(): bmw = Cotxe() print(bmw.arrencar()) print(bmw.accelerar(120)) print(bmw.frenar(120)) print(bmw.aturarse()) if __name__ == "__main__": main() ``` Si fem l'execució d'aquest tros, veurem que el resultat és: ``` Arrenco en primera Accelero fins a 120 Km/h Redueixo la velocitat en 120 Km/h Aturo en punt mort i amb el fre de mà posat ``` L'avantatge és que si ara fiquem la classe Patinet amb la seva peculiar implementació, veurem que el issubclass() ens donarà false sense haver de fer cap implementació. Ho podeu fer com exercici. Una forma de forçar error d'execució quan una subclasse no implementa tots els mètodes de la interfície seria la següent. Modificarem IVehicle d'aquesta forma, afegint un subclasshook() i explicitament aixecant errors (raise NotImplementedError) a cada mètode quan no siguin implementats ``` import abc class IVehicle(metaclass=abc.ABCMeta): @classmethod def __subclasshook__(cls, subclass): return (hasattr(subclass, 'arrencar') and callable(subclass.arrencar) and hasattr(subclass, 'aturarse') and callable(subclass.aturarse) and hasattr(subclass, 'accelerar') and callable(subclass.accelerar) and hasattr(subclass, 'frenar') and callable(subclass.frenar) or NotImplemented) @abc.abstractmethod def arrencar(self) -> str: raise NotImplementedError @abc.abstractmethod def aturarse(self) -> str: raise NotImplementedError @abc.abstractmethod def accelerar(self, speed: int) -> str: raise NotImplementedError @abc.abstractmethod def frenar(self, speed: int) -> str: raise NotImplementedError ``` Ara construïm patinet tal i com ja hem fet abans, sense el mètode Frenar: ``` import IVehicle class Patinet(IVehicle.IVehicle): def arrencar(self) -> str: return "Arrenco amb el manillar" def aturarse(self) -> str: return "Aturo amb el manillar i posant un peu a terra" def accelerar(self, speed: int) -> str: return "Accelero fins a " + str(speed) + " Km/h" def frenarEnSemafor(self, speed: int) -> str: return "Com a patinet no m'aturo al semafor i no reduiré la velocitat en " + str(speed) + " Km/h" ``` Quan instanciem un objecte Patinet i executem, veurem que ens dóna un error: ``` from Patinet import Patinet def main(): bongo = Patinet() print(bongo.arrencar()) print(bongo.accelerar(20)) print(bongo.frenarEnSemafor(20)) print(bongo.aturarse()) if __name__ == "__main__": main() ``` ``` Traceback (most recent call last): File "c:\Interfaces\Main.py", line 18, in <module> main() File "c:\Interfaces\Main.py", line 11, in main bongo = Patinet() ^^^^^^^^^ TypeError: Can't instantiate abstract class Patinet with abstract method frenar ``` --- #### *Bibliografia* W. Murphy. **Implementing an Interface in Python** ([https://realpython.com/python-interface/](https://)) X. Quesada. **M06 Accés a dades. Interfaces amb Python 3** ([https://hackmd.io/@JdaXaviQ/S1eI0tEDs#M06-Acc%C3%A9s-a-dades-Interfaces-amb-Python-3](https://))