<jupyter_start><jupyter_text>Special Methods in a CLASS- Using special methods, your classes can act like sets, like dictionaries, like functions, like iterators, or even like numbers.  1. Basic Class<jupyter_code>class MyClass:
    name = None
    
    def __init__(self):
        self.name = "Jai"
        print("init")
        
    def __call__(self):
        print("__Call__ is called")
        
    def __str__(self):
        print("__str__ is called")
        return self.name
    
    def __repr__(self):
        print("__repr__ is called")
        return self.name

    def __format__(self):
        print("__format__ is called") 
    
    def __dir__(self):
        return ["__init__", "__call__", "__repr__", "print_name", "set_name"]
    
obj1 = MyClass()<jupyter_output>init<jupyter_text>2. Classes That Act Like IteratorsMethods used- \__iter__ :- to iterate through a sequence    iter(seq)- \__next__ :- to get the next value from an iterator	next(seq)- \__reversed__:- to create an iterator in reverse order	reversed(seq)1. The __iter__() method is called whenever you create a new iterator. It’s a good place to initialize the iterator with initial values.2. The __next__() method is called whenever you retrieve the next value from an iterator.3. The __reversed__() method is uncommon. It takes an existing sequence and returns an iterator that yields the items in the sequence in reverse order, from last to first.<jupyter_code>class Fibonacci:
    def __init__(self, max):
        self.max = max
        self.counter = 0
        
    def __iter__(self):
        self.a = 0
        self.b = 1
        return self
    
    def __next__(self):
        if self.counter > self.max:
            raise StopIteration
        
        fib = self.a
        self.b += self.a 
        self.a = self.b
        self.counter += 1
        
        return fib
    
    def __reveresed__(self): #uncommon
        pass
for i in Fibonacci(10):
    print(i, end = " ")<jupyter_output>0 1 1 2 3 5 8 13 21 34<jupyter_text>3. Computed AttributesNotesYou Want…So You Write…And Python Calls…①to get a computed attribute (unconditionally)x.my_propertyx.__getattribute__('my_property')②to get a computed attribute (fallback)x.my_propertyx.__getattr__('my_property')③to set an attributex.my_property = valuex.__setattr__('my_property', value)④to delete an attributedel x.my_propertyx.__delattr__('my_property')⑤to list all attributes and methodsdir(x)x.__dir__()If your class defines a __getattribute__() method, Python will call it on every reference to any attribute or method name (except special method names, since that would cause an unpleasant infinite loop).If your class defines a __getattr__() method, Python will call it only after looking for the attribute in all the normal places. If an instance x defines an attribute color, x.color will not call x.__getattr__('color'); it will simply return the already-defined value of x.color.The __setattr__() method is called whenever you assign a value to an attribute.The __delattr__() method is called whenever you delete an attribute.The __dir__() method is useful if you define a __getattr__() or __getattribute__() method. Normally, calling dir(x) would only list the regular attributes and methods. If your __getattr__() method handles a color attribute dynamically, dir(x) would not list color as one of the available attributes. Overriding the __dir__() method allows you to list color as an available attribute, which is helpful for other people who wish to use your class without digging into the internals of it.<jupyter_code>class ComputedAtributted:
    def __init__(self, name):
        self.name = name
        
    def __setattr__(self, prop, value):
        # self.name = value
        print("__setattr__ called, with params prop: {0}, value: {1}".format(prop, value))
    
    def __getattr__(self, prop):
        print("__getattr__ called, with param: {}".format(prop))
        if prop == "name":
            return "name = JAi"
        else:
            raise AttributeError
            
    def __getattribute__(self, prop):
        print("__getattrinute__ called, with param: {}".format(prop))
        if prop == "name":
            return "name = JAi"
        else:
            raise AttributeError
            
    def __delattr__(self, prop):
        print("__delattr__ called with param".format(prop))
        
    def __dir__(self):
        return ["__init__", "__setattr__", "__getattribute__", "__delattr__"]
obj = ComputedAtributted("Jai")
obj.name = "<NAME>"

name = obj.name
print(name)<jupyter_output>__setattr__ called, with params prop: name, value: Jai
__setattr__ called, with params prop: name, value: Jai Singhal
__getattrinute__ called, with param: name
name = JAi<jupyter_text>4. Classes That Act Like FunctionsYou can make an instance of a class callable — exactly like a function is callable — by defining the \__call__() method.  5. Classes That Act Like NumbersUsing the appropriate special methods, you can define your own classes that act like numbers. That is, you can add them, subtract them, and perform other mathematical operations on them. This is how fractions are implemented — the Fraction class implements these special methods, then you can do things like this:NotesYou Want…So You Write…And Python Calls…additionx + yx.__add__(y)subtractionx - yx.__sub__(y)multiplicationx * yx.__mul__(y)divisionx / yx.__truediv__(y)floor divisionx // yx.__floordiv__(y)modulo (remainder)x % yx.__mod__(y)floor division &amp; modulodivmod(x, y)x.__divmod__(y)raise to powerx ** yx.__pow__(y)left bit-shiftx &lt;&lt; yx.__lshift__(y)right bit-shiftx &gt;&gt; yx.__rshift__(y)bitwise andx &amp; yx.__and__(y)bitwise xorx ^ yx.__xor__(y)bitwise orx | yx.__or__(y)But wait! There’s more! If you’re doing “in-place” operations, like x /= 3, there are even more special methods you can define.NotesYou Want…So You Write…And Python Calls…in-place additionx += yx.__iadd__(y)in-place subtractionx -= yx.__isub__(y)in-place multiplicationx *= yx.__imul__(y)in-place divisionx /= yx.__itruediv__(y)in-place floor divisionx //= yx.__ifloordiv__(y)in-place modulox %= yx.__imod__(y)in-place raise to powerx **= yx.__ipow__(y)in-place left bit-shiftx &lt;&lt;= yx.__ilshift__(y)in-place right bit-shiftx &gt;&gt;= yx.__irshift__(y)in-place bitwise andx &amp;= yx.__iand__(y)in-place bitwise xorx ^= yx.__ixor__(y)in-place bitwise orx |= yx.__ior__(y)There are also a few “unary” mathematical operations you can perform on number-like objects by themselves.NotesYou Want…So You Write…And Python Calls…negative number-xx.__neg__()positive number+xx.__pos__()absolute valueabs(x)x.__abs__()inverse~xx.__invert__()complex numbercomplex(x)x.__complex__()integerint(x)x.__int__()floating point numberfloat(x)x.__float__()number rounded to nearest integerround(x)x.__round__()number rounded to nearest n digitsround(x, n)x.__round__(n)smallest integer &gt;= xmath.ceil(x)x.__ceil__()largest integer &lt;= xmath.floor(x)x.__floor__()truncate x to nearest integer toward 0math.trunc(x)x.__trunc__()PEP 357number as a list indexa_list[x]a_list[x.__index__()]<jupyter_code>class Mathematics:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    
    def __add__(self, obj):
        print("__add__ is called with params: {0}". format(obj))
        self.a += obj.a
        self.b += obj.b
        return self
    
    def __sub__(self, obj):
        print("__sub__ is called with params: {0}". format(obj))
        self.a -= obj.a
        self.b -= obj.b
        return self
    
    def __neg__(self):
        print("__neg__ is called")
        self.a = (-1)*self.a
        self.b = (-1)*self.b     
        return self
    
    def __iadd__(self, obj):
        print("__iadd__ is called  with param {}".format(obj))
        self.a = self.a + obj.a
        self.b = self.b + obj.b
        return self
    
#     def __getattribute__(self, param):
#         print("__getattr__ called")
#         if param == "a":
#             print(param)
#         elif param == "b":
#             print(param)
obj1 = Mathematics(5, 1)
obj2 = Mathematics(3, 6)

obj1 = obj1 + obj2
obj1 = obj1 - obj2
obj1 = -obj1
obj1 += obj2<jupyter_output>__add__ is called with params: <__main__.Mathematics object at 0x7f12e4043c50>
__sub__ is called with params: <__main__.Mathematics object at 0x7f12e4043c50>
__neg__ is called
__iadd__ is called  with param <__main__.Mathematics object at 0x7f12e4043c50><jupyter_text>\__call__ method Called when the instance is “called” as a function; if this method is defined, x(arg1, arg2, ...) is a shorthand for x.__call__(arg1, arg2, ...).To make the classes callable, we use \__call__ method<jupyter_code>import socket

class Resolver:
    
    def __init__(self):
        self._cache = {}
    
    def __call__(self, host):
        if host not in self._cache:
            self._cache[host] = socket.gethostbyname(host)
        return self._cache[host]
    
    def clear(self):
        self._cache.clear()
    
    def has_host(self, host):
        return host in self._cache
resolve = Resolver()

resolve("www.djangopy.org")
resolve.has_host("www.djangopy.org")
resolve.clear()
resolve.has_host("www.djangopy.org")
from timeit import timeit

# resolve.clear()
timeit(setup = "from __main__ import resolve", stmt = "resolve('www.djangopy.org')", number = 1)<jupyter_output><empty_output><jupyter_text>classes are callable<jupyter_code>def sequence_class(immutable):
    if immutable:
        cls = tuple
    else:
        cls = list
    return cls

t = sequence_class(True)
print(t)
x = t("<NAME>")
x<jupyter_output><class 'tuple'><jupyter_text>conditional expressions<jupyter_code>def sequence_class_type2(immutable):
    return tuple if immutable else list

seq = sequence_class_type2(False)
t = seq("jai")
print(type(t))
print(t)<jupyter_output><class 'list'>
['j', 'a', 'i']<jupyter_text>Lambdas<jupyter_code>scientists = ["<NAME>", "<NAME>", "<NAME>", "<NAME>", "<NAME>"]

sorted(scientists, key = lambda name: name.split()[1], reverse = True)<jupyter_output><empty_output><jupyter_text>Detecting callablewe can check if the object is callable or not, we use built in function callable<jupyter_code>class Myclass:
    def __init__(self):
        self.name= "jai"
        
class Myclass2:
    def __init__(self):
        self.name= "jai"
        
    def __call__(self):
        return True

def is_even(x):
    return x%2 == 0

is_odd = lambda x: x%2 != 0

# functions are callable
print(callable(is_even))

#lambdas are callable
print(callable(is_odd))

# classes are callabe
print(callable(list))

#class methods are callable
print(callable(list.append))

print(callable(Myclass))
obj = Myclass()
print("This is >>{}<< because class does not contain __call__".format(callable(obj)))

print(callable(Myclass2))
obj2 = Myclass2()
print("This is >>{}<< because class contains __call__".format(callable(obj2)))<jupyter_output>True
True
True
True
True
This is >>False<< because class does not contain __call__
True
This is >>True<< because class contains __call__
{'fraction_non_alphanumeric': 0.1048255919031267, 'fraction_numerical': 0.009217422736309416, 'mean_word_length': 3.653910849453322, 'pattern_counts': {'":': 4, '<': 50, '<?xml version=': 0, '>': 51, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '361716', 'license': 'MIT', 'n_tokens_mistral': 3574, 'n_tokens_neox': 3475, 'n_words': 1101, 'path': 'python_basics/Beyond Basic Functions.ipynb', 'repo_name': 'jai-singhal/data_science', 'size': '11103'}