├── .gitignore ├── LICENSE ├── RAF sheet.py ├── README.md ├── off day.py ├── oop.jpg ├── oop01 (classes and objects).py ├── oop02 (class variables).py ├── oop03 (class methods).py ├── oop04 (static methods).py ├── oop05 (inheritance and subclasses).py ├── oop06 (special_magic_dunder methods).py ├── oop07 (@property decorator).py ├── oop08 (combining multiple classes).py ├── oop09 (5 important tips and tricks for oop).py ├── oop10 (polymorphism in python).py ├── oop11 (abstraction in python).py ├── oop12 (Inner class).py ├── oop13 (libray & user code).py ├── oop14 (metaclasses1).py ├── oop15 (metaclasses2).py ├── oop16 (metaclasses3).py ├── oop17 (__slots__).py └── oop18 (quick tips).py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ahammad Shawki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RAF sheet.py: -------------------------------------------------------------------------------- 1 | class Employee: 2 | num_emp=0 3 | raise_amount=1.05 4 | def __init__(self,first,last,pay): 5 | self.first=first 6 | self.last=last 7 | self.pay=pay 8 | Employee.num_emp +=1 9 | def apply_raise(self): 10 | self.pay=self.pay*self.raise_amount 11 | @classmethod 12 | def set_raise(cls,amount): 13 | cls.raise_amount=amount 14 | @classmethod 15 | def form_str(cls,str_emp): 16 | first,last,pay=str_emp.split("-") 17 | return cls(first,last,pay) 18 | @staticmethod 19 | def is_weekend(day): 20 | if day.weekday==5 or day.weekday==6: 21 | return True 22 | else: 23 | return False 24 | def __repr__(self): 25 | return f"Employee({self.first},{self.last},{self.pay})" 26 | def __str__(self): 27 | return f"{self.fullname()} - {self.email}" 28 | def __add__(self,other): 29 | return self.pay+other.pay 30 | def __len__(self): 31 | return len(self.fullname()) 32 | @property 33 | def email(self): 34 | return f"{self.first}{self.last}@gmail.com" 35 | @property 36 | def fullname(self): 37 | return f"{self.first} {self.last}" 38 | @fullname.setter 39 | def fullname(self,name): 40 | first,last=name.split(" ") 41 | self.first=first 42 | self.last=last 43 | @fullname.deleter 44 | def fullname(self): 45 | print("name deleted!") 46 | self.first=None 47 | self.last=None 48 | 49 | class Developer(Employee): 50 | raise_amount=1.10 51 | def __init__(self,first,last,pay,language): 52 | super().__init__(first,last,pay) 53 | self.language=language 54 | 55 | class Manager(Employee): 56 | def __init__(self,first,last,pay,employees=None): 57 | super().__init__(first,last,pay) 58 | if employees==None: 59 | self.employees=[] 60 | else: 61 | self.employees=employees 62 | def add_employee(self,emp): 63 | if emp not in self.employees: 64 | self.employees.append(emp) 65 | def remove_employee(self,emp): 66 | if emp in self.employees: 67 | self.employees.remove(emp) 68 | def print_employee(self): 69 | for emp in self.employees: 70 | print("-->",emp) 71 | 72 | 73 | emp_1=Employee("ahammad","shawki",1000) 74 | emp_2=Employee("cristiano","ronaldo",2000) 75 | str_emp1="sergio-ramos-3000" 76 | emp_3=Employee.form_str(str_emp1) 77 | dev_1=Developer("gerath","bale",4000,"ruby") 78 | dev_2=Developer("keylor","navas",5000,"java") 79 | man_1=Manager("zinedin","zidan",6000,[dev_1,dev_2]) 80 | 81 | print(Employee.num_emp) 82 | print(emp_3.email) 83 | import datetime 84 | a=datetime.date(2018,12,12) 85 | print(Employee.is_weekend(a)) 86 | print(dev_2.email) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Poster](https://github.com/ahammadshawki8/Object-Oriented-Programming-in-Python/blob/master/oop.jpg) 2 | 3 | # _Object oriented approach is one of the best ways of coding_ 4 | 5 | Python is a great programming language that supports OOP. You will use it to define a class with attributes and methods, which you will then call. Python offers a number of benefits compared to other programming languages like Java, C++ or R. It's a dynamic language, with high-level data types. 6 | After learning the stuffs in this repository, coders will have a good command over oop approach of python, they will get femiliar with the basics of OOP and beyond the basics. 7 | 8 | 9 | # _I am grateful to the following materials which helped me to create this repository:_ 10 | 11 | Books: 12 | 1. Learn Python the Hard Way: A Very Simple Introduction to the Terrifyingly Beautiful World of Computers and Code - (Zed Shaw) 13 | https://www.amazon.com/Learn-Python-Hard-Way-Introduction/dp/0321884914 14 | 2. Python Notes for Professionals https://books.goalkicker.com/PythonBook/ 15 | 3. Data Structures and Algorithms in Python - (Michael T. Goodrich, Roberto Tamassia, Michael H. Goldwasser) 16 | https://www.amazon.com/Structures-Algorithms-Python-Michael-Goodrich/dp/1118290275#:~:text=This%20all%2Dnew%20Data%20Structures,that%20is%20clear%20and%20concise. 17 | 18 | Video Tutorials: 19 | 1. Corey Schafer (@CoreyMSchafer) - https://www.youtube.com/user/schafer5 20 | 2. Sentdex (@Sentdex)- https://www.youtube.com/user/sentdex 21 | 22 | # _Licence_ 23 | Details can be found in [LICENSE](https://github.com/ahammadshawki8/Object-Oriented-Programming-in-Python/blob/master/LICENSE) 24 | -------------------------------------------------------------------------------- /off day.py: -------------------------------------------------------------------------------- 1 | # creating a class of my class. 2 | 3 | class Science: 4 | form ="c" 5 | num_student=0 6 | 7 | def __init__(self,name,college_num,fav_sub): 8 | self.name=name 9 | self.college_num=college_num 10 | self.fav_sub=fav_sub 11 | Science.num_student+=1 12 | 13 | def introduce(self): 14 | print(f"Hey! I am {self.name}.My form is {self.form} and college number is {self.college_num}. I like {self.fav_sub} most.") 15 | 16 | def change_sub(self,sub): 17 | self.fav_sub=sub 18 | print("My favourite subject is {} now!".format(self.fav_sub)) 19 | 20 | @classmethod 21 | def change_form(emp,form): 22 | emp.form=form 23 | print("Science C is now become Science {} ".format(emp.form)) 24 | 25 | @staticmethod 26 | def school_day(day): 27 | if day.weekday==5 or day.weekday==6: 28 | return False 29 | else: 30 | return True 31 | 32 | def __add__(self,other): 33 | return self.college_num+other.college_num 34 | def __len__(self): 35 | return len(self.name) 36 | 37 | @property 38 | def print_name(self): 39 | print(self.name) 40 | 41 | @print_name.setter 42 | def print_name(self,name): 43 | self.name=name 44 | print(self.name) 45 | 46 | @print_name.deleter 47 | def print_name(self): 48 | self.name=None 49 | 50 | class Hater_mkj(Science): 51 | def __init__(self,name,college_num,fav_sub,hate): 52 | super().__init__(name,college_num,fav_sub) 53 | self.hate=hate 54 | def prove(self): 55 | if self.hate: 56 | print("MKJ is the worst teacher in the world. Piss on you!") 57 | else: 58 | print("I think MKJ and ME both are foolish :(") 59 | 60 | 61 | student1=Science("Shawki",5130,"Math") 62 | student2=Science("Hasnine",5150,"Chemistry") 63 | student3=Science("Arko",5162,"Math") 64 | student4=Science("Mahidul",5139,"Physics") 65 | student5=Science("Abir",5169,"eating") 66 | student6=Hater_mkj("Anonymus",0000,"not chemistry",False) 67 | 68 | 69 | student1.introduce() 70 | student2.introduce() 71 | student5.introduce() 72 | 73 | print() 74 | 75 | student3.change_sub("Physics") 76 | 77 | print(student1.form) 78 | print(student2.form) 79 | print(Science.form) 80 | 81 | student6.introduce() 82 | student6.prove() 83 | 84 | print(student1+student2) 85 | print(len(student1)) 86 | 87 | student1.print_name 88 | student1.print_name="New_name" 89 | del student1.print_name 90 | student1.print_name -------------------------------------------------------------------------------- /oop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahammadshawki8/Object-Oriented-Programming-in-Python/9a51892a99f9920c20c3abf8342a060af94305e6/oop.jpg -------------------------------------------------------------------------------- /oop01 (classes and objects).py: -------------------------------------------------------------------------------- 1 | # oop1(object oriented programming) 2 | 3 | # there are 3 styles in programming. 4 | # 1. Imperative : doing programming with diffferent loops, if/else statement and general basics. 5 | # 2. Functional : doing programming with pure, higher order, recursive and all types of functions. 6 | # 3. Object oriented: doing programming with classes and objects. 7 | 8 | # OUTSIDE OF THE TOPIC: 9 | # pure function is the type of function which didnt effect any out code of the function. exp: adding calculater 10 | # impure function is the type of function which effect any out code of the function. exp:appending to a list 11 | # higher order function is the type of function which either take a function as an arguement or return 12 | # a function as their return value is called a higher order function. exp:map,filter 13 | 14 | 15 | # classes and objects 16 | 17 | # what is an object? 18 | # an object is basically a collection of properties which can be expressed as variables and some functions. 19 | # we need to put the object in a variable. 20 | # that variable will contain whole set of properties of the object. 21 | # object contains data which are unique to each other. 22 | 23 | # what is attributes? 24 | # the variables within the object are called either "Instance variables" or "attributes". 25 | 26 | # what is method? 27 | # the functions within the object are called methods. 28 | 29 | # what is a class? 30 | # a class is basically like a blue print from which we can make objects. 31 | # a class doesn't refer to any perticular object. 32 | # we need to give a suitable name to a class. 33 | # the first letter of the name must be "capitalize". 34 | 35 | # creating a class. 36 | class Employee: 37 | pass 38 | # here we write the pass method because we are not going to work with it right now. 39 | 40 | # creating a object. 41 | emp1 = Employee() # this simply says create a new object with the class "Employee". 42 | # here we are using default python constructer for this class "Employee". 43 | 44 | # setting attributes or instances(variables) to an object. 45 | emp1.first="ahammad" 46 | emp1.last="shawki" 47 | emp1.pay=1000 48 | # here each arrtibutes are unique to other. 49 | 50 | # we can now create a new object in this class too 51 | emp2=Employee() 52 | emp2.first="christiano" 53 | emp2.last="ronaldo" 54 | emp2.pay=2000 55 | 56 | # printing from class 57 | print(emp1.pay) 58 | print(emp2.pay) 59 | 60 | # but in this code, we are writing same line in the object again and again. 61 | # that's OK but not great!! 62 | # it is too messy. 63 | # it can give us an attribute error if we mis-spell any attibute incorrectly in the object. 64 | # so, the best way to deal with it is using a constructer. 65 | 66 | # lets create the same class using constructer. 67 | class Employee1: 68 | def __init__(self,first,last,pay):# this is called a constructer.in python to make constructer we must use the keyword "__init__" 69 | self.first=first 70 | self.last=last # in our constructor we are setting the instance variables or attributes. 71 | self.pay=pay 72 | self.email=first +"."+last+"@company.com" 73 | 74 | # when we create methods within a class,they receive the attributes as the first arguement automatically. 75 | # and by convention we should call the attribute "self". 76 | # we can call it whatever we want. But it is a good idea to stick with the conventions. 77 | # so, we need to write "self" as the first arguement of the constructer. 78 | # once we have a constructer in our class the default constructer will stop working. 79 | 80 | # now we can pass value in our constructor(outside the class) 81 | emp3=Employee1("zinedin","zidan",3000) 82 | emp4=Employee1("sergio","ramos",4000) 83 | # in our object, attributes pass automatically. 84 | # so we don't have to write any self argument. 85 | 86 | print(emp3.pay) 87 | print(emp4.pay) 88 | 89 | 90 | """Here what is happening behind the scene? 91 | self is a keyword. 92 | when we built constructers and an object like this one- 93 | python thinks that, 94 | self=emp1 95 | so "self.first" refers to"emp1.first" 96 | """ 97 | 98 | # if we want to print the full name of an employee. 99 | # we can do it manually outside our class. 100 | print(f"{emp3.first},{emp3.last}") 101 | 102 | # but let ignore this bunch of code and go to our class and make a method which will handle this task easily. 103 | class Employee2: 104 | def __init__(self,first,last,pay): 105 | self.first=first 106 | self.last=last 107 | self.pay=pay 108 | self.email=first +"."+last+"@company.com" 109 | def fullname(self): # here note that, we need to add "self" arguement to every method we want to add in the class. 110 | return f"{self.first} {self.last}" 111 | 112 | emp5=Employee2("zinedin","zidan",3000) 113 | emp6=Employee2("sergio","ramos",4000) 114 | 115 | print(emp5.fullname()) # remember we have to use brackets after fullname. Because it is a method not an attribute. 116 | #or- 117 | print(Employee2.fullname(emp6)) -------------------------------------------------------------------------------- /oop02 (class variables).py: -------------------------------------------------------------------------------- 1 | # oop2 2 | # class variables 3 | 4 | # what is class variables? 5 | # class variables are variables that are shared among the attributes of a class. 6 | # while instance variables are unique to each other, class variable should be the same for each attributes. 7 | 8 | class Employee: 9 | def __init__(self,first,last,pay): 10 | self.first=first 11 | self.last=last 12 | self.pay=pay 13 | def fullname(self): 14 | return f"{self.first} {self.last}" 15 | def apply_raise(self): 16 | self.pay=int(self.pay*1.04) 17 | emp1=Employee("ahammad","shawki",200) 18 | emp2=Employee("cristiano","ronaldo",400) 19 | 20 | print(emp1.pay) 21 | emp1.apply_raise() 22 | print(emp1.pay) 23 | 24 | # in this code, we are calculating the raise amout of employee. 25 | # it is good but not great 26 | # if we want to change the amount, we have to change it manually several times in several places. 27 | # we can rather use a class variable. 28 | 29 | class Employee2: 30 | raise_amount=1.04 # here we are setting our class variable. 31 | 32 | def __init__(self,first,last,pay): 33 | self.first=first 34 | self.last=last 35 | self.pay=pay 36 | def fullname(self): 37 | return f"{self.first} {self.last}" 38 | def apply_raise(self): 39 | self.pay=int(self.pay*self.raise_amount)# in order to access our class variable, we need to write self before the variable name. 40 | #or self.pay=int(self.pay*Employee.raise_amount)# we can also write the class name before the variable name. 41 | # but this two cases have difference? 42 | 43 | emp1=Employee2("ahammad","shawki",200) 44 | emp2=Employee2("cristiano","ronaldo",400) 45 | 46 | print(emp2.pay) 47 | emp2.apply_raise()#applying the function on emp2 48 | print(emp2.pay) 49 | 50 | # here to know actually what is going on, we are writing some additional code 51 | print(Employee2.raise_amount) 52 | print(emp1.raise_amount) 53 | print(emp2.raise_amount) 54 | # we can access the class variable from both the class and the attributes 55 | 56 | # how to print the emp1 info in a dictionary 57 | print(emp1.__dict__) 58 | # there is no raise_amount in the list. 59 | 60 | # now we print out the Employee2 class. 61 | print(Employee2.__dict__) 62 | # this class contains the raise_amount attribute. 63 | 64 | # when we running the code in line 47, python first search to access the raise_amount from the object. 65 | # if it doesnt find any attribute, 66 | # it will access the raise_amount from the Employee2 class. 67 | 68 | # When we update the raise_amount outside/of our class, the raise_amount will automatically updated in all attributes. 69 | Employee2.raise_amount=1.05 70 | print(Employee2.raise_amount) 71 | print(emp1.raise_amount) 72 | print(emp2.raise_amount) 73 | print("\n\n") 74 | 75 | # but when we change the raise_amount of any attribute, it only changes the raise amount of that specific attribute. 76 | emp2.raise_amount=1.09 77 | print(Employee2.raise_amount) 78 | print(emp1.raise_amount) 79 | print(emp2.raise_amount) 80 | 81 | # why did it do that? 82 | # when we made that assingning it actually created the raise_amount attribute within the emp2. 83 | # so now we have a raise amount arrtibute in emp2. 84 | # it is not longer a class variable for emp2, it is a attribute. 85 | print(emp2.__dict__) 86 | # so if python finds the attribute in the object, 87 | # it wont access it from the class. 88 | 89 | Employee2.raise_amount=1.08 90 | print(Employee2.raise_amount) 91 | print(emp1.raise_amount) 92 | print(emp2.raise_amount) 93 | # here we have again change the class variable from the class, 94 | # but emp2 has raise_amount still 1.09 as it has made its own property(attribute) 95 | 96 | # so we should write "self.raise_amount" rather than "Employee.raise_amount" 97 | # because it gives us flexibility to update our raise_amount in any object later. 98 | # it also allowed any subclass to overwrite that constant if we wanted to. 99 | 100 | # now lets look another example of class variable where it wouldn't really make sense to use self. 101 | # lets say we wanted to keep track how many employee we have. 102 | # so the number of employee should be the same for all our class and object 103 | class Employee3: 104 | numemp=0 # each time we created a new employee it will increase 105 | raise_amount=1.04 106 | 107 | def __init__(self,first,last,pay): 108 | self.first=first 109 | self.last=last 110 | self.pay=pay 111 | Employee3.numemp +=1 # we are going to do that in our constructer. because it runs everytime when we create a new employee. 112 | # here we must use "Employee3" instead of "self" 113 | # because in the previous case we can think about that we may need to overridden the class variable. 114 | # but in this case there's no use case we can think of where we want our total number of employees to be different for any one attribute. 115 | 116 | def fullname(self): 117 | return f"{self.first} {self.last}" 118 | def apply_raise(self): 119 | self.pay=int(self.pay*self.raise_amount) 120 | 121 | print(Employee3.numemp)# it will give us zero because we have not make any object yet. 122 | 123 | emp1=Employee3("ahammad","shawki",200) 124 | emp2=Employee3("cristiano","ronaldo",400) 125 | 126 | # now, we print out the number of employee 127 | print(Employee3.numemp)# it will give us two beceuse it has raised two times. 128 | 129 | # DIFFERENCE BETWEEN ATTRIBUTE AND CLASS VARIABLE 130 | # in attribute we need to write self. 131 | # and we can call them by object. 132 | 133 | # in class variable we can write both class and self. 134 | # we can call by both class and object. 135 | -------------------------------------------------------------------------------- /oop03 (class methods).py: -------------------------------------------------------------------------------- 1 | # oop3 2 | # class methods 3 | 4 | # regular methods in a class automatically pass attribute as a argument as the first arguement. By convention, we call it "self". 5 | # class methods in a class automatically pass class as a arguement as the first arguement. By convention, we call it "cls". 6 | class Employee: 7 | 8 | raise_amount=1.04 9 | numemp=0 10 | def __init__(self,first,last,pay): 11 | self.first=first 12 | self.last=last 13 | self.pay=pay 14 | Employee.numemp +=1 15 | 16 | def fullname(self): 17 | return f"{self.first} {self.last}" 18 | 19 | def apply_raise(self): 20 | self.pay=int(self.pay*self.raise_amount) 21 | # to turn a regular method into a class method, we need to add a decorator to the top called @classmethod. 22 | # this decorator altering the functionality of our method to where now we receive the class to our first arguement. 23 | # by convention, we called the first arguement of our class method "cls" 24 | # we cant use the class as our first argument here, because the word has a different meaning in this language. 25 | @classmethod 26 | def set_raise_amt(cls,amount): 27 | cls.raise_amount=amount 28 | # now within this method we are going to work with class instead of object 29 | 30 | 31 | emp1=Employee("ahammad","shawki",200) 32 | emp2=Employee("cristiano","ronaldo",400) 33 | 34 | Employee.set_raise_amt(1.05)# changing the raise_amount 35 | # this wiil change all raise_amount both class and object. 36 | # this happens because we ran this set_raise_amt method which is a class method which means now we are working with class instead of the object. 37 | # and we are setting that class variable raise amount equal to the amount that we passed in here which is 1.05. 38 | 39 | # what we have done here is the same thing of saying- 40 | Employee.raise_amount=1.05 41 | 42 | print(Employee.raise_amount) 43 | print(emp1.raise_amount) 44 | print(emp2.raise_amount) 45 | 46 | # we can also use class methods as alternative constructors 47 | # it means we can use this class methods in order to provide multiple ways to creating our object. 48 | 49 | 50 | # lets say someone is using our class. 51 | emp_str_1 ="john-doe-700" 52 | emp_str_2 ="steve-smith-800" 53 | emp_str_3 ="sergio-ramos-900" 54 | # we have three strings here that are employees separated by hyphens. 55 | 56 | # if we want to crete new objects with this string we have to first split on the hyphen- 57 | first, last, pay =emp_str_1.split("-") 58 | new_emp1=Employee(first,last,pay) 59 | print(new_emp1.pay) 60 | # but this takes much code and time. 61 | # so lets create an alternative constructer that allows us to pass in the string and we can create the employee. 62 | # so lets create a new class method and we are going to use that method as an alternative constructer. 63 | 64 | class Employee2: 65 | raise_amount=1.04 66 | numemp=0 67 | def __init__(self,first,last,pay): 68 | self.first=first 69 | self.last=last 70 | self.pay=pay 71 | Employee.numemp +=1 72 | def fullname(self): 73 | return f"{self.first} {self.last}" 74 | def apply_raise(self): 75 | self.pay=int(self.pay*self.raise_amount) 76 | 77 | @classmethod 78 | def form_string(cls,emp_str):# here form is a convention. 79 | first, last, pay =emp_str.split("-") 80 | return cls(first,last,pay)# here we are using cls instead of Employee2 because cls and Employee2 are basically the same thing. 81 | 82 | emp_str_1 ="john-doe-700" 83 | emp_str_2 ="steve-smith-800" 84 | emp_str_3 ="sergio-ramos-900" 85 | 86 | new_emp1=Employee2.form_string(emp_str_1) 87 | print(new_emp1.pay) 88 | 89 | # characteristics of class methods: 90 | #1. we need to add decorator @classmethod on the top of the class method. 91 | #2. we need to add cls as the first arguement of the class method. 92 | #3. we should use cls inside the class method. 93 | #4. Outside the class, we should call the class method with the class name. 94 | #5. we can use class method as alternative constructor. 95 | 96 | -------------------------------------------------------------------------------- /oop04 (static methods).py: -------------------------------------------------------------------------------- 1 | # oop4 2 | # static method 3 | 4 | # regular methods in a class automatically pass attribute as a argument as the first arguement. By convention, we call it "self" 5 | # class methods in a class automatically pass class as a arguement as the first arguement. By convention, we call it cls. 6 | # static methods in a class don't pass any arguement automatically. They pass neither the attribute nor the class. 7 | # static function behave just like regular functions except we include them in our class because they have some logical connections with the class. 8 | 9 | class Employee: 10 | num_emp=0 11 | raise_amount=1.04 12 | def __init__(self,first,last,pay): 13 | self.first=first 14 | self.last=last 15 | self.pay=pay 16 | self.email=first+last+"@gmail.com" 17 | Employee.num_emp +=1 18 | def fullname(self): 19 | return f"{self.first} {self.last}" 20 | def apply(self): 21 | self.pay=int(self.pay*self.raise_amount) 22 | @classmethod 23 | def set_raise(cls,amount): 24 | cls.raise_amount=amount 25 | @classmethod 26 | def str_name(cls,emp_str): 27 | first, last, pay = emp_str.split("-") 28 | return cls(first, last ,pay) 29 | 30 | # suppose, we want to know if the day is weekday or not in our class. 31 | # it is logically connected to Employee class. 32 | # but it does not depends on class or attributes. 33 | # so we need to create a static method which tells us it is a weekday or not. 34 | 35 | @staticmethod # to create a static method we need to use this decorator 36 | def workday(day): 37 | if day.weekday()==5 or day.weekday()==6: # in python monday is numbered 0, sunday is numbered 6 and so on. 38 | return False # here day.weekday() function is used to refer to days of the week in the function. 39 | else: 40 | return True 41 | 42 | emp1=Employee("ahammad","shawki",1000) 43 | emp2=Employee("shakil","abrar",2000) 44 | 45 | import datetime 46 | my_date=datetime.date(2016,7,10)# this function makes our date read-able for python. 47 | #or my_date=datetime.datetime.strptime("2016,7,10","%Y,%m,%d").date() 48 | print(Employee.workday(my_date))# we need to write Employee before calling static method. 49 | 50 | # how to be sure that it is a class method or a static method? 51 | # if anywhere in the method we need to use cls variable that is definitely class method. 52 | # if anywhere in the method we need to use self variable that is definitely regular method. 53 | # otherwise, it is a static method. 54 | 55 | # in regular method we need to write self. 56 | # and we can call them by object. 57 | 58 | # in class method we need to write cls. 59 | # and we can call them by class. 60 | 61 | # in static method we need to write nothing. 62 | # and we should call them by class. 63 | -------------------------------------------------------------------------------- /oop05 (inheritance and subclasses).py: -------------------------------------------------------------------------------- 1 | # oop5 2 | # inheritance and subclasses. 3 | 4 | # inheritance allows us to inherit attributes and methods from a parent class. 5 | # this is useful because we can create subclasses and get all the functionalitiey of our parent class, 6 | # and then we can overwrite or add completely new functionality without affecting the parent class. 7 | 8 | # now lets create different types of employees. 9 | # lets say we wanted to create developers and managers. 10 | # here we need to use subclasses. 11 | class Employee: 12 | raise_amount=1.04 13 | 14 | def __init__(self,first,last,pay): 15 | self.first=first 16 | self.last=last 17 | self.pay=pay 18 | self.email=first+last+"@gmail.com" 19 | def fullname(self): 20 | return f"{self.first} {self.last}" 21 | def apply_raise(self): 22 | self.pay=int(self.pay*self.raise_amount) 23 | 24 | # making a subclass 25 | class Developer(Employee):# here we are using the parent class name in the brackets, 26 | pass # to specify which parent class' functionality we want to inherit. 27 | 28 | # here our subclass dont have any code of its own. 29 | # but this subclass will have all the attributes methods of our Employee class. 30 | 31 | dev_1=Employee("ahammad","shawki",200) 32 | dev_2=Employee("cristiano","ronaldo",400) 33 | 34 | print(dev_1.email) 35 | print(dev_2.email) 36 | 37 | # it will also work if we create our object in Developer subclass 38 | dev_1=Developer("ahammad","shawki",200) 39 | dev_2=Developer("cristiano","ronaldo",400) 40 | 41 | print(dev_1.email) 42 | print(dev_2.email) 43 | 44 | # whats happen here? 45 | # when we instantiated our Developers it first looked in our Developers subclass for constructer. 46 | # and its not going to find it in our developer class because it's currently empty. 47 | # so what python is going to do then is walk up a chain of inharitance until it finds what it is looking for. 48 | # this chain called "method resoulution order" 49 | 50 | # we can visulize that by help function. 51 | #print(help(Developer)) 52 | # here we can see that- 53 | """Method resolution order: 54 | Developer 55 | Employee 56 | builtins.object""" 57 | # so when we create a new developer object it first look in our developer subclass for constructer. 58 | # if it didn't find it there then search in the Employee parent class. 59 | # if it didn't find it there also the last place that it would have looked is this bulitins.object class. 60 | # every class in python inherits from this base bulitins.object class. 61 | 62 | # we can also know by help method that our subclass has inherited all the variable and methods from the parent class for free. 63 | 64 | # now we want to customize our subclass. 65 | # and we are going to change the raise_amount for our developers. 66 | # but first lets see what happens when we apply raise_amount function to our current developers. 67 | print(dev_1.pay) 68 | dev_1.apply_raise() 69 | print(dev_1.pay) 70 | # lets say we want our developers to raise amount of 1.10 71 | # we can do this by- 72 | class Developer2(Employee): 73 | raise_amount=1.10 74 | dev_3=Developer2("sergio","ramos",500) 75 | 76 | print(dev_3.pay) 77 | dev_3.apply_raise() 78 | print(dev_3.pay) 79 | 80 | # now python using our developer subclass raise amount instead of parent class raise amount. 81 | 82 | # actually by changing the raise_amount and our subclass dont effect on any of our employee parent class. 83 | # so we can make this kinds of changes of our subclasses without worrying about breaking anything in the parent class. 84 | dev_4=Employee("gerath","bale",800) 85 | 86 | print(dev_4.pay) 87 | dev_4.apply_raise() 88 | print(dev_4.pay) 89 | 90 | # sometimes we need to initiate our subclasses with more information than our parent class can handle. 91 | # lets say we want to add an extra attribute for our developers which is their main programming language. 92 | # but currently our parent class doesn't contain that attribute. 93 | # so to pass an additional attribute for our developers subclass, we need to give the subclass its own constructer. 94 | # so we can do this- 95 | class Developer3(Employee): 96 | def __init__(self,first,last,pay,prog_lang): 97 | super().__init__(first,last,pay)# here we don't need to write all the code like self.pay=pay etc. 98 | self.prog_lang=prog_lang 99 | # instead of doing that we will let our parent class to handle first, last and pay attribute. 100 | # we will let developer to set the prog_lang attribute. 101 | # so in order to let our parentclass to handle previous attribute, we can write- 102 | # super().__init__(first,last,pay) 103 | # here super is a function which allows us to do this. 104 | # in the brackets after init we dont have to write "self" as our first arguement. 105 | # now we have handled the prog_lang just like old tecnic. 106 | 107 | dev_5=Developer3("luca","modrich",300,"ruby") 108 | dev_6=Developer3("neymar","jr.",900,"java") 109 | 110 | print(dev_5.email) 111 | print(dev_5.prog_lang) 112 | 113 | # lets make a new subclass called manager. 114 | class Manager(Employee): 115 | def __init__(self,first,last,pay,employees=None):# here we don't use empty list for our default employees value. 116 | super().__init__(first,last,pay) # because we should not pass any mutable datatype like empty list or dictionary. 117 | if employees is None: # instead of that we use None and do some extra coding to make sure that our code is error free. 118 | self.employees=[] 119 | else: 120 | self.employees=employees 121 | # add a employee 122 | def add_emp(self, emp): 123 | if emp not in self.employees: 124 | self.employees.append(emp) 125 | # remove a employee 126 | def remove_emp(self, emp): 127 | if emp in self.employees: 128 | self.employees.remove(emp) 129 | # print out the fullnames of employees 130 | def print_emp(self): 131 | for emp in self.employees: 132 | print("-->",emp.fullname()) 133 | man_1=Manager("zinedin","zidane",100,[dev_1]) 134 | 135 | print(man_1.email) 136 | man_1.add_emp(dev_2) 137 | man_1.add_emp(dev_3) 138 | man_1.remove_emp(dev_1) 139 | man_1.print_emp() 140 | 141 | # so now we know the importance of subclass? 142 | # => here the code for all of our developers and managers is specific. 143 | # => and they dont create problem with each other. 144 | # => and we can inherit all the properties of parent class to our subclass by a single line of code. 145 | # => so we are really getting reuse our code nicely here if we use subclasses. 146 | 147 | # python has two buit_in function called isinstance and issubclass. 148 | 149 | # is instance will tell us if an object is an instance/object of a class. 150 | print(isinstance(man_1,Manager)) 151 | print(isinstance(man_1,Employee)) 152 | print(isinstance(man_1,Developer)) 153 | # here we need to enter two arguement. 154 | # first one is the instance and the second is the class. 155 | 156 | # is subclass will tell us if it is a subclass of a class. 157 | print(issubclass(Developer,Employee)) 158 | print(issubclass(Manager,Employee)) 159 | print(issubclass(Manager,Developer)) 160 | # here we need to enter two arguement. 161 | # first one is the subclass and the second is the parent class. 162 | 163 | # there is also an important function called hasatter(). 164 | # it is used to see if a class or a object has certain properties or not. 165 | # for example, if we want to see if Manager class has the add_emp() method or not, we can- 166 | print(hasattr(Manager,"add_emp")) # NOTE: we have to pass the name of the property in string. 167 | # we can also use this method with object instead of class, 168 | print(hasattr(man_1,"remove_emp")) 169 | 170 | 171 | 172 | # Extra tip: 173 | # types of inheritance: 174 | # single: 175 | # when a inheritance involves one child class and one parent class only. 176 | # multiple: 177 | # when a inheritance involves more than one parent class. 178 | # multilevel: 179 | # the child class acts as a parant class for another parent class. 180 | # hierarchical: 181 | # it involvs multiple hybrid inheritance form the same parent class. it spreads like a tree. 182 | # hybrid: 183 | # it involves more than one type of inheritance. 184 | 185 | # code: 186 | # single: 187 | class Parent: 188 | def func1(self): 189 | print("A function from the parent class") 190 | 191 | class Child(Parent): 192 | def func2(self): 193 | print("A function from the child class") 194 | 195 | # multiple: 196 | class Parent1: 197 | def func3(self): 198 | print("A function from the parent1 class") 199 | 200 | class Parent2: 201 | def func4(self): 202 | print("A function from the parent2 class") 203 | 204 | class Child1(Parent1, Parent2): 205 | def func5(self): 206 | print("A function from the child1 class") 207 | 208 | # multilevel: 209 | class Parent3: 210 | def func6(self): 211 | print("A function from the parent3 class") 212 | 213 | class Child2(Parent3): 214 | def func7(self): 215 | print("A function from the child2 class") 216 | 217 | class Child3(Child2): 218 | def func8(self): 219 | print("A function from the child3 class which is a of child2 class") 220 | 221 | # hierarchical(basic): 222 | class Parent4: 223 | def func6(self): 224 | print("A function from the parent4 class") 225 | 226 | class Child4(Parent4): 227 | def func7(self): 228 | print("A function from the child4 class") 229 | 230 | class Child5(Parent4): 231 | def func8(self): 232 | print("A function from the child5 class") 233 | 234 | # hierarchical(hybrid): 235 | class Parent5: 236 | def func9(self): 237 | pass 238 | 239 | class Child6(Parent5): 240 | def func10(self): 241 | pass 242 | 243 | class Child7(Parent5): 244 | def func11(self): 245 | pass 246 | 247 | class Child8(Child6,Child7): 248 | def func12(self): 249 | pass 250 | -------------------------------------------------------------------------------- /oop06 (special_magic_dunder methods).py: -------------------------------------------------------------------------------- 1 | # oop6 2 | # special/magic/dunder methods 3 | 4 | # there are some methods in python we can use within our classes. 5 | # this methods called special methods or magic methods or dunder methods. 6 | # this special methods allow us to emulate some built_in behaviour within python and its also how we implement operator overloading. 7 | class Employee: 8 | raise_amount=1.05 9 | def __init__(self,first,last,pay): 10 | self.first=first 11 | self.last=last 12 | self.pay=pay 13 | self.email=first+last+"@gmail.com" 14 | def fullname(self): 15 | return f"{self.first} {self.last}" 16 | def apply_raise(self): 17 | self.pay =self.pay*self.raise_amount 18 | 19 | emp_1=Employee("ahammad","shawki",10000) 20 | emp_2=Employee("cristiano","ronaldo",20000) 21 | 22 | # if we print emp_1 , we find some message. 23 | print(emp_1) 24 | # it is nice if we change its behaviour and print out something like user-friendly . 25 | # these special methods are going to allow us to do that. 26 | 27 | # special method are always sorrunded by double underscore(_) 28 | # so for that reason, it also called dunder. 29 | # so dunder init means __init__ 30 | 31 | # so lets look some other common special methods. 32 | class Employee2: 33 | raise_amount=1.05 34 | def __init__(self,first,last,pay): 35 | self.first=first 36 | self.last=last 37 | self.pay=pay 38 | self.email=first+last+"@gmail.com" 39 | def fullname(self): 40 | return f"{self.first} {self.last}" 41 | def apply_raise(self): 42 | self.pay =self.pay*self.raise_amount 43 | def __repr__(self):# repr is meant to be an ambiguous representation of the object and should be used for debugging ang logging and things like that. 44 | return "Employee('{}','{}','{}')".format(self.first,self.last,self.pay) 45 | def __str__(self):# str is meant to be more of a readable representation of an object and is used as a display to the end_user. 46 | return f"{self.fullname()} - {self.email}" 47 | def __add__(self, other): 48 | return self.pay + other.pay 49 | def __len__(self): 50 | return len(self.fullname()) 51 | 52 | emp_3=Employee2("ahammad","shawki",10000) 53 | emp_4=Employee2("cristiano","ronaldo",20000) 54 | 55 | print(emp_3) 56 | # now it returned a string that we specified in the __repr__ method. 57 | # so if we wanted to recreate this we can just copy our output. 58 | # and its the exact same thing that we used to make our object. 59 | 60 | # but when we make our __str__ method then it will access our str special method. 61 | # it is a readable display for end_users. 62 | 63 | # even we can also print dunder repr and str 64 | print(repr(emp_3)) 65 | print(str(emp_3)) 66 | # so whats going on the background is that its directly calling those special methods. 67 | print(emp_3.__repr__()) 68 | print(emp_3.__str__()) 69 | # its is actually the same thing. 70 | 71 | # so this to special methods allow us to how our object will be printed and displayed. 72 | # so print(emp_3) first execute dunder str. 73 | # if there is no dunder str it will run dunder repr. 74 | # if there is no dunder repr it will then run that ugly message. 75 | # so its a good habit thet if we create dunder str, we should create dunder repr in our class too. 76 | 77 | # there are also many magic methods in arithmatic. 78 | print(1+2) 79 | # when we run this code, it will run a dunder add in background. 80 | print(int.__add__(1,2))# here int is a default object. 81 | 82 | # string uses their own dunder add method. 83 | print("a"+"b") 84 | print(str.__add__("a","b")) 85 | 86 | # we can also add the salaries of our employees by dunder add. 87 | # remember that though this methods are available for our code, but thats not available for our class by default. 88 | # so in order to make them available for our class and objects we need to do some additional coding. 89 | # if we do not do so our code will give us error. 90 | print(emp_3+emp_4)# to add this we need to use + instead of "," 91 | 92 | # there also many dunder methods. 93 | # we can find them in this description "https://docs.python.org/3/references/datamodel.html#special-method-names" 94 | 95 | # infact, "len" is a dunder method too. 96 | print(len("shawki")) 97 | print("shawki".__len__()) 98 | # we can apply this to our class. 99 | print(len(emp_3)) 100 | # it is useful when someone writing a document and needs too know how many characters the employees name will take up. 101 | 102 | # so we see that in python all operation have a top level function like len or add. 103 | # and the top level functions are surrounded by __ which allows us to implement those top level functions. 104 | 105 | # lets see some other magic methods. 106 | # __len__ len() 107 | # __add__ + 108 | # __sub__ - 109 | # __mul__ - 110 | # __truediv__ / 111 | # __floordiv__ // 112 | # __mod__ % 113 | # __pow__ ** 114 | # __and__ & 115 | # __xor__ ^ 116 | # __import__ | 117 | # __lt__ < 118 | # __le__ <= 119 | # __eq__ == 120 | # __ne__ != 121 | # __gt__ > 122 | # __ge__ >= 123 | # __getitem__ 124 | # __setitem__ 125 | # __delitem__ 126 | # __iter__ 127 | # __next__ 128 | # __name__ 129 | # __main__ 130 | # __contains__ 131 | # __call__ x() 132 | # __doc__ 133 | # __iadd__ += 134 | # __isub__ -= 135 | # __imul__ *= 136 | # __idiv__ /= 137 | # __imod__ %= 138 | 139 | # we have lots and lots more dunder methods in python. we have said earliar and saying again go to that link. 140 | # "https://docs.python.org/3/references/datamodel.html#special-method-names" 141 | # or we can goolge it. 142 | -------------------------------------------------------------------------------- /oop07 (@property decorator).py: -------------------------------------------------------------------------------- 1 | # opp7 2 | # property decorator 3 | 4 | # property decorator allows us to give our class attributes getter,setter and a deleter functionality. 5 | class Employee: 6 | def __init__(self,first,last,pay): 7 | self.first=first 8 | self.last=last 9 | self.pay=pay 10 | self.email=first+last+"@gmail.com" 11 | def fullname(self): 12 | return f"{self.first} {self.last}" 13 | 14 | emp_1=Employee("ahammad","shawki",10000) 15 | 16 | print(emp_1.first) 17 | print(emp_1.email) 18 | print(emp_1.fullname()) 19 | # here we have printed what we have expected. 20 | # now we are going to update our first name and again do the same actions. 21 | emp_1.first="jim" 22 | print(emp_1.first) 23 | print(emp_1.email) 24 | print(emp_1.fullname()) 25 | # but it prints the previous email which we haven't expected. 26 | # now what should we do in this situation? 27 | # here property decorator is useful. 28 | # it allows us to define a method that we can access it like an attribute. 29 | class Employee2: 30 | def __init__(self,first,last,pay): 31 | self.first=first 32 | self.last=last 33 | self.pay=pay 34 | 35 | def fullname(self): 36 | return f"{self.first} {self.last}" 37 | def email(self):# so right now this method is simmilar to fullname method.so each time we ran it it will access the current first and last name. 38 | return f"{self.first}{self.last}.@gmail.com" 39 | 40 | emp_2=Employee2("ahammad","shawki",10000) 41 | emp_2.first="jim" 42 | print(emp_2.first) 43 | print(emp_2.email())# but now as email is a method so we need to use brackets. 44 | print(emp_2.fullname()) 45 | # so if anyone is using our class they have to change their code also. 46 | # but we dont want to do that. 47 | class Employee3: 48 | def __init__(self,first,last,pay): 49 | self.first=first 50 | self.last=last 51 | self.pay=pay 52 | @property 53 | def fullname(self): 54 | return f"{self.first} {self.last}" 55 | @property # it is a property decorator.it is defining the email method of our class like a attribute. 56 | def email(self): 57 | return f"{self.first}{self.last}.@gmail.com" 58 | 59 | emp_3=Employee3("ahammad","shawki",10000) 60 | emp_3.first="jim" 61 | print(emp_3.first) 62 | print(emp_3.email) 63 | print(emp_3.fullname) 64 | # so in order to accessing email like an attribute. 65 | # we can just add a property decorator above that method. 66 | # we can do this fullname as well. 67 | 68 | class Employee4: 69 | def __init__(self,first,last,pay): 70 | self.first=first 71 | self.last=last 72 | self.pay=pay 73 | @property 74 | def fullname(self): 75 | return f"{self.first} {self.last}" 76 | @property 77 | def email(self): 78 | return f"{self.first}{self.last}.@gmail.com" 79 | 80 | @fullname.setter# it is a setter 81 | def fullname(self,name):# here we need to create another method with the same name. 82 | first,last=name.split(" ") 83 | self.first=first 84 | self.last=last 85 | 86 | 87 | emp_4=Employee4("ahammad","shawki",10000) 88 | emp_4.fullname="jim headings" 89 | print(emp_4.first) 90 | print(emp_4.email) 91 | print(emp_4.fullname) 92 | # here we change our fullname. 93 | # lets say we wanted to change our first, last, email by changing our fullname. 94 | # if we just change this property decorator without doing any additional code it will give us an error 95 | # in order to do that error-free, we are going to use a setter. 96 | # it is another decorator. 97 | # the name we are going to use for our setter is going to be the name of the property. 98 | 99 | # we can also make a deleter in the same way. 100 | # if we want to delete the fullname of our employee we have to run some clean up code. so to do this, 101 | # we are going to do the same action as setter but instead of setter it will going to be deleter. 102 | class Employee5: 103 | def __init__(self,first,last,pay): 104 | self.first=first 105 | self.last=last 106 | self.pay=pay 107 | @property 108 | def fullname(self): 109 | return f"{self.first} {self.last}" 110 | @property 111 | def email(self): 112 | return f"{self.first}{self.last}.@gmail.com" 113 | @fullname.setter 114 | def fullname(self,name): 115 | first,last=name.split(" ") 116 | self.first=first 117 | self.last=last 118 | 119 | @fullname.deleter# it is a deleter 120 | def fullname(self): 121 | print("name deleted!") 122 | self.first =None 123 | self.last =None 124 | 125 | emp_5=Employee5("ahammad","shawki",10000) 126 | emp_5.fullname="jim headings" 127 | print(emp_5.first) 128 | print(emp_5.email) 129 | print(emp_5.fullname) 130 | 131 | # deleter code is useful if we want delete an attribute. 132 | del emp_5.fullname 133 | # when we run this code it set our first and last attribute to none value. 134 | 135 | # property decorator are also use to make an attribute read-only. 136 | # if we there is a method in the class with the same name of an attribute, 137 | # then if we use property decorator and run that, the function will be executed not the attribute. 138 | # so the attribute become an read_only attribute. -------------------------------------------------------------------------------- /oop08 (combining multiple classes).py: -------------------------------------------------------------------------------- 1 | # oop_extra 8 2 | # combining multiple classes and object. 3 | class Robot : 4 | def __init__(self,name,color,weight): 5 | self.name=name 6 | self.color=color 7 | self.weight=weight 8 | 9 | def introduce_self(self): 10 | return "My name is "+ self.name 11 | 12 | r1=Robot("Tom","red",30) 13 | r2=Robot("Jerry","blue",40) 14 | 15 | print(r1.introduce_self()) 16 | print(r2.introduce_self()) 17 | 18 | class Person : 19 | def __init__(self,name,personality,isSitting): 20 | self.name=name 21 | self.personality=personality 22 | self.isSitting=isSitting 23 | 24 | def sit_down(self):# when we run this method to any object the is sitting value will be true. 25 | self.isSitting=True 26 | def stand_up(self):# when we run this method to any object the is sitting value will be false. 27 | self.isSitting=False 28 | 29 | p1=Person("Shawki","Intelligent",False) 30 | p2=Person("Sowad","talkative",True) 31 | 32 | # if p1 owns r2 and p2 owns r1 33 | p1.robotOwened=r2 34 | p2.robotOwened=r1 35 | 36 | # now we can access this robotOwned atrribute in p1/p2 object. 37 | print(p1.robotOwened.introduce_self()) 38 | print(p2.robotOwened.introduce_self()) -------------------------------------------------------------------------------- /oop09 (5 important tips and tricks for oop).py: -------------------------------------------------------------------------------- 1 | # Tip 1 2 | # multiple inheritance 3 | class A: 4 | def intro(self): 5 | print(" I am from class A") 6 | 7 | class B: 8 | def intro(self): 9 | print(" I am from class B") 10 | 11 | # we can create a subclass and inherit from multiple parentclass too. 12 | class C(A,B): 13 | pass 14 | 15 | # now if we create a object of C class and apply the intro function to it. 16 | object_C=C() 17 | object_C.intro() 18 | 19 | # we can see that it is executing the class A intro() function. 20 | # python always follow a order for execution of differnt functions. 21 | # we can see the order by mro() function. 22 | print(C.mro()) 23 | 24 | # we can see that the order is 25 | #[, , , ] 26 | # so it first search C, then A, then B and after that object. 27 | 28 | # what is MRO? 29 | # Method Resolution Order (MRO) is the order in which Python looks for a method in a hierarchy of classes. 30 | # Especially it plays vital role in the context of multiple inheritance as single method may be found in multiple super classes. 31 | 32 | # super() method 33 | # we have already seen a use case of super() method. we have use this to inherit the constructor from the parent class. 34 | # actually, super() is used in the subclass for calling a function which is situated in the parent class. 35 | class G: 36 | def num(self): 37 | print(1) 38 | class H(G): 39 | def num(self): 40 | print(0) 41 | super().num()# this num() is situated in the parent class. 42 | 43 | object_H=H() 44 | object_H.num() 45 | 46 | # NOTE: in the previous example of super() method we have changed the functionality of a method of parent class in the child class. 47 | # in programming, this is called "Method Over-riding" simmilar to the operator overloading. 48 | 49 | # tip 3 50 | # operator overloading 51 | # we see how differnt operators work in python. they are nothing but dunder methods. 52 | # we can actually change the characteristics in python. 53 | # it is called operator overloading. 54 | class Myoperats: 55 | def __init__(self,value): 56 | self.value=value 57 | def __add__(self,other): 58 | return (self.value **2) + (other.value**2) 59 | 60 | num1=Myoperats(5) 61 | num2=Myoperats(12) 62 | print(num1+num2) 63 | # here we have change what our + operator actually does. 64 | 65 | # tip 4 66 | # object life cycle 67 | # object have three periods in their life in the memory. 68 | # they are Creation, Manipulation and Destruction. 69 | 70 | # when we create a object by a class, __new__ and __init__ method start working. 71 | # and other part of the code can use the object and manipulate it. 72 | # we can destroy the object after we used it. this is called "garbage collection". 73 | # when "garbage collection" is done the memory that the object used become free and we can use it for another reason. 74 | # python automatically done the "garbage collection" after we use the object. 75 | 76 | # so we can now make an ideal defination of "garbage collection" 77 | # The process by which Python periodically frees and reclaims blocks of memory 78 | # that no longer are in use is called Garbage Collection. 79 | # python's garbage collector runs during program execution and is triggered 80 | # when an object's reference count reaches zero. 81 | 82 | # what is reference count? 83 | # when we code, an onject can be linked with multiple objects. 84 | # the number of objects that an object is linked is called its reference count. 85 | # lets see an example. 86 | a=42 # a is linked to 42. so its reference count is 1 87 | b=a # a is linked to b. so its reference count is 2 88 | c=[1,2,3] 89 | c[0]=a # a is linked to c. so its reference count is 3 90 | 91 | # now we can manually decrase the reference count by del method. 92 | del a # a is not linked to 42. so its reference count is 2 93 | b=98 # a is not linked to b. so its reference count is 1 94 | c[0]=54 # a is not linked to c. so its reference count is 0 95 | 96 | # when a reference count become 0. it no longer any use for us. So, python delete the object a from the memory. 97 | # that is what "garbage collection is" 98 | 99 | # tip 5 100 | # data hiding 101 | 102 | # oop has 4 important concepts 103 | # they are encapsulation, inheritance, polymorphism, and abstraction. 104 | 105 | # encapsulation means a technic where we combine some variables and functions and express them as a single unit. 106 | # this concepts makes a barrier between the variables of different classes. 107 | # we can only grab the variables by excuting some functions of the same class. 108 | # this is called "data hiding". 109 | # in other words, data hiding means hiding the implimentation details of a class. 110 | 111 | # in other programming language, we can use keywords(access modifiers) to make class attribute and methods private or protected. 112 | # but in python, things are little different. they said, we are all consenting adults here. 113 | # it means it is not advisable to keep any class's element protected from outside's access. 114 | # so python dont have any real method for data hiding. 115 | # instead of that, python advise not to access important implementation details from outside. 116 | 117 | # weakly private 118 | # when we use underscore(_) before an attribute or methods name, we are saying that they are weakly private. 119 | # we can use them outside of our class but we should not access them outside of our class. 120 | class Mlist: 121 | def __init__(self,*contents): 122 | self._hidden=[*contents] 123 | def add(self,*contentsr): 124 | self._hidden.extend([*contentsr]) 125 | def _show(self): 126 | print(self._hidden) 127 | 128 | l1=Mlist(1,2,3,4,5,6) 129 | l1.add(7,8,9) 130 | l1._show() 131 | 132 | # note that: if we import a module having weakly private attributes and methods, then they are not imported. 133 | # even if we use from module_name import * 134 | # so they remain private. 135 | 136 | # strongly private 137 | # when we use double underscore(__) before an attribute or methods name, we are saying that they are strongly private. 138 | # python change their name little bit. so we cant access them from the outside. 139 | 140 | # mainly, it doesn't done for resisting the access. 141 | # it is done for if we have any other attribute in the subclass with the same name, so that it wont confilct with that. 142 | class Make: 143 | __cake=10 144 | def print_num(self): 145 | print(self.__cake) 146 | 147 | a=Make() 148 | a.print_num() 149 | # print(a.__cake) 150 | # we cant run this line. it would say that, Make has no attribute __cake. 151 | # but we can still access the attribute like this. _classname__attributename 152 | print(a._Make__cake) 153 | # actually, python make the change to its name. 154 | 155 | # why is encapsulation and data hiding important? 156 | # it is important because it gives one programmer freedom to omplement the details of the component, 157 | # without concern that other programmer will be writing code that intricately depends on those internal decisions. 158 | 159 | # it means programmers wont have to worried about that someone will change the private properties of that class in future 160 | # so that it become unsuable. 161 | 162 | -------------------------------------------------------------------------------- /oop10 (polymorphism in python).py: -------------------------------------------------------------------------------- 1 | # polymorphism 2 | # it is one of the main principle of oop programming. 3 | 4 | # sometimes a object comes in many types and forms. 5 | # so we can create a method, that will access all types of that object 6 | # and do the same thing regardless what type of the object it is. 7 | # the idea is called polymorphism. 8 | # it means there will be one function but it can behave various ways depending on the type of the input. 9 | 10 | # polymorphism can be achieved by overriding and overloading in python. 11 | 12 | # method overriding: 13 | # it means having two method with the same name but doing different tasks. 14 | # it means one of the methods override the other. 15 | # if there is any method in the parent class and a method with the same name in the child class, 16 | # then if we execute the method, the method of the corresponding class will be executed. 17 | class Parent: 18 | name="Father" 19 | def num(self): 20 | print(1) 21 | 22 | class Child(Parent): 23 | # overriding variable 24 | name="Son" 25 | 26 | # overriding num() method in the child class 27 | def num(self): 28 | print(0) 29 | 30 | object_P=Parent() 31 | print(object_P.name) 32 | object_P.num() 33 | 34 | object_H=Child() 35 | print(object_H.name) 36 | object_H.num() 37 | 38 | # here we are using the same method and variable for 2 different class 39 | # and they are handling each class separately which is polymorphism 40 | 41 | 42 | # overloading 43 | # in python we can define a method in such a way that there are multiple ways to call it. 44 | # if we are given a single method or function, we can specify the number of parameters our self. 45 | class Human: 46 | def hello(self,country=None): 47 | if country is None: 48 | print("Hello") 49 | elif country=="ENGLAND": 50 | print("Hello!") 51 | elif country=="GERMANY": 52 | print("Gutan Tag!") 53 | elif country=="FRANCE": 54 | print("Bonjour!") 55 | elif country=="MEXICO": 56 | print("Ola Amigo!") 57 | elif country=="INDIA": 58 | print("Namaste!") 59 | else: 60 | print("Hey!") 61 | 62 | obj=Human() 63 | obj.hello("MEXICO") 64 | 65 | obj_2=Human() 66 | obj_2.hello("FRANCE") 67 | 68 | # here hello is the same method but it will work differently depending on the arguements. 69 | # we can do polymorphism with this overloading too. 70 | 71 | 72 | # there is another type of overloading which is operator overloading. 73 | 74 | # add(+) operator is the same operator but works differently for different types of input. 75 | # it sum the value of int and float objects 76 | # and it concate string objects 77 | # it extend list objects. 78 | 79 | # we can also create our own class and change the functionality of the add(+) operator. 80 | class Special(): 81 | def __init__(self,value): 82 | self.value=value 83 | def __add__(self,other): 84 | return self.value * other.value 85 | # changing the behaviour of + method (making it *) 86 | 87 | num_1=Special(100) 88 | num_2=Special(20) 89 | 90 | print(num_1+num_2) 91 | print(67+89) 92 | print("Good "+"Boy") 93 | # here + is the same operator but it returns different values for different types of objects. 94 | # so it is a example of operator overloading. 95 | -------------------------------------------------------------------------------- /oop11 (abstraction in python).py: -------------------------------------------------------------------------------- 1 | # abstraction in python 2 | # it is one of the main principles in oop 3 | 4 | # DEFINITION: 5 | # abstraction is the act of removing elements of specificity to emphasize commonality. 6 | # it is the process of describing things using only the important details for the task at hand. 7 | # in computer context, this involvs only providing attributes and methods to an object 8 | # that are useful for that perticular task only. 9 | 10 | 11 | # Abstract Class 12 | # abstract classes are the classes that contains abstract methods. 13 | # an abstract method is a method that is declared, but contains no implementation. 14 | # abstract classes cannot be instantiated it means we cannot create objects of the abstract class. 15 | # and requires subclasses to provide implementations for the abstract methods (we can create objects of subclass) 16 | # Subclasses of an abstract class in python are not required to implement abstract methods of the parent class. 17 | 18 | # we want to make an abstract class. 19 | # in python there is a pre-defined abstract class called ABC(abstract base class). 20 | # we have to import ABC class from abc module. 21 | from abc import ABC, abstractmethod 22 | 23 | # here A is an abstract class 24 | class Configure(ABC): 25 | # lets create an abstract method. we have to use the @abstractmethod decorator. 26 | # we have to import it from abc module 27 | @abstractmethod 28 | def display(self): 29 | pass 30 | # so abstract method has declaration but no implementation. 31 | 32 | # if we want to implement this abstract method we need to create a subclass. 33 | class Mechanic(Configure): 34 | # implementing the abstract method by overriding 35 | def display(self): 36 | print("this is display method for mechnic") 37 | # we can call the abstract method from parent class by super() method. 38 | super().display() 39 | # generally we dont have any implementation in abstract methods. but if we have any then we can use that. 40 | 41 | 42 | # we have to create object of the child class. 43 | m=Mechanic() 44 | m.display() 45 | 46 | 47 | 48 | # what if we have two different abstract methods in the parent calss and only one overridden abstract method in the child class? 49 | 50 | class Configure2(ABC): 51 | @abstractmethod 52 | def engine(self): 53 | pass 54 | 55 | @abstractmethod 56 | def color(self): 57 | pass 58 | 59 | class Mechanic2(Configure2): 60 | def engine(self): 61 | print("this is engine method for mechnic") 62 | 63 | # def color(self): 64 | # pass 65 | 66 | # here we have two abstract methods in parent class 67 | # and we only have implemented one of the methods in the subclass. 68 | # now can we create an object for the subclass? 69 | # m2=Mechanic2() 70 | # m2.engine() 71 | # we can see that we cant do it because there is another abstract method remaining 72 | # which our subclass inherit from the parent class and we havent implement that. 73 | # so our Mechanic2() class is also a abstract class 74 | 75 | # how to solve this problem? 76 | # there is several ways we can do this. 77 | # method 1: we can implement the remaining function in the subclass. 78 | # method 2: create another subclass of the subclass Mechanic2() and implement the remaining function there. 79 | 80 | class Sub_Mechanic2(Mechanic2): 81 | def color(self): 82 | print("this is abstract color method form a subclass of mechanic2") 83 | super().color() 84 | 85 | # create the object of the Sub_Mechanic2() class. 86 | m3=Sub_Mechanic2() 87 | m3.engine() 88 | m3.color() 89 | 90 | 91 | 92 | # we can also create a constructor in the abstract class. 93 | class Cal(ABC): 94 | def __init__(self,value): 95 | self.value=value 96 | # NOTE: here value is a local variable for the __init__ constructure. 97 | # when we use self.value it becomes class variable for the entire class. 98 | 99 | @abstractmethod 100 | def add(self): 101 | pass 102 | 103 | @abstractmethod 104 | def sub(self): 105 | pass 106 | 107 | class C(Cal): 108 | def __init__(self,value): 109 | super().__init__(value) 110 | 111 | def add(self): 112 | return self.value+100 113 | def sub(self): 114 | return self.value-10 115 | 116 | obj=C(100) 117 | print(obj.add()) 118 | print(obj.sub()) 119 | 120 | # Why should we use abstract class? 121 | # we should use abstraction when we are designing an application in a oop way. 122 | # if we create an abstract class, the subclasses will have some restrictions to which method they musst have to defined. 123 | # for example, in the previous example our Mechanic2 class must have 2 methods which are color() and engine() 124 | -------------------------------------------------------------------------------- /oop12 (Inner class).py: -------------------------------------------------------------------------------- 1 | # inner class 2 | 3 | # we have seen that a class can contain variables and methods. 4 | # but can a class contain another class? 5 | # Interestingly, yes. 6 | 7 | # lets see an example: 8 | class Student: 9 | 10 | def __init__(self,name,roll): 11 | self._name=name 12 | self._roll=roll 13 | 14 | # creating Laptop object inside the outer class 15 | 16 | #self.lap=self.Laptop(brand,cpu,ram) 17 | # we have to use self here as it in the class. 18 | # we can do this if we pass the attributes when creating a student class. 19 | 20 | def show(self): 21 | print(f"STUDENT INFORMATION \n\t name : {self._name} \n\t roll : {self._roll}") 22 | 23 | # students also have a laptop, so we want a laptop attribute in this class. 24 | # but the problem is when we talk about laptop, 25 | # it has different properties and configaration. 26 | # we can add those properties one by one as an attribute but that isn't great. 27 | # we can instead create a class and use that. 28 | 29 | class Laptop: 30 | def __init__(self,brand,cpu,ram): 31 | self.brand=brand 32 | self.cpu=cpu 33 | self.ram=ram 34 | 35 | def show(self): 36 | print(f"LAPTOP CONFIGURATION \n\t brand : {self.brand} \n\t cpu : {self.cpu} \n\t ram : {self.ram}") 37 | 38 | 39 | # to create the object of the inner laptop class in the outer class, we can do that in the __init__ method. 40 | # or we can directly create an object of Laptop class outside of the outer class. 41 | 42 | s1=Student("Shawki",5130) 43 | s2=Student("Arko",5162) 44 | 45 | s1.show() 46 | 47 | # creating Laptop object for students. 48 | s1.lap=s1.Laptop("Lenovo","i5",4) 49 | s2.lap=s2.Laptop("HP","i3",2) 50 | # this object will be an attribute of the student object. 51 | 52 | # we can print the attributes of Laptop using, 53 | print(s1.lap.brand) 54 | 55 | # we have two different classes with the same name. but we can access both of them. 56 | print() 57 | s1.show() 58 | print() 59 | s1.lap.show() 60 | 61 | # NOTE: it is not simmilar to inheritance. 62 | # it can reduce potential name conflicts because it allows for a simmilarly named class to exists in another context. 63 | # another advantage is that it allows for a more advanced form of inheritance 64 | # in which a subclass of the outerclass can override the defination of its inner class. 65 | -------------------------------------------------------------------------------- /oop13 (libray & user code).py: -------------------------------------------------------------------------------- 1 | # Libray and User code 2 | # suppose we are working in the user code class and we are inheritting form library code. 3 | # we want a specific method that must include in the parant class of the library module. 4 | # in that case we can use assert statements before creation of the User codes class. 5 | 6 | # Library Code 7 | class Parent: 8 | def first(self): 9 | return "hi" 10 | 11 | # User Code 12 | # from module_name import Parent # actually, library and user code situated in two different module. 13 | assert(hasattr(Parent, "first")),"Must need to have first() function" 14 | class Child(Parent): 15 | def second(self): 16 | return self.first() 17 | 18 | 19 | # now lets say, we are working with the library code and dont change the user code. 20 | # but we want some functions defined in the user code's class so that our library code doesn't crash. 21 | # we can do that in 3 technics. 22 | # 1. __build_class__ method 23 | # 2. meta class 24 | # 3. __init_subclass__ method 25 | 26 | 27 | # __build_class__ method 28 | # this method allows us to hook into and do cool things in the process of building classes. 29 | 30 | # this method is from builtins module 31 | import builtins 32 | 33 | # first grab the old __build_class__ within our interpreter. 34 | old_method = __build_class__ 35 | 36 | # now lets create our own method. 37 | def new_method(*args,**kwargs): 38 | # NOTE : 39 | # here **kwargs are those arguements that we pass in the perenthesis after the class name. 40 | # it is not the attributes and methods in the class body. 41 | print(f"Building class with arguements:{args} and key-word arguements: {kwargs}") 42 | return old_method(*args,**kwargs) 43 | 44 | # now replace the old method with our new one. 45 | builtins.__build_class__=new_method 46 | 47 | # Now if we create a class we can see the magic is happening. 48 | class Boss(): 49 | pass 50 | # we can see that the class is building. 51 | # Building class with arguements:(, 'Boss') and key-word arguements: {} 52 | 53 | 54 | # now we can do some cool stuffs with this. 55 | # first create a class. 56 | class InfrastructureLibary: 57 | def primary(self): 58 | return self.secondary() 59 | 60 | 61 | class User(InfrastructureLibary): 62 | def secondary(self): 63 | return "secondary" 64 | 65 | # suppose we are working with the low-level programming team when we are developing the first class. 66 | # we have no right to change the User class. 67 | # we can see the the User class must need to have the secondary() method unless our code will fail. 68 | # so how to remind the developer of the User class that they must need to include secondary() in there code. 69 | # well we can use the __build_class__ method here. 70 | 71 | import builtins 72 | 73 | old_method2 = __build_class__ 74 | 75 | # here we have changed the *args little bit. 76 | # it is actually separated into the name of the functions, name of the class and its base class. 77 | 78 | def new_method2(function,name,base=None,**kwargs): 79 | if base is InfrastructureLibary: 80 | print("WARNING: You must need to create a secondary() function.") 81 | if base is not None: 82 | return old_method2(function,name,base,**kwargs) 83 | return old_method2(function,name,**kwargs) 84 | 85 | builtins.__build_class__=new_method2 86 | 87 | # now if we create a child class of the InfrastructureLibary class, then we can see that gives us the warning. 88 | class NewUser(InfrastructureLibary): 89 | def secondary(self): 90 | return "secondary" 91 | 92 | # but in the Real world example, developers wont solve the problem like this. 93 | # rather than they use MetaClass. see metaclass.py module for better understanding. 94 | 95 | class Meta(type): 96 | def __new__(self,class_name,base,attrs): 97 | if not "secondary" in attrs: 98 | print("WARNING: You must need to create a secondary() function.") 99 | return super().__new__(self,class_name,base,attrs) 100 | 101 | class InfrastructureLibrary2(): 102 | def primary(self): 103 | return self.secondary() 104 | 105 | class User2(InfrastructureLibrary2,metaclass=Meta): 106 | def secondary(self): 107 | pass 108 | 109 | # But MetaClass is pretty complex topic in python. 110 | # so the developers of python have created a new method called __init_subclass__ 111 | # it allows us to hook into the subclass of any parent class. 112 | # __init_subclass__ runs after the creation of the subclass. 113 | 114 | class Infra: 115 | def __init_subclass__(cls,*args,**kwargs): 116 | super().__init_subclass__(*args,**kwargs) 117 | print("Must Have to include last() method") 118 | print("Creating subclass, name :", cls.__name__) 119 | 120 | def first(self): 121 | return self.last() 122 | 123 | class UserCode(Infra): 124 | def last(self): 125 | return "last" 126 | 127 | # __init_subclass__ has more things to learn. i will learn them when I need them. 128 | 129 | -------------------------------------------------------------------------------- /oop14 (metaclasses1).py: -------------------------------------------------------------------------------- 1 | # meta classes in python 2 | # part 1 3 | 4 | # in this module we will cover meta classes. 5 | # meta classes is a fairly complicated but interesting topic in python. 6 | 7 | # we are going to start from how classes are created, instantiated, the way they work in the lower level of python. 8 | # and then we will learn what metaclasses are, how they actually work and how would we want to use them. 9 | # here we will learn the basics of metaclass. 10 | # if we really want to use these we need to look to the python documentation for beyond the basics. 11 | 12 | # why metaclass? 13 | # there are several things we cannot do with classes. 14 | # meta class allow us to do those things. 15 | 16 | # lets get started. 17 | # lets we create a class inside a function. 18 | def hello(): 19 | class Hi: 20 | pass 21 | 22 | return Hi 23 | # here we wont getting any error. 24 | # it is legal in python to create a class inside a function and return it. 25 | # the reason we can do that in python is that in python classes are objects. 26 | # infact, "EVERYTHING IN PYTHON IS AN OBJECT" NOTE that 27 | 28 | # what it mean to be an object? 29 | # it means they are kind of living things. 30 | # we can interact them within runtime, we can pass in parameters, we can store it, save it, modify it etc. 31 | 32 | # we might say how can be classes are objects? 33 | # we thought that classses create object for us. 34 | # its true. but it doesn't mean that class doesn't an object. 35 | 36 | # so if class is a object, then we might have any higher level class which created that class for us. 37 | # that is a meta class 38 | 39 | # What does meta class actually do? 40 | # a class defines the basic rules for the objects. it defines the attributes, parameters, methods etc. 41 | # whereas a meta class defines the rules for a class. 42 | 43 | # so if we create a class we need the metaclass to create it. 44 | # this happens automatically in python. 45 | 46 | # now lets create another class. 47 | class Test: 48 | pass 49 | 50 | # lets print an object of that class 51 | a = Test() 52 | print(a) 53 | # we can see that it is saying <__main__.Test object at 0x000001FA3B071C70> 54 | 55 | # now lets print the class. 56 | print(Test) 57 | # we can see that it is saying 58 | # we can see that it doesn't tells us that it is an object. but it is an object. 59 | 60 | def func(): 61 | pass 62 | # we can use the type method to see the type of any object. 63 | print(type(func)) 64 | print(type(3)) 65 | print(type("h")) 66 | print(type(a)) # here we are getting the type 67 | # lets see the type of the class as it is a object too. 68 | print(type(Test)) 69 | # we can see that it is printing 70 | # it is pretty awkward. we can see that the type of the class is "type" 71 | # here type is the metaclass. it defines the rules and create the class for us. 72 | 73 | # we can use type to create a class manually. 74 | MyTest=type("MyTest",(),{}) 75 | # this is completely equivalent to the first time when we created the Test class. 76 | 77 | # we can create a object of that class also. 78 | b=MyTest() 79 | print(b) # we can see that it is giving <__main__.MyTest object at 0x000001F599A71DC0> 80 | 81 | # we can also print the class 82 | print(MyTest) 83 | # we can see that this is giving us 84 | 85 | # it is probably the less used technic for creating a class in python. 86 | # so we can create a class using metaclass "type" 87 | # and we need to pass the name of the class, 88 | # the name of the base class in () 89 | # and the name of the methods and attributes in {} 90 | 91 | # so lets see an example of a class using methods and attributes. 92 | 93 | # doing some pre work- 94 | class Parent: 95 | def show(self): 96 | print("hi") 97 | 98 | def func2(self): # since we will add this function to the class, we need to use self 99 | return "Hello" 100 | 101 | # defining the class with all options using type class. 102 | NewTest=type("NewTest",(Parent,),{"x":5,"new_func":func2}) 103 | # note that here we have use comma(,) after the parent class as we want to ensure that as tuple. 104 | 105 | # creating a object of that class. 106 | t=NewTest() 107 | 108 | # using the attributes and methods of t object. 109 | print(t.x) 110 | print(t.new_func()) 111 | print(t.show()) 112 | 113 | # we can also create attributes outside of the class just like the regular class. 114 | t.y=4 115 | print(t.y) 116 | 117 | # So this is how we use type and use metaclass, create class with the metaclass. 118 | # so we have seen that, metaclass is any callable that takes parameters for: 119 | # 1. the class's name 120 | # 2. the class's bases (parent class) 121 | # 3. the class's attributes (variables and methods) 122 | # the type is the default metaclass in python. 123 | 124 | # so this is the basics now we will actually dive into the real usecase of meta classes. 125 | # we will see how to create custom metaclass with the help of default type metaclass. 126 | """go to oop15(metaclasses2).py""" -------------------------------------------------------------------------------- /oop15 (metaclasses2).py: -------------------------------------------------------------------------------- 1 | # meta classes in python 2 | # part 2 3 | 4 | # here we will create our own metaclass which will inherit from the type class. 5 | # this is pretty simmilar thing as the default type metaclass. 6 | # but we will change how the object is constructed so that we understand what can we actually do with metaclass. 7 | # so lets get started 8 | 9 | # creating meta class 10 | class Meta(type): 11 | # we not necessarily have to inherit form type. but in our purpose that will work. 12 | # the reason for that is type does some other operations when creating a class. 13 | # if we dont inherit from type, our class wont be a metaclass, 14 | # it will be general class which use type metaclass to create a class. 15 | # besides, this inheritances will give us the ability of overriding methods of type class 16 | 17 | # now we will define a __new__ method. it executes before the __init__ method. 18 | # so this is always called first when a object is created. 19 | def __new__(cls, class_name, bases, attrs): 20 | # remember that, in type arguement we pass name, class_name, bases and the attributes. 21 | # NOTE: it is not a class method but by convention we pass cls instead of self as we are creating class object. 22 | 23 | # do some more stuffs to override this __name__function: 24 | print(f"Creating Class with {attrs}") 25 | 26 | # creating class in return 27 | return type(class_name, bases, attrs) 28 | # here we can use super().__new__ method instead of type as type is the parent class. 29 | # return super().__new__(cls,class_name,bases,attrs) 30 | # __init__ method changes the values and takes the parameters in. 31 | 32 | # now we will create another class 33 | class Dog(metaclass=Meta): # defining the new Metaclass in parenthesis. 34 | # creating some attribute 35 | x=5 36 | y=8 37 | def func(self): 38 | return "hello" 39 | # now we can see that it prints our message which we defined in the __new__ method. 40 | 41 | # now we know the information, metaclass can be extreamly powerful. 42 | # we can hook into the construction of a class and we can modify the construction. 43 | # lets change all of the attributes we have here into upper case. 44 | 45 | class NewMeta(type): 46 | 47 | def __new__(cls, class_name, bases, attrs): 48 | 49 | new_attrs={} 50 | for key,value in attrs.items(): 51 | if not key.startswith("__"): 52 | new_attrs[key.upper()]=value 53 | else: 54 | new_attrs[key]=value 55 | 56 | print(f"Creating Class with {new_attrs}") 57 | 58 | return type(class_name, bases, new_attrs) 59 | 60 | class Cat(metaclass=NewMeta): 61 | x=5 62 | y=8 63 | def func(self): 64 | return "hello" 65 | 66 | # now lets create an attribute of Cat 67 | c=Cat() 68 | # now lets print out the x attribute of c object. 69 | #print(c.x) 70 | # this will give us an error. but we have defined x attribute in the Cat class. 71 | # well we are getting error because we changed the x attribute to X in the metaclass. 72 | print(c.X) 73 | # same goes for the func() method. 74 | print(c.FUNC()) 75 | 76 | 77 | # now we have undertand metaclass, now we can do whatever we want when working with class. 78 | # we can change the bases, class name, attributes, do certain operations based on the parametes. 79 | 80 | # metaclasses are great for doing complicated things """ inside of the frameworks """. NOTE 81 | # metaclass is nice when we are writting library code and we want user code to be very specific. 82 | # when user codes classes are inheritting form a specific class situated in the library code, 83 | # we can force them or tell them to initialize certain methods in that class so that our code doesn't crash. 84 | # actually, in every example of metaclass we will see that, 85 | # there is a parent class and some subclasses 86 | # and we want to control the behaviours of subclasses without explicitely writing code in them. 87 | 88 | # again there are more complex topics in metaclass. we can see them in the python documentaion. 89 | # we dont need to use them much. we should use them when we are 101% sure about what we are doing. 90 | # they will make our code really hard to understand. 91 | # but understanding these types of expert topics in python will really help us to code better, 92 | # because then we can become sure what is going on the background. 93 | -------------------------------------------------------------------------------- /oop16 (metaclasses3).py: -------------------------------------------------------------------------------- 1 | # meta classes in python 2 | # part 3 3 | # problem solving with metaclass. 4 | 5 | # Problem: inherited docstrings aren't perticularly informative in python. 6 | 7 | # here we have two simple class (A and B) and B is the subclass of A 8 | class A: 9 | def func(self): 10 | """Doc_String of class A""" 11 | pass 12 | 13 | class B(A): 14 | pass 15 | 16 | # printing the doc string 17 | print(A().func.__doc__) 18 | print(B().func.__doc__) # this is doc string of class B. 19 | # but we are getting doc string of A twice. that is not what we expected. 20 | # so we cant knowthat we are getting the function through class A or B 21 | 22 | # More Specificaly, 23 | # the "nose testing" framework prints out the doc strings of test methods as it runs them. 24 | # unfortunately, if we have a test suite class that inherits from another class, 25 | # we wont be able to tell when its running method from parent class vs subclass. 26 | 27 | 28 | # Solution1: 29 | # the simple solution is that just manually include information in the docstrings. 30 | class A2: 31 | def func(self): 32 | """Doc_String of class A2""" 33 | pass 34 | 35 | class B2(A2): 36 | def func(self): 37 | """Doc_String of class B2""" 38 | 39 | print(A2().func.__doc__) 40 | print(B2().func.__doc__) 41 | 42 | # but there will be lot of work if we have lots of subclasses or methods. 43 | # so there might be a better solution. 44 | 45 | # Solution2: 46 | # we can manually change the docstring in the __init__ method. 47 | class A3: 48 | def __init__(self): 49 | old_doc=self.func.__doc__ 50 | cls_name=type(self).__name__ 51 | # changing the docstring 52 | #self.func.__doc__= str(old_doc) + str(cls_name) 53 | # but we are getting an error because doc string of method object is not writable. 54 | # NOTE: function docstrings in general are writable but methods docstring aren't. 55 | 56 | 57 | def func(self): 58 | """Doc_String of class """ 59 | pass 60 | 61 | class B3(A3): 62 | pass 63 | 64 | print(A3().func.__doc__) 65 | print(B3().func.__doc__) 66 | 67 | # so if there is any way that we can change the doc string before function becomes a method? 68 | # yes. but before that we have to step back. 69 | 70 | # what are classes? --> a class is a special kind of object which create other objects called instances. 71 | # type() method tells us the class of an instance. so if we look for the type of a class. 72 | print(type(A3)) # we can see that the type of a class is "type" it means class is a object of type class. 73 | 74 | # the type object actually do 3 different things in python. 75 | # 1. It denotes the type of an object (the types of classes, specifically). 76 | # 2. It tells us what type an object is. 77 | # 3. It can create new classes. 78 | 79 | # creating class with type 80 | def hello(self): 81 | return "hello" 82 | 83 | class_name="MyClass" 84 | bases=(A3,) 85 | attrs={"hello":hello} 86 | 87 | MyClass=type(class_name,bases,attrs) 88 | 89 | # Now, lets try to solve our problem again. 90 | # Solution 3: 91 | 92 | def make_class(name, bases, attrs): 93 | for f in attrs: 94 | attrs[f].__doc__=f"{attrs[f].__doc__} {name}" 95 | 96 | cls = type(name,bases,attrs) 97 | return cls 98 | 99 | def func(self): 100 | """Doc_String of class""" 101 | pass 102 | 103 | # creating class with make_class 104 | A4=make_class("A4",(object,),{"func":func}) 105 | print(A4().func.__doc__) # it prints Doc_String of class A4 106 | 107 | B4=make_class("B4",(A4,),{"func":func}) 108 | print(B4().func.__doc__) # it prints Doc_String of class A4 B4 109 | # but thats not we wanted. why this happend? 110 | 111 | # the answer is that both of these classes A4 and B4 are using same method in the memory. 112 | # so python modified the doctrings of the same object(function) in the memory. 113 | # rather than having two separate function in A4 and B4, they point to the same function. 114 | print(A().func.__doc__ is B().func.__doc__) # it will return True. 115 | 116 | # so how can we solve this problem. well we can also create a function on fly using the func_type() class. 117 | # so lets create a function that copy a function. 118 | def copy_function(f): 119 | func_type=type(f) 120 | new_func=func_type( 121 | f.__code__, # bytecode 122 | f.__globals__, # global namespace 123 | f.__name__, # function name 124 | f.__defaults__,# default keyword arguements values 125 | f.__closure__) # closure variables 126 | new_func.__doc__=f.__doc__ 127 | return new_func 128 | 129 | # now lets try again to solve our problem 130 | # Solution 4: 131 | def make_class2(name, bases, attrs): 132 | for f in attrs: 133 | new_f = copy_function(attrs[f]) 134 | new_f.__doc__=f"{attrs[f].__doc__} {name}" 135 | attrs[f] = new_f 136 | 137 | cls = type(name,bases,attrs) 138 | return cls 139 | 140 | def func2(self): 141 | """Doc_String of class""" 142 | pass 143 | 144 | A5=make_class2("A5",(object,),{"func":func2}) 145 | print(A5().func.__doc__) 146 | 147 | B5=make_class2("B5",(A5,),{"func":func2}) 148 | print(B5().func.__doc__) 149 | 150 | # hey we solved our problem using metaclass! 151 | # here the make_class function we are using is a meta class. 152 | 153 | 154 | # python use different complex way to create metaclass. 155 | def make_class3(name, bases, attrs): 156 | for f in attrs: 157 | # skipping special methods and non-callable functions since we dont want to mess up with those. 158 | if f.startswith("__") or not hasattr(attrs[f],"__call__"): 159 | continue 160 | 161 | # copy the function, replace the docstring and return the old method. 162 | new_f = copy_function(attrs[f]) 163 | new_f.__doc__=f"{attrs[f].__doc__} {name}" 164 | attrs[f] = new_f 165 | 166 | cls = type(name,bases,attrs) 167 | return cls 168 | 169 | # and then we are going to create a class with class keyword. 170 | # then we can do this by adding metaclass arguement in the parenthesis. 171 | class A6(metaclass=make_class3): 172 | def func3(self): 173 | """doc string of class""" 174 | pass 175 | 176 | class B6(A6, metaclass=make_class3): 177 | pass 178 | 179 | 180 | print(A6().func3.__doc__) 181 | print(B6().func3.__doc__) 182 | 183 | # note that, here we are not having our expected results for subclass B6. 184 | # it means subclasses not actually rewrite the doc strings correctly. 185 | # this is happening because func3() is not passed as an attribute of B6 (it is already an attribute of A6) 186 | # to make this really work we have to go through all the attributes of all the parent classes and copy them, too. 187 | 188 | 189 | # so now we know that meta classes are powerful tool. we can use it for many purposes. 190 | # What exactly can we do with meta classes? 191 | # we see that meta classes intervene on class (not instances) creation. 192 | # this gives us the oppurtunity to modify the class's method before the class is created. 193 | # we can, 194 | # 1. copy each of the functions that will become methods later 195 | # 2. change doc strings or any other properties of this new functions 196 | # 3. create the class using these new functions instead of those which are originally given. 197 | 198 | # we can find more useful real world example where metaclasses are used. 199 | # for example, django use metaclasses to simplify its interface. 200 | # it gives us opportunity to work with complex classes as user 201 | # where all the fancy complex parts are handled by the metaclass. 202 | 203 | -------------------------------------------------------------------------------- /oop17 (__slots__).py: -------------------------------------------------------------------------------- 1 | # __slots__ dunder method in python 2 | 3 | # here we will demonstrate in code why using __slots__ results faster instant attributes access. 4 | # and space in memory become more and more relevent as the number of instant creation grows. 5 | 6 | # first lets create two classes identical in may ways. 7 | class Planet: 8 | def __init__(self,cities): 9 | self.cities = cities 10 | 11 | # creating object 12 | earth = Planet(["Dhaka","DC"]) 13 | 14 | class SlottedPlanet: 15 | # the only difference between this two class it that it has a __slot__ apecial attribute at the top. 16 | __slots__ = ["cities"] # it takes strings and this strings are the name of all attributes 17 | def __init__(self,cities): 18 | self.cities=cities 19 | 20 | # creating object 21 | slotted_earth = SlottedPlanet(["Madrid","Paris"]) 22 | 23 | # lets print those objects. 24 | print(earth) 25 | print(slotted_earth) 26 | 27 | # printing the attributes of those objects. 28 | print(earth.cities) 29 | print(slotted_earth.cities) 30 | 31 | # we can see that there is no difference. 32 | 33 | # now lets add a new attribute in the earth object called country. 34 | earth.country=["Bangladesh","United States"] 35 | # if we try to do the same with the slotted_earth we get an attribute error. 36 | #slotted_earth.country=["Spain","France"] 37 | # the only attributes that this object can have are those which we have provided to the __slots__. 38 | 39 | # so we can even left the __slots__ attribute empty. 40 | class EmptyBin: 41 | __slots__=[] 42 | # creating object 43 | empty_object=EmptyBin() 44 | # we cant create any attribute of that object. 45 | #empty_object.att="new attribute" 46 | 47 | 48 | # why cant we add attributes in a slotted object in the general way? 49 | # we can also store attributes in a general earth object by storing them in the instant dictionary. 50 | # we can access the dictionary by __dict__ method. 51 | print(earth.__dict__) 52 | # but in the slotted_earth we do not have any __dict__ method. 53 | #print(slotted_earth.__dict__) 54 | # this will give us an attribute error. 55 | # so SlottedPlanet class dont even created a dictionary. 56 | 57 | # we know that dictionary even though empty one take up space. 58 | # so since slotted object dont create dictionary we can have a much space in the memory. so it is a lightweight object. 59 | 60 | # we can see the space of any object by the getsizeof() method from the sys module. 61 | import sys 62 | # we can see that this object take 48 bytes space. 63 | print(sys.getsizeof(earth)) 64 | # and its dictionay take 104 bytes space. 65 | print(sys.getsizeof(earth.__dict__)) 66 | # so in total 152 bytes of memory usage. 67 | 68 | # again the slotted_earth also takes only 48 bytes space. 69 | print(sys.getsizeof(slotted_earth)) 70 | # so we can see that the memory space is decrreased though a little portion of it. 71 | # but if we have big data, then it will be a great benefit by releasing much GBs of memory 72 | # and it also can give us a performance boost. 73 | 74 | 75 | # lets see the time it needed to perform operations in a regualar object vs slotted object. 76 | # we have to import timeit module for that. 77 | import timeit 78 | 79 | # creating two classes 80 | class UnSlotted: 81 | pass 82 | 83 | class Slotted: 84 | __slots__=["values"] 85 | 86 | # creating a get_set_delete_func function 87 | def get_set_delete_func(obj): 88 | # creating another function which will 89 | def get_set_del(): 90 | obj.values=[0,1] # set values 91 | obj.values # get values 92 | del [obj.values] # delete values 93 | return get_set_del # returnning the inner function 94 | 95 | # creating object 96 | non_slotted_obj = UnSlotted() 97 | slotted_obj=Slotted() 98 | 99 | # repeating operations multiple time using repeat() function of timeit module 100 | # and printing the minimum process time (in seconds) using min function(). 101 | print(min(timeit.repeat(get_set_delete_func(non_slotted_obj),repeat=5))) 102 | 103 | # doing the same operation with slotted_obj. 104 | # we can see 15% to 20% improvement in time. 105 | print(min(timeit.repeat(get_set_delete_func(slotted_obj),repeat=5))) 106 | 107 | # we can not only use classes having __slots__ attributes for memory and time boost, 108 | # we can also use them in the library code 109 | # and ensure that the user cant define new attributes and methodsin our class 110 | # which can make our class unstable. 111 | # library code 112 | class Library: 113 | __slots__=["attr1","attr2"] 114 | def __init__(self,attr1,attr2): 115 | self.attr1=attr1 116 | self.attr2=attr2 117 | 118 | # user code 119 | user1=Library(0,1) 120 | # we cant create more attribute here. 121 | 122 | # now lets see inheritance of slotted class. 123 | class Computer: 124 | __slots__=["ram"] 125 | 126 | slotted_comp=Computer() 127 | slotted_comp.ram="4 GB" 128 | 129 | # create a subclass of the slotted class 130 | class Laptop(Computer): 131 | pass 132 | 133 | inherited_laptop=Laptop() 134 | # if our subclass dont initiatize __slots__ attribute, 135 | # then every object of our subclass will still have instant dictionary. 136 | print(inherited_laptop.__dict__) 137 | 138 | # printing the size of inherited object 139 | print(sys.getsizeof(inherited_laptop)) 140 | print(sys.getsizeof(inherited_laptop.__dict__)) 141 | 142 | # adding attributes to inheritted object. 143 | inherited_laptop.ram="8 GB" 144 | inherited_laptop.rom="2 TB" 145 | # printing the instant dictionary again. 146 | print(inherited_laptop.__dict__) 147 | # we can see there is newly created rom attribute but no previous ram attribute. 148 | # actually the parant class will continue to be sloted if we created object of a subclass. 149 | # the attribute thst was presious defined in the __slots__ of parent class will store there. 150 | # whereas the newly defined attributes will store in the instant dictionary of that object. 151 | 152 | # lets do that again. but in this time we will use __slots__ attribute in the subclass and set that equal to empty. 153 | class Desktop(Computer): 154 | __slots__=[] 155 | 156 | inherited_desktop=Desktop() 157 | 158 | inherited_desktop.ram="16 GB" 159 | # though the __slots__ is empty, we can store the ram attribute which was previously mentioned in the __slots__ attribute of parent class. 160 | # but we cant add another attribute here it will give us an attribute error. 161 | #inherited_desktop.cpu="i7" 162 | # again, we cant see the __dict__ attribute too as it doesn't have one. 163 | #print(inherited_desktop.__dict__) 164 | 165 | # what if we want to add attributes to class dynamicaly? 166 | # then we can add "__dict__" attribute in the __slots__ attribute. 167 | class Dynamic: 168 | __slots__=("x","__dict__") # we can use both list or tuple to our __slots__ method. but we should use tuple if we want to make it immutable. 169 | 170 | dynamic_slotted=Dynamic() 171 | dynamic_slotted.x=1 172 | dynamic_slotted.y=2 173 | dynamic_slotted.z=3 174 | dynamic_slotted.p=4 175 | # it has a __dict__ method as we mention it earliar in the __slots__ 176 | print(dynamic_slotted.__dict__) 177 | # we can see that the first attribute x not in the __dict__ as we add that separately in the __slots__ 178 | # but "y", "z", "p" attribute are in the __dict__ 179 | 180 | # NOTE: one thing we have to be very careful about that we should not add same attribute in the parent and subclass's __slots__ attribute. 181 | class Parent: 182 | __slots__=("x") 183 | 184 | class Child: 185 | __slots__=("x","y") # x is in the both __slots__ method. 186 | 187 | class NewChild: 188 | __slots__=("y",) 189 | 190 | # creating object 191 | child_slotted=Child() 192 | new_child_slotted=NewChild() 193 | # printing the size of the object. 194 | print(sys.getsizeof(child_slotted)) 195 | print(sys.getsizeof(new_child_slotted)) 196 | 197 | # this wont give us any error. 198 | # but doing this will force our object to take op more space than they need to. 199 | # we can see that it takes more bytes than necessary. 200 | 201 | # NOTE: in a very large codebase, it is important not to use __slots__ unless very much necessary. 202 | # because there can be problem in multiple inheritance. 203 | class ParentA: 204 | __slots__=("x",) 205 | class ParentB: 206 | __slots__=("y",) 207 | 208 | # our Subclass inherits both from ParentA and ParentB 209 | class Subclass(ParentA,ParentB): 210 | pass 211 | # when both parents have a non-empty __slots__, 212 | # then we will have an TypeError saying "TypeError: multiple bases have instance lay-out conflict" 213 | 214 | # to overcome the problem one of the parent has to have a __slots__ defined with an empty list or tuple. 215 | -------------------------------------------------------------------------------- /oop18 (quick tips).py: -------------------------------------------------------------------------------- 1 | # OOP quick tips 2 | 3 | # Tip 1 4 | # printing the children classes of Parent class. 5 | # Parent classes 6 | class Father: 7 | def __init__(self): 8 | value=0 9 | 10 | def update(self): 11 | value+=1 12 | 13 | def renew(self): 14 | value=0 15 | 16 | def show(self): 17 | print(value) 18 | 19 | class Mother: 20 | def __init__(self): 21 | value=1 22 | 23 | def update(self): 24 | value-=1 25 | 26 | def renew(self): 27 | value=0 28 | 29 | def show(self): 30 | print(value) 31 | 32 | 33 | # Children classes 34 | class Child_1(Father): 35 | def update(self): 36 | value+=2 37 | 38 | class Child_2(Mother): 39 | def update(self): 40 | value-=2 41 | 42 | 43 | # the main function. 44 | def interiors(*classx): 45 | subclasses=set() 46 | work=[*classx] 47 | while work: 48 | parent=work.pop() 49 | for child in parent.__subclasses__(): 50 | if child not in subclasses: 51 | subclasses.add(child) 52 | work.append(child) 53 | 54 | return subclasses 55 | 56 | print(interiors(Father,Mother)) 57 | 58 | 59 | 60 | --------------------------------------------------------------------------------