Python Protocol Oriented Programming
Bruno Rocha
Posted on November 5, 2021
Python Objects 🐍
In Python everything is an object
.
An object is formed by:
- A memory location identified internally by an
id
. - A type (class) that determines the protocols of the object.
- A value (or a set of values) that the object holds.
Example:
# Given an object identified by the variable name `a`
>>> a = 1
# We can access its memory location `id`
>>> id(a)
1407854654624
# We can access its type
>>> type(a)
'<class 'int'>'
# We can access its value
>>> a
1
# We can use the behavior defined on its protocol
>>> a + 1
2
>>> a.hex()
'0x1'
The above will be similar for every object in Python.
Types 🔠
Every object is based on a type, a type is a class
definition.
-
print
<- isfunction
type -
"Hello"
<- isstr
type -
0
<- isint
type -
[0:5]
<- is aslice
type -
str.upper
<- is amethod_descriptor
type
Function String Integer Slice
__⬆️__ __⬆️_ _⬆️_ __⬆️__
print("Hello"[0] + "World!"[0:5].upper())
___⬆️___ __⬆️___
Symbol Method Descriptor
Protocols 📝
Python is a POP Language (Protocol Oriented Programming)
The type of the object determines its implementation, which exposes the behavior, the behavior are the things that the object can do or things that can be done with the object.
There are languages that calls it Traits of an object.
Each set of those abilities is what we call a Protocol, protocols are useful for setting contracts 🤝 between objects.
Identifying protocols on the Hello World program:
Callable Subscriptable Sliceable
__⬆️__ ______⬆️ __________⬆️
print("Hello"[0] + "World!"[0:5].upper())
________⬆️________ __⬆️__
Summable Callable
Callable 🗣️ Can be invoked using ()
A type is also callable when its protocol includes the__call__
method.Subscriptable ✍🏻 its elements can be accessed through a subscription.
The subscription can be numeric ordinal[0]
or named key['name']
. A type is Subscriptable when its protocol includes the__getitem__
method.Sliceable 🔪 its collection of elements can be sliced in parts.
A type is Sliceable when it is Subscriptable and its__getitem__
method can accept aslice
in place of theindex
orname
. A slice is the composition of[start:stop:step]
elements.Summable ➕ Can be combined with other objects via
+
operation.
The product of this combination is always new object. On numeric types this is the ability tosum
two or more numbers in a set. On sequences it is theconcatenation
of its fragments in to one. A type is Summable when its protocol includes the__add__
or__radd__
method.Printable 🖨️ Can be printed using
print
All the Python objects are printable,print
will look either for a__repr__
or a__str__
method for printing the object.
ℹ️ There are many more and in fact, you can define custom protocols, Protocols are very generic and there is no official list of protocols although there are some pre-defined protocols in the typing module.
from typing import Iterator, Iterable, Optional, Sequence, Awaitable, Generic
Complete list of protocols and sub typing is available on https://mypy.readthedocs.io/en/stable/protocols.html
🦆 Protocols empowers an approach called Duck Typing which is the fact that in Python if an object looks like, behaves like, and has the behavior of a Duck, it is said to be a Duck, regardless if this is the case of a Dog that learned to say quack immitating a Duck 🐶.
Typing and Protocol Checking
Some protocols can be checked using built-in functions
callable(print) is True
callable("Hello") is False
Some protocols must be checked against its type class
isinstance("Hello", str) is True
isinstance(0, slice) is False
There are cases where the only way to verify protocols
is checking for its attributes.
hasattr("Hello", "__add__") is True # Summable, we can use `+` operator.
Others where we need to use the EAFP pattern.
try:
"Hello" + 1
except TypeError: # Strong type checking
# we cannot `__add__` an `str` to an `int`
Typing Protocols
Python3 offers a way to define custom protocols
from typing import Protocol, runtime_checkable
@runtime_checkable
class CallableSummableSubscriptable(Protocol):
def __call__(self) -> T:
...
def __add__(self, other: T) -> T:
...
def __getitem__(self, item: T) -> T:
...
ℹ️ Protocol methods are just signatures with empty bodies, stated by the
...
.T
is usually a type alias indicating a generic type.
Protocols are useful to define contracts, bounds on function signatures for example defining a function that accepts an argument only if the type has the specified protocol.
def awesome_function(thing: CallableSummableSubscriptable):
# accepts only objects that implements that ⬆️ protocol.
# Protocols can be checked at runtime @runtime_checkable
# Or checked using static analysers e.g: mypy
Is there the Traditional OOP in Python?
OOP Python is actually POP (Protocol Oriented Programming)
- More about Protocols and Behavior.
- Less about tradicional OOP concepts.
All the traditional concepts and patterns are also available, but some are intrinsic to objects and protocols that in the end of the day the programmers doesn't have to take care of it at all in the same way.
Inheritance
The ability to inherit from another base type and take all its behavior and the ability to override with custom implementation.
class MyString(str)
def __str__(self):
return super().__str__().upper()
>>> x = MyString("Bruno")
>>> print(x)
"BRUNO"
Encapsulation
The ability to hide object attributes and methods and expose only a selected set of them or to expose them in a more controlled way.
# Descriptor is a protocol for getter/setter like approach.
class Field:
def __get__(...):
def __set__(...):
class Thing:
# Double underline means that the field is private
# but actually it is only a naming mangling convention.
__protected_attr = Field()
# Properties can also be used to define getter/setter
@property
def foo(self):
return self.__protected_attr
@foo.setter
def set_foo(self, value):
self.__protected_attr = value
Polymorfism
The ability for objects to behave differently regardless of its base type and for procedures to take different types of objects as arguments.
len("Bruno")
len([1, 2, 3])
dict.get("key")
dict.get("key", default="other")
print("Hello")
print(123)
print(*["Hello", "World"])
def function(*args, **kwargs):
...
ℹ️ In traditional OOP literature polymorphism is often used to define only the ability to have methods reusing the same name but different implementation, but in fact it goes deeper than this.
Conclusion
🧑🍳 A Chef when selecting ingredients for a recipe, will look for the protocols defined on each ingredient, the Chef can go to the supermarket and see a set of different types of onions 🧅, even if the recipe only says onion the Chef knows based on the desired behavior that wants a specific type of onion, white onions are swetter, better for cooked recipes, the purple onions are more acid so better for salads and raw recipes. (but that also depends a bit on taste)
🧑💻 The Software Engineer when choosing the data structures to use in an algorithm 🤖 must look to the protocols defined and how the objects behaves even if the requirements says it is a collection of texts for example, the engineer must analyse the program to choose wisely between a tuple, a list or even a set.
The protocols are often more important than the types.
NOTE: Credits to @mathsppblog to have inspired the first paragraph of this post https://twitter.com/mathsppblog/status/1445148609977126914
Posted on November 5, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.