In [ ]:
class C:
    def __init__(self):
        print('Object creation')
        
I = C()
print()

In [ ]:
class C:
    def __call__(self):
        print('Calling object!')
        
I = C()
I()

In [ ]:
class C:
    def __repr__(self):
        return 'Using __repr__'

I = C()
print(I)
I

In [ ]:
class C:
    def __repr__(self):
        return 'Using __repr__'
        
    def __str__(self):
        return 'Using __str__'

I = C()
print(I)
I

In [ ]:
class C:
    def __init__(self, datum):
        self.datum = datum
    
    def __len__(self):
        return len(self.datum)
    
    
I = C('')
if I:
    print('Datum is not the empty string')
else:
    print('Datum is the empty string')
print('The length of datum is:', len(I))
print()

I = C('X')
if I:
    print('Datum is not the empty string')
else:
    print('Datum is the empty string')
print('The length of datum is:', len(I))

In [ ]:
class C:
    def __init__(self, datum):
        self.datum = datum
        
    def __bool__(self):
        print('Let me evaluate...')
        return bool(self.datum)
    
    def __len__(self):
        return len(self.datum)
    
    
I = C('')
# __bool__() takes over __len__().
if I:
    print('Datum is not the empty string')
else:
    print('Datum is the empty string')
print()

I = C('X')
# __bool__() takes over __len__().
if I:
    print('Datum is not the empty string')
else:
    print('Datum is the empty string')

In [ ]:
class C:
    def __index__(self):
        return 16
        
    def __getitem__(self, index):
        if isinstance(index, int):
            print('Index:', index)
        else:
            print('Slice:', index, '--', index.start, index.stop, index.step)
        return range(0, 100, 10)[index]
  
    def __setitem__(self, index, value):
        if isinstance(index, int):
            print('1. Index:', index)
            print('2. Value:', value)
        else:
            print('1. Slice:', index, '--', index.start, index.stop, index.step)
            print('2. Value:', value)


I = C()

print(bin(I), oct(I), hex(I))
print(range(10, 30)[I])
print()

I[4]
I[2: 10: 3]
I[7] = 'X'
I[2: 10: 3] = 'X'
print()
# Example of an iteration context.
# When index becomes equal to 10, IndexError is raised.
print(list(I))
print()

print(30 in I)
print(35 in I)

In [ ]:
class C:
    def __init__(self):
        self.data = list(range(0, 100, 10))
        
    def __getitem__(self, index):
        return self.data[index]
    
    def __iter__(self):
        return self
    
    def __next__(self):
        try:
            return self.data.pop()
        except IndexError:
            raise StopIteration


# __iter__() takes over __getitem__() in an iteration context.
print(list(C()))
print()

# __iter__() takes over __getitem__() for membership test
print(30 in C())
print(35 in C())

In [ ]:
class C:
    def __init__(self):
        self.data = list(range(0, 100, 10))
        
    def __getitem__(self, index):
        return self.data[index]
    
    def __iter__(self):
        return self
    
    def __next__(self):
        try:
            return self.data.pop()
        except IndexError:
            raise StopIteration
            
    def __contains__(self, value):
        if value in self.data:
            print('Contains', value)
        else:
            print('Does not contain', value)

#  __contains()__ takes over __iter__() for membership test
30 in C()
35 in C()
print()

In [ ]:
class C:
    def __init__(self, datum):
        self.datum = datum
    
    def __lt__(self, value):
        return self.datum < value
   
    def __le__(self, value):
        return self.datum <= value

    def __eq__(self, value):
        return self.datum == value
    
    # Better to implement __eq__() but not __ne__(),
    # in which case the negation of the value returned by
    # a == b will be used when evaluating a != b.
    def __ne__(self, value):
        return self.datum != value
    
    def __gt__(self, value):
        return self.datum > value
    
    def __ge__(self, value):
        return self.datum >= value


I = C(2)
J = C(3)
print(I < J, I <= J, I == J, I != J, I > J, I >= J)

Illustration of add, radd and iadd as examples for the following list of operators, each of which has left and in-place variants:

  • add for +
  • sub for -
  • mul for *
  • truediv for /
  • floordiv for //
  • mod for %
  • pow for **
  • lshift for <<
  • rshift for >>
  • and for &
  • xor for ^
  • or for |

In [ ]:
class C:
    def __init__(self, datum):
        self.datum = datum
        
    def __add__(self, value):
        return C(self.datum + value)
    
    
I = C(2)
J = I + 3
print(J.datum)
# Cannot perform 3 + I
I += 5
print(I.datum)

In [ ]:
class C:
    def __init__(self, datum):
        self.datum = datum
        
    def __add__(self, value):
        return self.datum + value

    def __radd__(self, value):
        return self + value
        # Alternatively:
        # return self.__add__(value)
    # A possible alternative:
    # __radd__ = __add__

    def __iadd__(self, value):
        self.datum += value
        return self
    
    
I = C(2)
print(I + 3)
print(4 + I)
# __iadd__() takes over __add__().
I += 5
print(I.datum)

In [ ]:
class C:
    def __init__(self, datum):
        self.datum = datum
    
    def __getattr__(self, attribute):
        if attribute == 'accepted_undefined':
            return 'Accepted undefined'
        elif attribute == '__add__':
            print('Accepted addition')
            return getattr(self.datum, attribute)
#        else:
#            raise AttributeError(attribute)
        

I = C(2)
I.__mul__ = lambda value: I.datum * value
print(I.datum)
print(I.accepted_undefined)
print(I.unaccepted_undefined)
print(I.__add__(4))
# Raises TypeError:
# print(I + 4)
print(I.__mul__(4))
# Raises TypeError:
# print(I * 4)

In [ ]:
class C:
    def __init__(self, datum):
        self.datum = datum
    
    def __getattribute__(self, attribute):
        if attribute == 'accepted_undefined':
            return 'Accepted undefined'
        elif attribute == '__add__':
            print('Accepted addition')
            return getattr(object.__getattribute__(self, 'datum'), attribute)
#        else:
#            raise AttributeError(attribute)
        

I = C(2)
I.__mul__ = lambda value: object.__getattribute__(self, 'datum') * value
print(I.datum)
print(I.accepted_undefined)
print(I.unaccepted_undefined)
print(I.__add__(4))
# Raises TypeError:
# print(I + 4)
# Raises TypeError:
# print(I.__mul__(4))

In [ ]:
class C:
    def __setattr__(self, attribute, value):
        if attribute == 'handled_attribute':
            self.__dict__['handled_attribute'] = value
        

I = C()
I.handled_attribute = 'X'
print(I.handled_attribute)
# Raises AttributeError:
# I.other_attribute = 'Y'
# print(I.other_attribute)

In [ ]:
class C:
    def __init__(self):
        self.datum_1 = 'X'
        self.datum_2 = 'Y'
        self.datum_3 = 'Z'


    def __delattr__(self, attribute):
        if attribute == 'datum_1':
            print('datum_1 deleted')
        elif attribute == 'datum_2':
            print('datum_2 deleted')
            del self.__dict__['datum_2']

I = C()
del I.datum_1
del I.datum_2
print(I.__dict__)
print()

In [ ]:
class C:
    def __del__(self):
        print('Bye C object!')

I = C()
I = 'Something else'
print()