Object Orientation

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
def a():
    pass
a
<function __main__.a()>
callable(a)
True
a()
type(_)
bool
class A(object):
    pass
a = A() # `a` is an instance of the class `A`
type(a)
__main__.A
public class A {

    private String name;

    public A(String name){
        this.name = name;
    }

    public String a(String a){}
    public String a(int a){}

}
class B(object):

    def __init__(self, v):
        print('The ctor of the class B has been invoked.')
        self.slot = v

    def yourself(self):
        return self
B
__main__.B
dir(B)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'yourself']
b = B(v=4) # I'm creating an instance of the class B, referencing it with `b`.
The ctor of the class B has been invoked.
b.slot
4
b.yourself()
<__main__.B at 0x7ffecfa820a0>
assert b == b.yourself()
callable(B)
True
class Node(object):

    def __init__(self, left, value, right):
        # the followings are all slots for each instance of this class.
        self.left = left
        self.value = value
        self.right = right

    def __repr__(self) -> str:
        return 'Node({0}, {1}, {2})'.format(repr(self.left), repr(self.value), repr(self.right))

    def __str__(self) -> str:
        return '({}) <- {} -> ({})'.format(self.left, self.value, self.right)

    def visit_inorder(self, f):
        assert callable(f) # `f` is a Callable object
        self.left.visit_inorder(f)
        f(self.value) #!
        self.right.visit_inorder(f)

class EmptyTree:

    def visit_inorder(self, f):
        'There is no value that should be passed to `f`'
        pass

    def __str__(self):
        return '•'

    def __repr__(self):
        return 'EmptyTree()'

empty_tree = EmptyTree() # we will use the lone instance of an empty tree.
n1 = Node(left=empty_tree, value=3, right=empty_tree)
n1
Node(EmptyTree(), 3, EmptyTree())
callable(print)
True
n1.visit_inorder(f=print)
3
type(n1)
__main__.Node
Node(None, 3, None)
Node(None, 3, None)
repr(n1)
'Node(EmptyTree(), 3, EmptyTree())'
str(n1)
'(•) <- 3 -> (•)'
n2 = Node(left=n1, value=2, right=n1)
n2
Node(Node(EmptyTree(), 3, EmptyTree()), 2, Node(EmptyTree(), 3, EmptyTree()))
print(str(n2))
((•) <- 3 -> (•)) <- 2 -> ((•) <- 3 -> (•))
str(Node(left=[], value=object(), right=4))
'([]) <- <object object at 0x7ffecf65db30> -> (4)'
n2.visit_inorder(f=print)
3
2
3
n3 = Node(left=n2, value=0, right=n1)
s = []
n3.visit_inorder(f=lambda v: s.append(v))
s
[3, 2, 3, 0, 3]
def p(a):
    print(a)
    a + '4'
def p_equiv(a):
    print(a)
    a + '4'
    return None
a = p('hello world')
hello world
a = p_equiv('hello world')
hello world
type(a)
NoneType
b = lambda: print('hello world')
a = b()
hello world
type(a)
NoneType

Back to the past…

class Node(object):

    def __init__(self, left, value, right):
        # the followings are all slots for each instance of this class.
        self.left = left
        self.value = value
        self.right = right

    def __repr__(self) -> str:
        return 'Node({0}, {1}, {2})'.format(repr(self.left), repr(self.value), repr(self.right))

    def __str__(self) -> str:
        return '({}) <- {} -> ({})'.format(self.left, self.value, self.right)

    def visit_inorder(self, f):
        assert callable(f) # `f` is a Callable object
        if self.left: self.left.visit_inorder(f)
        f(self.value) #!
        if self.right: self.right.visit_inorder(f)
n1 = Node(left=None, value=3, right=None)
n1.visit_inorder(f=print)
3
n2 = Node(left=n1, value=2, right=n1)
n2
Node(Node(None, 3, None), 2, Node(None, 3, None))
n2.visit_inorder(f=print)
3
2
3

Back to the future…

True, False
(True, False)
type(True)
bool
type(False)
bool
class Node(object):

    def __init__(self, left, value, right):
        # the followings are all slots for each instance of this class.
        self.left = left
        self.value = value
        self.right = right

    def __repr__(self) -> str:
        return 'Node({0}, {1}, {2})'.format(repr(self.left), repr(self.value), repr(self.right))

    def __str__(self) -> str:
        return '({}) <- {} -> ({})'.format(self.left, self.value, self.right)

    def visit_inorder(self, f):
        assert callable(f) # `f` is a Callable object

        if self.left: self.left.visit_inorder(f)

        f(self.value) #!

        if self.right: self.right.visit_inorder(f)

    def __bool__(self):
        print('Print from Node.__bool__')
        return True


class EmptyTree:

    def __str__(self):
        return '•'

    def __repr__(self):
        return 'EmptyTree()'

    def __bool__(self):
        print('Print from EmptyTree.__bool__')
        return False

empty_tree = EmptyTree() # we will use the lone instance of an empty tree.
n2 = Node(left=n1, value=2, right=n1)
n2.visit_inorder(f=print)
Print from EmptyTree.__bool__
3
Print from EmptyTree.__bool__
2
Print from EmptyTree.__bool__
3
Print from EmptyTree.__bool__

class F(object):

    def __init__(self):
        self.accumulator = []

    def __call__(self, arg):
        self.accumulator.append(arg)

    def __iter__(self):
        return iter(self.accumulator)

    def __next__(self):
        yield from self.accumulator
f = F()
f.__call__(4)
f.accumulator
[4]
[f(i) for i in range(0, 100, 2)]
[None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None]
f.accumulator
[0,
 2,
 4,
 6,
 8,
 10,
 12,
 14,
 16,
 18,
 20,
 22,
 24,
 26,
 28,
 30,
 32,
 34,
 36,
 38,
 40,
 42,
 44,
 46,
 48,
 50,
 52,
 54,
 56,
 58,
 60,
 62,
 64,
 66,
 68,
 70,
 72,
 74,
 76,
 78,
 80,
 82,
 84,
 86,
 88,
 90,
 92,
 94,
 96,
 98]
g = F()
n1 = Node(left=empty_tree, value=3, right=empty_tree)
n2 = Node(left=n1, value=2, right=n1)
n2.visit_inorder(f=g)
g.accumulator
[3, 2, 3]
for i in g:
    print(i+1)
4
3
4
next(g)
<generator object F.__next__ at 0x7ffed0cf19e0>
next(_)
3