<jupyter_start><jupyter_text>Classes<jupyter_code>class Vehicle:
    """example class"""
    def __init__(self, wheels, seats):
        """
        The __init__ method is used for object creation.
        """
        self.wheels = wheels
        self.seats = seats
    def drive(self, km):
        print("driving {} km on {} wheels".format(km, self.wheels))
bicycle = Vehicle(2, 1)
bicycle.drive(10)
bicycle.wheels<jupyter_output><empty_output><jupyter_text>Inheritance<jupyter_code>class Car(Vehicle):
    def __init__(self, seats, power):
        super().__init__(wheels=4, seats=seats) # calls constructuor of parent class
        self.power = power
    def drive(self, km):
        print("driving {} km on {} wheels with {} kW power".format(
            km, self.wheels, self.power))
my_car = Car(5, 150)
my_car.drive(200)<jupyter_output>driving 200 km on 4 wheels with 150 kW power<jupyter_text>If a method is overwritten in the child class (like init in the example), the method of the parent class can be accessed with super().  Multiple Inheritance<jupyter_code>class PurchaseMixin:
    price = None
    currency = 'EUR'
    nr_owned = 0
    def buy(self, nr=1):
        self.nr_owned += nr
        print("purchased {} piece(s) for {} {}".format(nr, nr*self.price, self.currency))
    def sell(self, nr=1):
        if nr > self.nr_owned:
            raise ValueError('you own less than you want to sell!')
        self.nr_owned -= nr
        print("sold {} piece(s) for {} {}".format(nr, nr*self.price, self.currency))
class PurchasableCar(PurchaseMixin, Car):
    pass
my_cars = PurchasableCar(5, 150)
my_cars.price = 30000
my_cars.buy(2)
my_cars.sell()
my_cars.drive(250)
my_cars.nr_owned<jupyter_output>purchased 2 piece(s) for 60000 EUR
sold 1 piece(s) for 30000 EUR
driving 250 km on 4 wheels with 150 kW power<jupyter_text>Classes in Python may inherit from multiple parent classes. This can be used to add additional features to classes. Naming Resolution<jupyter_code>class A:
    def __init__(self):
        self.i = 1
        print('init A')
    def func(self, x):
        return x+1
class B:
    def __init__(self):
        self.i = 2
        print('init B')
    def func(self, x):
        return x+2
class AB(A,B):
    pass
class BA(B,A):
    pass
ab = AB()
ba = BA()
ab.func(0), ba.func(0)<jupyter_output>init A
init B<jupyter_text>If parent classes have the same methods (including init), the derived class uses the method of the 1st parent.<jupyter_code>class AB2(A, B):
    def func(self,x):
        return B.func(self, x)
ab2 = AB2()
ab2.func(0)<jupyter_output>init A<jupyter_text>Methods in the 2nd parent class, which have been overwritten my identically named methods in the 1st parent class, can be called using the parent class name as prefix.  Property Methods<jupyter_code>my_cars.price = 'hello'
my_cars.buy() # output is not what is expected<jupyter_output>purchased 1 piece(s) for hello EUR<jupyter_text>In languages like C++ and Java one usually implements getter and setter methods for any public property. This is discouraged in Python because it creates a lot of boilerplate code.Instead, properties should be accessed directly.If it turns out later that logic (like validations, formatting) is required when getting or setting properties, they can be changed in the class definition to property methods without changing the class api.<jupyter_code>from numbers import Number
class SafePurchaseMixin(PurchaseMixin):
    _price = None
    
    @property
    def price(self):
        if self._price is None:
            raise ValueError('price not set')
        return self._price
    
    @price.setter
    def price(self, x):
        if not isinstance(x, Number):
            raise TypeError('Price must be numeric')
        if x <= 0:
            raise ValueError('Price must be positive')
        self._price = x
        
    @price.deleter # for completeness, often not required
    def price(self):
        self._price = None
class PurchasableCar2(SafePurchaseMixin, Car):
    pass
my_cars = PurchasableCar2(5, 150)
my_cars.price = 30000 # note that the api is identical to PurchasableCars
my_cars.buy(2)
my_cars.sell()
my_cars.drive(250)
my_cars.nr_owned
try:
    my_cars.price = 'hello'
except TypeError as e:
    print(e)
del my_cars.price
try:
    print(my_cars.price)
except ValueError as e:
    print(e)<jupyter_output>price not set<jupyter_text>In the property methods for price, validation has been included without any change of the class api.  Static Methods<jupyter_code>class PurchasableCar3(PurchasableCar2):
    @staticmethod
    def convert_kw_to_hp(kw):
        return 1.36 * kw
PurchasableCar3.convert_kw_to_hp(120)<jupyter_output><empty_output><jupyter_text>Methods inside a class which do not need any input from the class itself, can (and should) be defined as static methods. This is done as follows:1. the decorator @staticmethod2. self is not a parameter of the methodStatic mehods can be used analogue to standard object methods. In addition, it they could be called on the class itself, without need to instanciate an object.  Class Methods<jupyter_code>class PurchasableCar4(PurchasableCar3):
    @classmethod
    def from_dict(cls, data):
        obj = cls(seats=data.get('seats'),
                 power=data.get('power'))
        obj.price=data.get('price')
        obj.currency=data.get('currency','EUR')
        return obj
car_data = {'seats': 2, 'power': 250, 'price': 125000}
sports_car = PurchasableCar4.from_dict(car_data)
print(f'''seats: {sports_car.seats}, 
power in hp: {sports_car.convert_kw_to_hp(sports_car.power)},
price: {sports_car.price} {sports_car.currency}''')<jupyter_output>seats: 2, 
power in hp: 340.0,
price: 125000 EUR<jupyter_text>Class methods are executed on classes, not objects (i.e. instances of classes).A common use case for class methods are alternative constructors which return class instances.More about static and class methods here:https://realpython.com/instance-class-and-static-methods-demystified/  Dunder Methods<jupyter_code>from math import sqrt
class ComplexNumber:
    """
    just as an example, in practice use the built-in complex data type!
    """
    def __init__(self,real, imag=0):
        self.r = real
        self.i = imag
    
    @staticmethod
    def _convert_num(x):
        """
        Converts number or ComplexNumber into real and imaginary part.
        This is an internal method, marked by leading underscore (_).
        """
        if isinstance(x, ComplexNumber):
            r2 = x.r
            i2 = x.i
        else:
            r2 = x
            i2 = 0
        return r2, i2
    
    def __str__(self):
        """Defines string representation of the object."""
        return "{r} + {i} * i".format(r=self.r, i=self.i)
    
    def __repr__(self):
        """Defines a printable representation of the object. 
        Usually __repr__ contains more debugging information than __str__."""
        return "ComplexNumber object, value: {r} + {i} * i".format(r=self.r, i=self.i)
    
    def __add__(self, x):
        r2, i2 = self._convert_num(x)
        return ComplexNumber(self.r + r2, self.i + i2)

    def get_abs_value(self):
        return sqrt(self.r**2 + self.i**2)
c1 = ComplexNumber(1, 2)
print(c1)<jupyter_output>1 + 2 * i<jupyter_text>The print function implicitly converts the ComplexNumber object into a string, i.e. printing the output of the \_\_str__ method.<jupyter_code>c1
c1.r, c1.i, c1.get_abs_value()
c2 = ComplexNumber(2, 3)
c3 = c1 + c2
print(c3)
print(c1 + 7)<jupyter_output>8 + 2 * i<jupyter_text>Singleton PatternA singleton is a class which may have only one instance. Any attempt to create a second instance of this class instead returns a pointer to the existing instance.Singleton support is not included in Python standard library. Indeed, it is very controversial if singletons should be used in Python at all, many consider this as an anti-pattern because of its intransparency and global state.There are many possibilities to introduce singletons in Python, the metaclass method below is probably the best.Source: https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python<jupyter_code>class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]
class MySingleton(metaclass=Singleton):
    def __init__(self):
        print('singleton initiated')
s1 = MySingleton()
s1.x = 'hello world'
hex(id(s1))
s2 = MySingleton()
hex(id(s2))
s2.x<jupyter_output><empty_output><jupyter_text>Namespaces<jupyter_code>outside1 = 42
outside2 = 0
def f_bad(x):
    outside2 = x # this is actually a local variable 
                 # which has nothing to do with outside 2 defined before
    print(outside2)
    return x + outside1 # extremely dangerous usage of global variable
outside2
f_bad(7), outside2<jupyter_output>7<jupyter_text>Variables defined inside a function (or class) are local, i.e. they are only valid inside the function and changes of them do not influence anything outside the function. This is true even if outside variables have the same name (here global2).Variables defined outside the function, however, can be accessed from inside the function. This is however very dangerous and should be avoided!<jupyter_code>def f_even_worse(x):
    global outside2 # makes the variable defined outside changable inside the function
    outside2 = x # the outside variable is actually changed here!
    print(outside2)
    return x + outside1 # extremely dangerous usage of global variable
outside2
f_even_worse(7), outside2<jupyter_output>7<jupyter_text>The *global* stament makes the variable defined outside changable inside the function. The usage of this pattern is __extremely__ dangerous and must be avoided!<jupyter_code>def f_good(x, i):
    print(x)
    return x + i
f_good(7, 42)<jupyter_output>7<jupyter_text>Refactored version of the previous function without implicit usage of variable defined outside the function.<jupyter_code>def f_mod_input(i: int, l: list):
    l.append(i)
    i = i*2
    return i
i1 = 42 # basic variable
l1 = [1, 2] # list object
f_mod_input(i1, l1)
i1, l1<jupyter_output><empty_output><jupyter_text>Both function parameters (here integer and list) are changed inside the function.* The basic type (here integer) is however not changed in the outside scope - call by value* The complex type (here list) is changed in the outside scope by the modification inside the function - call by referenceIt is a common source of errors and (in my opinion) a major weakness of Python language that call by value and call by reference are not clearly separated by syntax (like in C (++) where pointers have * as prefix).Call by reference can be used on purpose to modify objects in outer scope by the function. However, it is safer to explicitly create a new object inside the function, modify that and return it.<jupyter_code>def f_safer(i: int, l: list):
    l2 = l.copy() # creates new list object
    l2.append(i)
    i = i*2
    return i, l2
i1 = 42 # basic variable
l1 = [1, 2] # list object
f_safer(i1, l1)
i1, l1<jupyter_output><empty_output><jupyter_text>Context Managers Motivation and Usage<jupyter_code>with open('temp.txt','w') as f:
    f.write('hello world\n')
    f.write('test file for context manager\n')
with open('temp.txt','r') as f:
    content = f.readlines()
content<jupyter_output><empty_output><jupyter_text>The *with* statement is a context manager which gives access to the specified resource (here a text file) in the following code block.The main advantage is that the context manager automatically takes care of closing the resource when the code block is finished or there is an error.<jupyter_code>try:
    f = open('temp.txt')
    content = f.readlines()
finally:
    f.close()
content<jupyter_output><empty_output><jupyter_text>Implementation without context manager would be significantly more complex (here 5 lines instead of 2 for the read part) and error-prone (e.g. if it is forgotten to implement the close command or if it is not in a finally block).  Implementing Own Context Managershttps://stackabuse.com/python-context-managers/<jupyter_code>class MyFileHandler:
    """only for illustration - redundant because Python's open already includes a 
    context manager"""
    def __init__(self, filename, kind='r'):
        self.filename = filename
        self.kind = kind
    def __enter__(self):
        self.file = open(self.filename, self.kind)
        print('file opened')
        return self.file
    def __exit__(self, *exc):
        self.file.close()
        print('file closed')
try:
    with MyFileHandler('temp.txt','r') as f:
        content = f.readlines()
        print('file read')
        raise Exception('an unexpected problem - file should nevertheless be closed!')
except Exception as e:
    print(e)<jupyter_output>file opened
file read
file closed
an unexpected problem - file should nevertheless be closed!<jupyter_text>Context managers are implemented by defining the dunder methods    __enter__(self)    __exit__(self, *exc)    Note that in the example the file is closed even though an exception has been raised.  Cleanup<jupyter_code>from os import remove
remove('temp.txt')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08979439252336449, 'fraction_numerical': 0.01652336448598131, 'mean_word_length': 3.7908309455587395, 'pattern_counts': {'":': 0, '<': 73, '<?xml version=': 0, '>': 73, 'https://': 3, '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': '21', 'dir': 'jupyter-structured-clean-dedup', 'id': '563820', 'license': 'MIT', 'n_tokens_mistral': 4250, 'n_tokens_neox': 4063, 'n_words': 1460, 'path': 'notebooks/classes.ipynb', 'repo_name': 'lungben/python_tutorial', 'size': '13375'}