<jupyter_start><jupyter_text>Classes are Callable+ when we create a class using the `class` keyword Python automatically adds behaviors to the class in particular:   + it adds something to make the class __callable__   + the return value of that callable is an __object__       + the __type__ of that object is a __class object__       + we say the object is an instance of the class --> also called __instantiation__ or __instantiating the class__    ----     As we saw earlier, one of the things Python does for us when we create a class is to make it callable.Calling a class creates a new instance of the class - an object of that particular type.<jupyter_code>class Program:
    language = 'Python'  # data attribute of the class
    
    def say_hello(): # function attribute of the class
        print(f'Hello from {Program.language}!')
my_obj = Program()<jupyter_output><empty_output><jupyter_text>+ when we call a class, a class instance object is created + this class instance object has its own namespace (it's own `__dict__`)    + distinct from the nampespace of the class that was used to create object    + This object has some attributes Python automatically implements for us:    +  `__dict__` : is the object's local namespace    + `__class__` tells us which class was used to instantiate the object        + prefer using `type(obj)` instead of `obj.__class__`<jupyter_code>type(my_obj)
type(Program)
isinstance(my_obj, Program)<jupyter_output><empty_output><jupyter_text>These instances have their own namespace, and their own `__dict__` that is distinct from the class `__dict__`:<jupyter_code>my_obj.__dict__
Program.__dict__<jupyter_output><empty_output><jupyter_text>So, `language` and `say_hello` are class attributes not instance attributes Instances also have attributes that may not be visible in their `__dict__` (they are being stored elsewhere, as we'll examine later):<jupyter_code>my_obj.__class__<jupyter_output><empty_output><jupyter_text>Although we can use `__class__` we can also use `type`:<jupyter_code>type(my_obj) is my_obj.__class__<jupyter_output><empty_output><jupyter_text>Generally we use `type` instead of using `__class__` just like we usually use `len()` instead of accessing `__len__`. Why? Well, one reason is that people can mess around with the `__class__` attribute:<jupyter_code>class MyClass:
    pass
m = MyClass()
type(m), m.__class__<jupyter_output><empty_output><jupyter_text>But look at what happens here:<jupyter_code>class MyClass:
    __class__ = str
m = MyClass()
type(m), m.__class__<jupyter_output><empty_output><jupyter_text>So as you can see, `type` wasn't fooled!<jupyter_code>isinstance(m, MyClass)  # the more common to recognize the type of an instance
isinstance(m, str) 
# Don't use __class__<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07725321888412018, 'fraction_numerical': 0.0, 'mean_word_length': 5.385844748858448, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 34, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, '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': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '393601', 'license': 'MIT', 'n_tokens_mistral': 844, 'n_tokens_neox': 827, 'n_words': 322, 'path': 'OOP/04 - Classes are Callable.ipynb', 'repo_name': 'Rafiei-tayebe/AP4002', 'size': '2796'}