layout: true class: inverse --- # .ps-title[Pythonic Interfaces] ### The secret to building maintainable, quality code! .invisible[-] .invisible[-] .invisible[-] .invisible[-] ## .ps-yellow[Praveen G Shirali] ### .ps-blue[PyCon India 2020] | .ps-orange[03rd October 2020] ### https://pshirali.github.io/pythonic-interfaces --- layout: false class: inverse # In this talk, we'll cover ### [1] .ps-blue[What is an interface?] ### [2] .ps-blue[Why is it important?] ### [3] .ps-blue[How to enforce interfaces?] ### [4] .ps-blue[Python3, 3rd-party libraries] ### [5] .ps-blue[How do I decide what to use?] --- layout: false class: inverse #.ps-title[[1/5] .ps-blue[What is an interface?]] --- layout: false class: inverse # An interface is defined as ... .invisible[-] ## a .ps-red[.underline[shared boundary]] across which ## .ps-yellow[.underline[two or more components]] .ps-green[.underline[share information]] .invisible[-] .invisible[-] .invisible[-] .invisible[-] #### Reference [1] --- layout: false class: inverse # A .ps-yellow[DataProcessor] class in an application ... .center[] --- layout: false class: inverse # ... relies on another class to provide data .center[] --- layout: false class: inverse # A .ps-yellow[FileIO] class reads & writes data to a file .center[] --- layout: false class: inverse # The .ps-yellow[DataProcessor] calls the .ps-green[read] method .center[] --- layout: false class: inverse # More IO classes need to be supported !! .center[] --- layout: false class: inverse # .ps-yellow[MemIO] is similar to .ps-yellow[FileIO], but in-memory .center[] --- layout: false class: inverse # .ps-yellow[DataProcessor] has clear requirements ... .center[] --- layout: false class: inverse # Define a new interface class: .ps-yellow[ReadWriter] .center[] --- layout: false class: inverse # .ps-yellow[FileIO] satisfies .ps-yellow[ReadWriter] interface .center[] --- layout: false class: inverse # .ps-yellow[MemIO] satisfies .ps-yellow[ReadWriter] interface .center[] --- layout: false class: inverse # Could any new .ps-yellow[IO] satisfy .ps-yellow[ReadWriter]? .center[] --- layout: false class: inverse # A world without interface definition ... .center[] --- layout: false class: inverse # Would you leave it to chance ? .center[] --- layout: false class: inverse # Could .ps-yellow[ReadWriter] be enforced on all .ps-yellow[IOs]? .center[] --- layout: false class: inverse # ... so that it errors on deviation? [1/5] .center[] --- layout: false class: inverse # ... so that it errors on deviation? [2/5] .center[] --- layout: false class: inverse # ... so that it errors on deviation? [3/5] .center[] --- layout: false class: inverse # ... so that it errors on deviation? [4/5] .center[] --- layout: false class: inverse # ... so that it errors on deviation? [5/5] .center[] --- layout: false class: inverse # .ps-yellow[What does interface enforcement mean?] .invisible[-] ## Use code to verify that a .ps-blue[class adheres to an interface] ## If it doesn't, then the .ps-red[application raises errors] .invisible[-] ## Interface adherance is a .ps-green[requirement] for the application ## to either .ps-purple[start], or .ps-orange[run correctly]. --- layout: false class: inverse # .ps-yellow[What do we want to enforce?] ### [1] .ps-green[presence of required methods, correct names] ### [2] .ps-green[method signatures (arguments, order, type annotations etc)] ### [3] .ps-green[Descriptors/decorators (@staticmethod, @property etc)] ### [4] .ps-green[Special types: generator, coroutine functions (async) etc] ### [5] .ps-green[..may be more? (anything that improves quality)] --- layout: false class: inverse # In summary, the expectation is ... --- layout: false class: inverse .left-half[ # Interface ```python class DosaInterface: @classmethod def make(cls, masala=True):, pass @property def is_crispy(self) -> bool: pass def dip_in_chutney(self): pass def set_rating(self, r: int): pass async def repeat_order(self): pass ``` ] --- layout: false class: inverse .left-half[ # Interface ```python class DosaInterface: @classmethod def make(cls, masala=True):, pass @property def is_crispy(self) -> bool: pass def dip_in_chutney(self): pass def set_rating(self, r: int): pass async def repeat_order(self): pass ``` ] .right-half[ # Implementation 1 ```python class TiffinRoomDosa: @classmethod def make(cls, masala=True):, # implementation @property def is_crispy(self) -> bool: # implementation def dip_in_chutney(self): # implementation def set_rating(self, r: int): # implementation async def repeat_order(self): # implementation ``` ] --- layout: false class: inverse .left-half[ # Interface ```python class DosaInterface: @classmethod def make(cls, masala=True):, pass @property def is_crispy(self) -> bool: pass def dip_in_chutney(self): pass def set_rating(self, r: int): pass async def repeat_order(self): pass ``` ] .right-half[ # Implementation 2 ```python class StreetSideDosa: @classmethod def make(cls, masala=True):, # implementation @property def is_crispy(self) -> bool: # implementation def dip_in_chutney(self): # implementation def set_rating(self, r: int): # implementation async def repeat_order(self): # implementation ``` ] --- layout: false class: inverse .left-half[ # Interface ```python class DosaInterface: @classmethod def make(cls, masala=True):, pass @property def is_crispy(self) -> bool: pass def dip_in_chutney(self): pass def set_rating(self, r: int): pass async def repeat_order(self): pass ``` ] .right-half[ # Implementation 3 ```python class HighwayEateryDosa: @classmethod def make(cls, masala=True):, # implementation @property def is_crispy(self) -> bool: # implementation def dip_in_chutney(self): # implementation def set_rating(self, r: int): # implementation async def repeat_order(self): # implementation ``` ] --- layout: false class: inverse #.ps-title[[2/5] .ps-blue[Why is it important?]] --- layout: false class: inverse, middle # Example: An application's evolution ... .center[] --- layout: false class: inverse, middle # Components (/classes) interact ... .center[] --- layout: false class: inverse, middle # More get added, some get removed ... .center[] --- layout: false class: inverse, middle # Some components improve ... .center[] --- layout: false class: inverse, middle # The application gains maturity ... .center[] --- layout: false class: inverse, middle # Risk: some changes may break the app ... .center[] --- layout: false class: inverse, middle # Ideal Scenario: plug & play w/o breaking .center[] --- layout: false class: inverse # .ps-yellow[Benefits?] .invisible[-] ## With interfaces, applications have .ps-green[clear boundaries] ## They are .ps-blue[easier to maintain], and .ps-orange[less error prone] .invisible[-] ## With interface enforcement, some .ps-purple[errors can be prevented] ## as they are .ps-red[enforced] by the application, and .ps-yellow[detected early] --- layout: false class: inverse #.ps-title[[3/5] .ps-blue[Interface Enforcement]] --- layout: false class: inverse # Abstract Base Class ```python # ABCMeta is the enforcement mechanism # available as a **metaclass** # from abc import ABCMeta # decorate interface methods with `abstractmethod` decorator # from abc import abstractmethod ``` --- layout: false class: inverse # Abstract Base Class - Interface definition ```python from abc import ABCMeta, abstractmethod # ReadWriter is the interface definition # class ReadWriter(metaclass=ABCMeta): # <-- Class can have any name @abstractmethod def read(self, size: int = -1) -> str: pass @abstractmethod def write(self, data: str) -> int: pass ``` --- layout: false class: inverse # Abstract Base Class - Usage ```python # Concrete implementations derive from abstract base classes # class MemIO(ReadWriter): def read(self, size: int = -1) -> str: # actual implementation def write(self, data: str) -> int: # actual implementation if __name __ == "__main__": m = MemIO() # <--- Compliance is checked on instantiation ``` --- layout: false class: inverse # Abstract Base Class - Enforcement ```python class MemIO(ReadWriter): # missing `write` method implementation def read(self, size: int = -1) -> str: # actual implementation if __name __ == "__main__": m = MemIO() # <--- Compliance is checked on instantiation ``` ``` # ---- [output] ---- Traceback (most recent call last): File "{filename}", line {number}, in
m = MemIO() TypeError: Can't instantiate abstract class MemIO with abstract methods write ``` --- layout: false class: inverse # Abstract Base Class - Limitations ```python # ABCMeta does not enforce method signatures # class MemIO(ReadWriter): def read(self): # <--- incorrect signature # actual implementation def write(self): # <--- incorrect signature # actual implementation if __name __ == "__main__": m = MemIO() # <--- Does not raise errors ``` --- layout: false class: inverse # Abstract base classes can: ### [1] .ps-green[enforce presence of methods only by name] ### [2] .ps-green[enforce automatically on derived classes (via metaclass+inheritance)] # But! ### [1] .ps-blue[_abc_ is old. Python3 has evolved.] ### [2] .ps-blue[_abc_ design goals were/are different (ref(2): talk by Raymond Hettinger)] --- layout: false class: inverse #.ps-title[[4/5] .ps-blue[Python3 & libraries]] --- layout: false class: inverse # Python3 has: ### .ps-green[type annotation support, _async_] ### .ps-green[better typing, inspect library] .invisible[-] .invisible[-] .invisible[-] ### .ps-blue[Rich API definition capabilities] ### .ps-yellow[Methods can be defined in so many ways! .. Lets explore a few ...] --- layout: false class: inverse ## I pity the _foo_ ... ```python def foo(self): # name is the identifier def foo(self, a: int, b: float): # signature matters @property def foo(self): # not callable @foo.setter def foo(self, bar: bool): # assign. Don't call @staticmethod def foo(): # no 'self' @classmethod def foo(cls): # class as 1st argument def foo(self): yield # generator function async def foo(self): # coroutine function ``` --- layout: false class: inverse # What can we do with .ps-yellow[_inspect_] ? --- layout: false class: inverse # inspect.signature -- Usage ```python import inspect class MyIO: def read(self, size: int = -1) -> bytes: pass sig = inspect.signature(MyIO.read) # <------ `inspect.Signature` object ``` --- layout: false class: inverse # inspect.signature -- Comparison ```python def A(a:bytes, b:str, x: int = 10, y:int = 20) -> bool: pass def B(a:bytes, b:str, x: int = 10, y:int = 20) -> bool: pass >>> import inspect >>> inspect.signature(A) == inspect.signature(B) True ``` --- layout: false class: inverse # @property, data descriptors ... ```python class TestDD: # Data Descriptor Example Class @property # getter def my_prop(self): # same name pass @my_prop.setter def my_prop(self, x: int): # same name pass @my_prop.deleter def my_prop(self): # same name pass >>> inspect.signature(TestDD.my_prop) TypeError:
is not a callable object ``` --- layout: false class: inverse # @property, data descriptors ... ```python >>> import inspect >>> inspect.isdatadescriptor(TestDD.my_prop) True >>> prop_object = getattr(TestDD, "my_prop") ----------------------------------------------------------------------------- >>> getattr(prop_object, "fget") # getter >>> getattr(prop_object, "fset") # setter >>> getattr(prop_object, "fdel") # deleter ``` --- layout: false class: inverse # generator, coroutine functions ... ```python class GenCoro: def foo(self): yield async def bar(self): pass ----------------------------------------------------------------------------- >>> inspect.isgeneratorfunction(GenCoro.foo) True >>> inspect.iscoroutinefunction(GenCoro.foo) True ``` --- layout: false class: inverse # classmethod, staticmethod ... ```python # https://github.com/ksindi/implements/blob/master/implements.py # ::getobj_via_dict def getobj_via_dict(cls, name): for c in cls.__mro__: if name in c.__dict__: return c.__dict__[name] return None ----------------------------------------------------------------------------- >>> my_cls = getobj_via_dict(CSM, "my_cls") >>> isinstance(my_cls, (classmethod, types.ClassMethodDescriptorType)) True # just classmethod for PY < 3.7 >>> my_stat = getobj_via_dict(CSM, "my_stat") >>> isinstance(my_stat, (staticmethod, types.BuiltinMethodType)) True ``` --- layout: false class: inverse ## .ps-yellow[Revisit:] I pitied the _foo_ ... ```python ✓ def foo(self): # name is the identifier ✓ def foo(self, a: int, b: float): # signature matters @property ✓ def foo(self): # not callable @foo.setter ✓ def foo(self, bar: bool): # assign. Don't call @staticmethod ✓ def foo(): # no 'self' @classmethod ✓ def foo(cls): # class as 1st argument ✓ def foo(self): yield # generator function ✓ async def foo(self): # coroutine function ``` --- layout: false class: inverse # Some 3rd party libraries ... ### implements - simple, supports py3.6+ only, composition over inheritance - https://pypi.org/project/implements/ ### python-interfaces - supports py2 & py3, uses metaclasses - https://pypi.org/project/python-interface/ ### zope.interface - version 5 now, lots of features - https://pypi.org/project/zope.interface/ --- layout: false class: inverse #.ps-title[[5/5] .ps-blue[How to choose?]] --- layout: false class: inverse # Composition vs Inheritance .left-half[ Example: `implements` 1. clean wrapper 2. explicit ```python @implements(IFace) class MyClass: ... ``` ] .right-half[ Example: `python-interface`, `abc` 1. inheritance + metaclass driven 2. implicit ```python # abc: Interface inherits from abc.ABCMeta class MyClass(metaclass=IFace): ... # python-interface class MyClass(implements(IFace)): ... ``` ] --- layout: false class: inverse # Early vs Late enforcement .left-half[ ### Early (better) Example: `implements`, `python-interface` 1. Enforced on import/class-creation 2. Discover errors soon ```python @implements(IFace) # << checked! class MyClass: ... ``` ] .right-half[ ### Late Example: `abc` 1. Enforced on instantiation. 2. Code path dependent ```python # abc: Interface inherits from abc.ABCMeta class MyClass(metaclass=IFace): ... m = MyClass() # << checked here ``` ] --- layout: false class: inverse # Recap ### [1] .ps-green[Defining interfaces is good. Enforcing is even better!] ### [2] .ps-green[Py3: 3rd party libraries have better support than _abc_] ### [3] .ps-green[_inspect_ is powerful! Do more with it] ### [4] .ps-green[With enforcement, lean on finding errors early] --- layout: false class: inverse # References #### [1] Wikipedia: Interface: https://en.wikipedia.org/wiki/Interface_(computing) #### [2] R. Hettinger @ PyCon Russia 2019: https://www.youtube.com/watch?v=S_ipdVNSFlo # Further Reading #### [1] https://realpython.com/python-interface/ #### [2] https://realpython.com/inheritance-composition-python/ #### [3] https://www.youtube.com/watch?v=F4wUrj6pmSI -- Understanding Go Interfaces (by Francesc Campoy). Great talk to understanding interfaces conceptually (though it is explained in the context of Golang). --- layout: false class: inverse # Open for Q&A, Discussion & Feedback Let me know if you've faced hurdles with interfaces. How have you solved them? Has this talk been helpful? How can we improve this further? ## Praveen G Shirali - Email: .ps-blue[praveengshirali@gmail.com] - `pshirali` on BangPypers Slack - https://linkedin.com/in/praveenshirali - https://github.com/pshirali - I'm .underline[not] on Twitter ## Link to this talk - https://pshirali.github.io/pythonic-interfaces