<jupyter_start><jupyter_text>Python Classes<jupyter_code>class Circle(object):
    'Optional class documentation string'
    # pi is a class variable - Shared by all objects of the class
    pi = 3.142

    # Class constructor / initializer
    def __init__(self, radius):
        # self.radius is an instance variable
        self.radius = radius

    def area(self):
        # access pi through instance
        return self.pi * (self.radius ** 2)
    
# Creating instances (objects of the class)
a = Circle(3)
b = Circle(3)
print("a.area", a.area())
print("a.pi =", a.pi, end='\n\n')

print("After updating radius of object 'a'")
a.radius = 5
print("a.pi =", a.pi)
print("a.radius =", a.radius)
print("a.area", a.area(), end='\n\n')

print("After changing value of pi through object 'a'")
a.pi = 10
print("Circle.pi =", Circle.pi)
print("a.pi =", a.pi)
print("b.pi =", b.pi)
print("a.radius =", a.radius)
print("a.area", a.area(), end='\n\n')

print("After changing value of pi through Class")
Circle.pi = 50
print("Circle.pi =", Circle.pi)
print("a.pi =", a.pi)
print("b.pi =", b.pi)
print("a.area", a.area())
print("b.area =", b.area(), end='\n\n')

print("Note: Updating class variable through instance creates a local copy of class variable in the instance.")
print("Here, b.pi is not modified, so it still refers to pi in the class Circle")<jupyter_output>a.area 28.278
a.pi = 3.142

After updating radius of object 'a'
a.pi = 3.142
a.radius = 5
a.area 78.55

After changing value of pi through object 'a'
Circle.pi = 3.142
a.pi = 10
b.pi = 3.142
a.radius = 5
a.area 250

After changing value of pi through Class
Circle.pi = 50
a.pi = 10
b.pi = 50
a.area 250
b.area = 450

Note: Updating class variable through instance creates a local copy of class variable in the instance.
Here, b.pi is not modified, so it still refers to pi in the class Circle<jupyter_text>One can add, remove, or modify attributes of classes and objects at any time.<jupyter_code>print("Attributes before adding:\n", dir(a), end='\n\n')
a.xyz = 'abc'  # Add an 'xyz' attribute.
a.color = 7  # Modify 'radius' attribute.
print("Attributes after adding:\n", dir(a), end='\n\n')
del a.color  # Delete 'radius' attribute.
print("Attributes after deleting:\n", dir(a))<jupyter_output>Attributes before adding:
 ['__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__', 'area', 'pi', 'radius']

Attributes after adding:
 ['__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__', 'area', 'color', 'pi', 'radius', 'xyz']

Attributes after deleting:
 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subc[...]<jupyter_text>The attributes of Class can be accessed/modified with following functions −The hasattr(obj,name) − to check if an attribute exists or not.The getattr(obj, name[, default]) − to access the attribute of object.The setattr(obj,name,value) − to set an attribute. If attribute does not exist, then it would be created.The delattr(obj, name) − to delete an attribute.<jupyter_code>print(hasattr(a, 'xyz'))
print(getattr(a, 'xyz'))
setattr(a, 'xyz', 'pqr')
print(getattr(a, 'xyz'))
delattr(a, 'xyz')
print(hasattr(a, 'xyz'))
setattr(a, 'xyz', 'abc')<jupyter_output>True
abc
pqr
False<jupyter_text>Built-In Class AttributesEvery Python class keeps following built-in attributes and they can be accessed using dot operator like any other attribute. These are also called Magic Methods.\_\_dict\_\_ − Dictionary containing the class's namespace.\_\_doc\_\_ − Class documentation string or none, if undefined.\_\_name\_\_ − Class name.\_\_module\_\_ − Module name in which the class is defined. This attribute is "__main__" in interactive mode.\_\_bases\_\_ − A possibly empty tuple containing the base classes, in the order of their occurrence in the base class list.  Constructor and destructor\_\_init\_\_ : It is a class initializer. Whenever an instance of a class is created this method is called.\_\_del\_\_ : It is a class cleanup method. Whenever an instance of a class is deleted or garbage collected, this method is called.<jupyter_code>class class1(object):
    def __init__(self):
        print('Creating object of class class1')
    
    def __del__(self):
        class_name = self.__class__.__name__
        print("Object of", class_name, "destroyed")

c1 = class1()
del c1<jupyter_output>Creating object of class class1
Object of class1 destroyed<jupyter_text>Adding indexing operator [] to classAdd method \_\_getitem\_\_Implementing getitem in a class allows its instances to use the [] (indexer) operator.<jupyter_code>class class2(object):
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z
    def __getitem__(self, i):
        if i == 1:
            return self.x
        elif i == 2:
            return self.y
        elif i == 3:
            return self.z
        else:
            return None

foo = class2('one', 'two', 'three')
print(foo[1])
print(foo[3])
print(foo['two'])<jupyter_output>one
three
None<jupyter_text>Class InheritancePython supports class hierarchies.<jupyter_code>class P1:
    def __init__(self):
        print ("Calling P1 constructor")
    def m1(self):
        print ("Calling P1's method m1")
    def m2(self):
        print ("Calling P1's method m2")

class P2:
    def __init__(self):
        print ("Calling P2's constructor")
    def m3(self):
        print ("Calling P2's method m3")


class Ch1(P1, P2):
    def __init__(self):
        print ("Calling Ch1's constructor")
    def cm1(self):
        print ("Calling Ch1's method cm1")
    def m2(self):
        print ("Calling Ch1's method m2 which override P1's m2 methos")

a = Ch1()
a.m1()
a.m3()
a.cm1()
a.m2()

print(issubclass(Ch1, P1))
print(issubclass(Ch1, P2))
print(isinstance(a, Ch1))<jupyter_output>Calling Ch1's constructor
Calling P1's method m1
Calling P2's method m3
Calling Ch1's method cm1
Calling Ch1's method m2 which override P1's m2 methos
True
True
True<jupyter_text>Python Class Data HidingEvery attribute (data/method) in the class are public.The convention is, any attribute name prefixed with an underscore (e.g. \_spam) should be treated as a non-public member. It should be considered an implementation detail and subject to change without notice (but it is accessible).Since there is a valid use-case for class-private members (namely to avoid name clashes of names with names defined by subclasses), there is limited support for such a mechanism, called name mangling. Any identifier of the form \_\_spam (at least two leading underscores, at most one trailing underscore) is textually replaced with \_classname\_\_spam, where classname is the current class name with leading underscore(s) stripped.Name mangling is helpful for letting subclasses override methods without breaking intraclass method calls. For example:<jupyter_code>class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12726364431122766, 'fraction_numerical': 0.014612214312476583, 'mean_word_length': 4.156471345782356, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 23, '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': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '47885', 'license': 'Apache-2.0', 'n_tokens_mistral': 2695, 'n_tokens_neox': 2585, 'n_words': 902, 'path': 'py_Classes.ipynb', 'repo_name': 'tarunkp/Python_Tutuorial_QuickRef', 'size': '8007'}