29
loading...
This website collects cookies to deliver better user experience
dry-python
library called classes
.greet
different types differently (yes, "hello world" examples, here we go).greet
:str
instances as Hello, {string_content}!
MyUser
instances as Hello again, {username}
greet
as a simple example does not really make much "business" sense, but more complicated things like to_json
, from_json
, to_sql
, from_sql
, and to_binary
do make a lot of sense and can be found in almost any project.greet
example.isinstance()
checks inside the function itself.@dataclass
class MyUser(object):
name: str
def greet(instance: str | MyUser) -> str:
if isinstance(instance, str):
return 'Hello, "{0}"!'.format(instance)
elif isinstance(instance, MyUser):
return 'Hello again, {0}'.format(instance.name)
raise NotImplementedError(
'Cannot greet "{0}" type'.format(type(instance)),
)
isinstance
won't be enough, because we need extendability. We need to support other types, which are unknown in advance.greet
their custom types.len()
and __len__
, it solves exactly the same problem.@dataclass
class MyUser(object):
name: str
def greet(self) -> str:
return 'Hello again, {0}'.format(self.name)
typing.Protocol
:from typing_extensions import Protocol
class CanGreet(Protocol):
def greet(self) -> str:
"""
It will match any object that has the ``greet`` method.
Mypy will also check that ``greet`` must return ``str``.
"""
def greet(instance: CanGreet) -> str:
return instance.greet()
print(greet(MyUser(name='example')))
# Hello again, example
class Person(object):
def become_friends(self, friend: 'Person') -> None:
...
def is_friend_of(self, person: 'Person') -> bool:
...
def get_pets(self) -> Sequence['Pet']:
...
Person
(pun intended) deserve to know that some to_json
conversion exists that can turn this poor Person
into textual data? What about binary pickling?greet
method to the str
type?str
subtype with greet
method in it:class MyStr(str):
def greet(self) -> str:
return 'Hello, {0}!'.format(self)
print(greet(MyStr('world')))
# Hello, world!
print(greet('world'))
# fails with TypeError
JavaScript
(in 2000s and early 2010s, mostly popularized by jQuery
plugins) and Ruby
(still happening right now). Here's how it looks:String.prototype.greet = function (string) {
return `Hello, ${string}!`
}
mypy
does not support it at alldjango-rest-framework
might recommend to add special abstractions to greet
different types:import abc
from typing import Generic, TypeVar
_Wrapped = TypeVar('_Wrapped')
class BaseGreet(Generic[_Wrapped]):
"""Abstract class of all other """
def __init__(self, wrapped: _Wrapped) -> None:
self._wrapped = wrapped
@abc.abstractmethod
def greet(self) -> str:
raise NotImplementedError
class StrGreet(BaseGreet[str]):
"""Wrapped instance of built-in type ``str``."""
def greet(self) -> str:
return 'Hello, {0}!'.format(self._wrapped)
# Our custom type:
@dataclass
class MyUser(object):
name: str
class MyUserGreet(BaseGreet[MyUser]):
def greet(self) -> str:
return 'Hello again, {0}'.format(self._wrapped.name)
print(greet(MyStrGreet('world')))
# Hello, world!
print(greet(MyUserGreet(MyUser(name='example'))))
# Hello again, example
Dict[type, Type[BaseGreet]]
.<X> is not json-serializable
as many of us might have seen it with drf
's serializers when trying to serialize a custom unregistered type.Rust
, people still argue whether it is functional or not) handle this problem.class
concept as we know it in Python and, of course, there's no subclassingobject
s as we do in Python, they don't mix behavior and structure (however, Elixir
has Alan Kay's real objects)@doc "Our custom protocol"
defprotocol Greet do
# This is an abstract function,
# that will behave differently for each type.
def greet(data)
end
@doc "Enhancing built-in type"
defimpl Greet, for: BitString do
def greet(string), do: "Hello, #{string}!"
end
@doc "Custom data type"
defmodule MyUser do
defstruct [:name]
end
@doc "Enhancing our own type"
defimpl Greet, for: MyUser do
def greet(user), do: "Hello again, #{user.name}"
end
Elixir
even if they are not familiar with this language. That's what I call beauty!# Using our `Greet.greet` function with both our data types:
IO.puts(Greet.greet("world"))
# Hello, world!
IO.puts(Greet.greet(%MyUser{name: "example"}))
# Hello again, example
Elixir
's Protocol
s is that it is not currently possible to express that some type does support our Greet.greet
for Elixir
's type checker.Elixir
, which is 100% dynamically typed.Enumerable
allows to work with collections: counting elements, finding members, reducing, and slicingString.Chars
is something like __str__
in Python, it converts structures to human-readable format// Our custom trait
trait Greet {
fn greet(&self) -> String;
}
// Enhancing built-in type
impl Greet for String {
fn greet(&self) -> String {
return format!("Hello, {}!", &self);
}
}
// Defining our own type
struct MyUser {
name: String,
}
// Enhancing it
impl Greet for MyUser {
fn greet(&self) -> String {
return format!("Hello again, {}", self.name);
}
}
Rust
's static typing, we can express that some function's argument supports the trait we have just defined:// We can express that `greet` function only accepts types
// that implement `Greet` trait:
fn greet(instance: &dyn Greet) -> String {
return instance.greet();
}
pub fn main() {
// Using our `greet` function with both our data types:
println!("{}", greet(&"world".to_string()));
// Hello, world!
println!("{}", greet(&MyUser { name: "example".to_string() }));
// Hello again, example
}
Elixir
.Rust
uses its Trait
s:Trait
s are the core of this language, it is widely used in cases when you need to define any shared behavior.Haskell
has typeclasses to do almost the same thing.Haskell
syntax below, it might be not very pleasant and clear to read, especially for people who are not familiar with this brilliant language, but we have what we have:{-# LANGUAGE FlexibleInstances #-}
-- Our custom typeclass
class Greet instance where
greet :: instance -> String
-- Enhancing built-in type with it
instance Greet String where
greet str = "Hello, " ++ str ++ "!"
-- Defining our own type
data MyUser = MyUser { name :: String }
-- Enhancing it
instance Greet MyUser where
greet user = "Hello again, " ++ (name user)
Rust
and Elixir
:Greet
typeclass that has a single function to implement: greet
String
type, which is a built-in (alias for [Char]
)MyUser
type with name
field of String
typeGreet
typeclass for MyUser
is the last thing we dogreet
function:-- Here you can see that we can use `Greet` typeclass to annotate our types.
-- I have made this alias entirely for this annotation demo,
-- in real life we would just use `greet` directly:
greetAlias :: Greet instance => instance -> String
greetAlias = greet
main = do
print $ greetAlias "world"
-- Hello, world!
print $ greetAlias MyUser { name="example" }
-- Hello again, example
Haskell
relies on its typeclasses the heaviest.Haskell
and traits from Rust
are a bit different, but we won't go into these details to keep this article rather short.singledispatch
.from functools import singledispatch
@singledispatch
def greet(instance) -> str:
"""Default case."""
raise NotImplementedError
@greet.register
def _greet_str(instance: str) -> str:
return 'Hello, {0}!'.format(instance)
# Custom type
@dataclass
class MyUser(object):
name: str
@greet.register
def _greet_myuser(instance: MyUser) -> str:
return 'Hello again, {0}'.format(instance.name)
print(greet('world'))
# Hello, world!
print(greet(MyUser(name='example')))
# Hello again, example
dry-python/classes
?singledispatch
implementation,singledispatch
you cannot be sure that everything will work, because it is not supported by mypy
.greet(1) # mypy is ok with that :(
# runtime will raise `NotImplementedError`
dry-python/classes
we have fixed that.from classes import typeclass
@typeclass
def greet(instance) -> str:
...
@greet.instance(str)
def _greet_str(instance: str) -> str:
return 'Iterable!'
greet(1)
# Argument 1 to "greet" has incompatible type "int"; expected "str"
@singledispatch
signature contract:@greet.register
def _greet_dict(instance: dict, key: str) -> int:
return instance[key] # still no mypy error
dry-python/classes
:@greet.instance(dict)
def _greet_dict(instance: dict, key: str) -> int:
...
# Instance callback is incompatible
# "def (instance: builtins.dict[Any, Any], key: builtins.str) -> builtins.int";
# expected
# "def (instance: builtins.dict[Any, Any]) -> builtins.str"
@singledispatch
also does not allow defining generic functions:@singledispatch
def copy(instance: X) -> X:
"""Default case."""
raise NotImplementedError
@copy.register
def _copy_int(instance: int) -> int:
return instance
# Argument 1 to "register" of "_SingleDispatchCallable"
# has incompatible type "Callable[[int], int]";
# expected "Callable[..., X]"
reveal_type(copy(1))
# Revealed type is "X`-1"
# Should be: `int`
from typing import TypeVar
from classes import typeclass
X = TypeVar('X')
@typeclass
def copy(instance: X) -> X:
...
@copy.instance(int)
def _copy_int(instance: int) -> int:
... # ok
reveal_type(copy(1)) # int
@singledispatch
to work with only subtypes of specific types, even if you want to.@singledispatch
:@greet.register
def _greet_iterable(instance: Iterable) -> str:
return 'Iterable!'
# TypeError: Invalid annotation for 'instance'.
# typing.Iterable is not a class
from typing import Iterable
from classes import typeclass
@typeclass
def greet(instance) -> str:
...
@greet.instance(Iterable, is_protocol=True)
def _greet_str(instance: Iterable) -> str:
return 'Iterable!'
print(greet([1, 2, 3]))
# Iterable!
greet
function. Something like:def greet_and_print(instance: '???') -> None:
print(greet(instance))
@singledispatch
.dry-python/classes
:from classes import AssociatedType, Supports, typeclass
class Greet(AssociatedType):
"""Special type to represent that some instance can `greet`."""
@typeclass(Greet)
def greet(instance) -> str:
"""No implementation needed."""
@greet.instance(str)
def _greet_str(instance: str) -> str:
return 'Hello, {0}!'.format(instance)
def greet_and_print(instance: Supports[Greet]) -> None:
print(greet(instance))
greet_and_print('world') # ok
greet_and_print(1) # type error with mypy, exception in runtime
# Argument 1 to "greet_and_print" has incompatible type "int";
# expected "Supports[Greet]"
isinstance()
conditions - through OOP - to typeclasses.dry-python/classes
can save you from lots of mistakes and help to write more expressive and safe business logic.dry-python
libraries for extra effect!Supports
should take any amount of type arguments: Supports[A, B, C]
. This type will represent a type that supports all three typeclasses A
, B
, and C
at the same time
List[int]
and List[str]
. This might require adding runtime typecheker to dry-python/classes
dry-python
development on GitHub
classes
repo