My In Depth Python Comprehension
Intro
In this section, I describe all my knowledge on Python I have managed to gather so far. It contains Python pure architecture, concepts and language-specific properties.
You can navigate to following concepts here:
Python
- Pyhon is the interpreted programming language and its interpreter runs code from top to bottom, from right to left linearly. Almost everything is executable during run-time of code. This is in opostion to Java or C where entire code is complied by the compiler to the machine's bucekt of bits before it runs.
- For example Python's class is simply executable code during runtime:
for _ in range(10) class Base: pass
- The code above definesclass Base
10 times as the for loop goes. - Everything in Python is an object:
def add(x,y): return x + y add # <function add at 0x002F6850>
- Python interpreter can tell us where the function as the object is located in the memeory (output above). - Python supports both Object-oriented Programming (OOP) and Functional Programming (FP):
OOP FP Object creation and manipulation with specific methods. Function can be used as first class object. Object-oriented featured like inheritance, encapsultaion, polymorphism. Supports lambda function characteristic for functional paradigm. - First class object:
- function can be treated like an object,
- function can be passed as an argument to another function.
def square(x): return x*x def my_map(func, lst) result = [] for i in lst: result.append(func(i)) return result squares = my_map(square, [1,2,3,4,5]) squares # [1,4,9,16,25]
✔square
function has been passed to themy_map
unexecuted (without brackets).
✔ It's being executed later inmy_map
's body for every item in the passed list.
- function can be assigned to a variable:sream = print scream("Hello!") # Hello!
- function can be returned by another function:
def outer_func() message = "Hello!" def inner_func(): print(message) return inner_func welcome_func = outer_func() welcome_func() ------------- # output # Hello!
✔outer_func
being executed returnsinner_func
that waits for its execution.
✔inner_func
is assigned to variablewelcome_func
which can be executed as the same as every function.
✔inner_func
is also so-called closure which is every inner function in the body of outer function. Closure remembers and has access to variables in the local scope it was created. Closure has an access to that variable even after the outer function has finished executing. In this case,inner_func
can accessmember
variable in the local scope ofouter_fun
. - Decorators:
- Decorator is a function that takes another functions as an argument, extends its functionality and returns another function without altering the original function's code that was passed in.
def decorator_fun(original_fun): def wrapper_fun(*args,**kwargs): print(f'Wrapper executed this before {original_fun.__name__} runs.') return original_fun(*args,**kwargs) return wrapper_fun def display_info(): print('Displaying basic info') decorated_display_info = decorator_fun(display_info) decorated_display_info() -------------- # output # Wrapper executed this before display_info runs. # Displaying basic info
✔wrapper_fun
is a closure that remembers passed argument todecorator_fun
even thoughdecorator_fun
finishes execution first.
✔ Inside thewrapper_fun
we append additional functionality before running original function.
✔decorator_fun(display_info)
returnswrapper_fun
that is waiting to be executed and remembersdisplay_info
function as a passed argument to the decorator.
✔ Executingdecorated_display_info()
executeswrapper_fun
returiningoriginal_fun
being executed as well. - Pythonic way to use decorator:
@decorator_fun def display_info(): print('Displaying basic info') display_info() ------------- # output # Wrapper executed this before display_info runs. # Displaying basic info
✔@decorator_fun
means decorator applied that extend basicdisplay_info
function.
Namespace
- There is a variable scope hierarchy:
1. Local scope (within a function).
2. Enclosing scope (local scope of outer (enclosing) function).
3. Global scope (top level of a module).
4. Built-in. - Local scope within a function saves us from using and, most important, from overwriting global variables.
- Working with namesapces:
sream = print scream("Hello!")
- Above code definesscream
as alias for the exisiting python'sprint
function.
- Assignment ofscream = print
introduces the identifierscream
into the current namespace with the value being the object that represents built-in function.
- When assignment of an identifier (variable) to a value (object) happens, the definition in being made in the current namespace.
- Python implements namespaces with ist own dictionary that maps each identifier to associated object.
- When top-level assignment - we assign variable to an object in the global scope.
- When assignment within a function - we assign variable to an object in the local scope.
- Example: assignmentx = 5
within a function has no effect on the identifierx
outside. - Namespace resolution:
- Python searches three scopes for a called name:
1. the local (L),
2. then the global (G),
3. the built-in (B)
and stops at the first place the name is found.
- In practice, when we want to access atribute using dot operator syntax, the Python interpreter runs a name resolution process that looks up for a called name, checking:
1. local instance scope,
2. class scope,
3. supercalss within inheritance hierarchy,
4. it keeps seraching, otherwise it raises AttributeError when not found.
Object-oriented programming
- Object is an entity encapsulating data with methods for manipulating the data.
- Object implementation goes with:
- instance dictionary: holds state (data) and points to its class,
- class dictionary: holds functions (methods) for manipulating the data.
- Special method __init__:
- Calling the class during instatiation creates instance that is being referred by the class with a key-word self.
- In fact, __init__ is not a constructor, it is an initializer that takes an instance self and populates it with attributes (data) that __init__ receives. -
class User: def __init__(self, name, surname): self.name = name self.surname = surname self.email = name + '.' + surname + '@myshop.com'
- The goal of instance variable self is to take unique data for the specific instance.
- The goal of __init__ is to populate the instance with that data. - Inheritance:
- ... - Subclass:
- A class that delegates work to another class (parentclass).
- A subclass and its parent-class are just two different dictionaries that contain functions to manipulate the data.
- A sublcass points to ist parent due to flow: instance dictionary - > subclass dictionary -> parent-class dictionary.
- Using subclasses can be used as a techique for code re-use.
- In fact, a sublcass is in charge. It decides what work gets delegated.
Regular methods, class methods, static methods
- Mehtods:
✔ Regular methods within a class take self as the first parameter. Regular methods manipulates object's data.
✔ Class methods take cls as the first parameter. Class methods manipulates class's data. It can also play a role of alternatice constructor (more below).
✔ Static methods takes nor self neither cls parameter. Static methods behaves like a regular function but they are included in a class body becasue of its logical connection to that class.
- Class methods:
class User: def __init__(self, first, last, gold): self.first = first self.last = last self.email = f'{first.lower()}.{last.lower()}@company.com' @classmethod def create_user_from_string(cls, str): first, last, gold = str.split("-") return cls(first, last, int(gold)) user_1 = User('Artur', 'Skrzeta', 1000) user_2 = User.create_user_from_string('Jan-Nowak-2000') user_1.email # artur.skrzeta@company.com user_2.email # jan.nowak@company.com
- objectuser_1
is created using__init__
method that populates object's attributes wiht passed arguments.
- objectuser_2
is created usingcreate_user_from_string
classmethod which is an alternative constructor that can instantiate a class object with proper attributes. - Static methods:
from datetime import date class User: def __init__(self, first, last, gold): self.first = first self.last = last self.gold = gold self.email = f'{first.lower()}.{last.lower()}@game.com' @staticmethod def is_workday(day): if day.weekday() == 5 or day.weekday() == 6: return False return True @property def gold(self): if User.is_workday(date.today()): return self.gold_with_bonus - 100 else: return self.gold_with_bonus - 1000 @gold.setter def gold(self, gold): if User.is_workday(date.today()): self.gold_with_bonus = gold + 100 else: self.gold_with_bonus = gold + 1000 user_1 = User('Artur', 'Skrzeta', 2000) user_1.gold # 2000 user_1.gold_with_bonus # 3000
- Static method connects to the class logic of assigning the bonuses to the user's account.
- When account created on sunday or saturday, it gets 1000 of additional gold.
- When account created on working day, it gets 100 of additional gold.
- The instance's variable ofgold
stays with 2000 as it was initialized with such value.
- However, the instance was created on the weeknd so the variablegold_with_bonus
gets 3000.
Getters and setters
- Getters ('accessors') and setters ('mutators') are used in OOP for aplying the encapsulation principle. Encapsulation means bundling attributes (data within a class) with the methods that operate on them. According to encapsulation principle, the attributes of a class have to be set as private to hide and protect them from the external code.
- Getters an setters simply dictate the way of accessing the class's private data. In short, the getter is used for retrieving the data and the setter for changing the data within a class.
- In Python the encapsulation matterw is solved as following:
- getter: the method which is used for getting a value is decorated with @property,
- setter: the method which has to function as the setter is decorated with @x.setter. - Code:
class Circle(): def __init__(self, radius): self.radius = radius @property def radius(self): return self.diameter / 2 @radius.setter def radius(self, radius): self.diameter = radius * 2 def main(): c = Circle(2) print(c.diameter) print(c.radius) c.diameter = 6 print(c.diameter) print(c.radius) if __name__ == "__main__": main() --------------------- # output # 4 # 2 # 6 # 3
-@radius.setter
setsself.diameter
with valueradius * 2
while object creation.
- To prove setter's work, I can acccessdiameter
attribute right away.
- With@property
I accessdiameter
attribute but only the wayradius
method allows.
- However, I don't have access toself.radius
directly as it's taken over by@property
getter.
-self.radius
is accepted only during initialization butdiameter
is being stored within a class or instances.
-@property
and@radius.setter
encapsulatesself.radius
and makesdiameter
attribute accessible for external code. - Going with decorators and getting rid of getters and setters traditional methods makes a class shorter and running faster. This is perfect solution for dynamic languages, however not allowed in a compiled language.
- Here comes another example that with
@property
decorator we can access method as an attribute:class Employee: def __init__(self, first, last): self.first = first self.last = last @property def company_email(self): return f'{self.first.lower()}.{self.last.lower()}@company.com' @property def full_name(self): return f'{self.first} {self.last}' @full_name.setter def full_name(self, name): first, last = name.split(' ') self.first = first self.last = last e = Employee('John', 'Smith') e.full_name # John Smith e.company_email # john.smith@company.com e.full_name = 'Johnny Smith' e.first # Johnny e.company_email # johnny.smith@company.com
- Functioncompany_email
decorated with@property
adds a special line of code to return a string in a proper form.
- We can access returned value ofcompany_email
just like every instance's attributes.
- With this line of code:e.full_name = 'Johnny Smith'
we make use of the setter that establishself.first
andself.last
due to the logic it includes.
- When we setfull_name
with setter, this variable cannot be put in the__init__
asself.full_name = ""
orself.full_name = f'{first} {last}'
.
Easier to Ask Forgiveness than Permission (EAFP)
- When you ask for a permission then you have to access object multiple tims as opposed to asking for forgiveness where you access object once.
- Let's take two classes on which we will be asking first for permission and then for forgiveness:
class Robot: def talks(self): print("I'm talking.") def walk(self): print("I'm walking.") class Duck: def quack(self): print("Quack, quack!") def fly(self): print("Flap, flap!")
- At first we will be asking for permission with if statements. We call it 'Look Before You Leap (LBYL)':
def quack_and_fly(thing): if isinstance(thing,Duck): thing.quack() thing.fly() else: print("This has to be a duck.") d = Duck() quack_and_fly(d) r = Robot() quack_and_fly(r) ---------- # output # Quack, quack! # Flap, flap! # This has to be a duck.
✔ We can see that objectd
is an insatnce ofDuck
type so we get the permission to execute Duck's methods on the objet.
✔ We can see that objectr
is not an instance ofDuck
type so we don't get the permission to execute Duck's methods on it.
✔ Notice that we have to access an object each time to check its type, and once we get the permission then we access the same object one more time. - Let's try to ask for forgiveness with try except block:
def quack_and_fly(thing): try: thing.quack() thing.fly() thing.walk() thing.bark() except AttributeError as e: print(e) d = Duck() quack_and_fly(d) r = Robot() quack_and_fly(r) ------------ # output # Quack, quack! # Flap, flap! #'Duck' object has no attribute 'walk' # 'Robot' object has no attribute 'quack'
✔ The main assumption of EAFP is trying to execute the code to see what happens: if it works - great, if not, then raise an error.
✔ This way we accessing object only once, when trying to retieve attribute on this. - One more comparison EAFP vs LBYL:
person = {'name':'Json', 'age':23, 'job':'Software Developer'} # LBYL if 'name' in person and 'age' in person and 'job' in person: print(f"I'm {person['name']}, I'm {person['age']} and I work as {person['job']}") else: print("Missing some data!") # EAFP try: print(f"I'm {person['name']}, I'm {person['age']} and I work as {person['job']}") except KeyError as e: print(f"Missing {e} key")
Features
App includes following features:
Demo
Varaiables name objects in the memory
Naming different objects:
- Naming lists:
list_1 = [1,2,3] list_2 = [1,2,3] id(list_1) == id(list_2) # False
-list_1
andlist_2
variables point to two different objects even though both list has the same values and they are in the same sizes. - Naming strings:
str_1 = "Artur" str_2 = "Artur" id(str_1) == id(str_2) # True
-str_1
andstr_2
variables point to the same object in the memory.
- They are two different tags for the same object.
is operator:
- Returns TRUE if two variables point to the same object:
a = 'Hello World' c = a type(a) # <class 'str'> id(a) == id(c) # True a is c # True
-type()
returns type of the object that passed variable is pointing to.
-id()
returns unique id of the object that variable is pointing to.
Immutability
- Immutability means with every change to variable, a new object is being created.
- Integers are immutable:
a = 10 b = a id(a) == id(b) # True a = 20 id(a) == id(b) # False
- Variables are only tags for memory locations that holds an object.
- At the beginning variable with the name ofa
tags object10
but further in the code the same name of the variable is then shifted to object20
.
- Whenb = a
then both variables points to the same object soid(a) == id(b)
isTrue
.
- Whena = 20
then this line creates a new object such thatid(a) == id(b)
isFalse
.
- So, immutability means that there is no possibility to overwrite one memory location. With every try of overwriting there is a new memory location being reserved. - Strings are immutable:
srt1 = 'welcome' str2 = 'welcome' id(str1) == id(str2) # True str2 = str2 + ' to python' id(str1) == id(str2) # False
- At the beggining where we initializestr1
andstr2
, they point to the same object.
- Oncestr2
modified, then a new object is created in the new memory location.
- So basically,'welcome'
is one object in the memory but'welcome to python'
is another object in defferent place in the memory.
- Tuples are immutable:
t1 = (1,2,3) t2 = t1 t1 = (3,4,5) id(t1) == id(t2) # False
- Whent2 = t1
then both variables tags the same object of(1,2,3)
.
- Whent1 = (3,4,5)
then it creates a totally new object in different memeory slot and tahs ith witht1
name.
Mutability
- When mutating a mutable object, we work with the same object all the time (in the same place of memory).
- List are mutable:
list1 = [1,2,3,4] list2 = list1 list1.append(10) list1 # [1,2,3,4,10] list2 # [1,2,3,4,10] id(list1) == id(list2) # True
- variables:list1
andlist2
points to the same obejct,
- because of mutable nature of Python list, when appending, we still append to the same object no matter which variable we use to refer to it. - Let see short comparison of mutability with immutability:
Mutability Immutability List, Set, Dictionary String, Number, Tuples Updating one memory location when modifing variable. Creating new memory location when modifing variable. Modifing the same object. Creating a new object.
Mutable default arguments:
- Passing empty list as a defualt argument
def add_employee(emp, emp_list=[]): emp_list.append(emp) add_employee.__defaults__ # ([],) add_employee('Artur') add_employee.__defaults__ # (['Artur'],) add_employee('John') add_employee.__defaults__ # (['Artur', 'John'],)
- Function creates empty list by default when we don't pass a list during the function's call.
- Functionadd_employee
creates empty list by default only once. As the list is a mutable object, it's being mutated by the function on every call. - Setting default list as None:
def add_employee(emp, emp_list=None): if emp_list is None: emp_list = [] emp_list.append(emp) return emp_list add_employee.__defaults__ # (None,) add_employee('Artur') add_employee.__defaults__ # (None,) add_employee('John') add_employee.__defaults__ # (None,)
- if condition in theadd_employee
function checks if a list passed to the function, if not thenemp_list is None
isTrue
and it creates a new empty object list instead
Passing parameters to a function
Passing by Value | Passing by Reference |
---|---|
Makes a copy of passed object. | Passes an address of actual object. |
Original object doesn't get modified. | Original object changes. |
Creating own memory location for a copied object. | Accessing original object via address. |
When immutable object being passed like string, integer, tuple. | When mutable object being passed like a list. |
Passing by Value:
- Here is the example of passing string object:
def fun(s1): s1 = "abcd" print(s1) print(id(s1)) s1 = "xxx" print(s1) print(id(s1)) fun(s1) print(s1) print(id(s1)) ---------------- # xxx # 11111 # abcd # 22222 # xxx # 11111
- Variables1
within the function is in local scope.
- Variables1
outside the function is in global scope.
- Passing a string intofun
makes a copy of the object and tags it withs1
as a local variable.
- As a string is immutabele, codes1 = "abcd"
creates a new object in the memory location withs1
tag name.
- Doing change on a copy doesn't affect the original object tagged with global variables1
.
- In the local scopes1
taggs string object of "abcd".
- In the global scopes1
taggs string object of "xxx". - Here is another example of passing by value:
def fun(s1): print(s1.upper()) print(id(s1.upper())) print(s1) print(id(s1)) s1 = 'Artur' fun(s1) print(s1) print(id(s1)) --------- # output # 'ARUTR' # 2222222 # 'Artur' # 3333333 # 'Artur' # 3333333
-upper()
infun
doesn't change the original object but creates a copy instead.
- Copied object in the local scope is unchanged byupper()
as well as the original object in the global scope stays unchanged.
Passing by Reference:
- It means when a function modifies an object that passed parameter refers to, the change also reflects back outside:
def fun(my_list): my_list[0] = 99 print(my_list) print(id(my_list)) my_list = [1, 2, 3, 4, 5] fun(my_list) print(my_list) print(id(my_list)) -------------- # output # [99, 2, 3, 4, 5] # 43576936 # [99, 2, 3, 4, 5] # 43576936
- Above function gets parametermy_list
as a reference to the object in the memory.
- Modifying first element of it affects object outside the function.
- We can see that ids of the object inside and ouside the function are the same. - Here is the example where argument is passed by reference and the reference itself is being overwritten inside the called function:
def fun(my_list): my_list = [10, 11, 12, 13, 14] print(my_list) print(id(my_list)) my_list = [1, 2, 3, 4, 5] fun(my_list) print(my_list) print(id(my_list)) -------------- # output # [10, 11, 12, 13, 14] # 43522222 # [1, 2, 3, 4, 5] # 43522123
- Variablemy_list
is in the local scope in thefun
.
- Variablemy_list
is in the global scope outside thefun
.
- Bymy_list = [10, 11, 12, 13, 14]
we create the local list object in the memory.
- However, the object outside the function stays untouched.
- In the local scope of the function,my_list
tags object:[10, 11, 12, 13, 14]
.
- In the global scope outside the function,my_list
tags object:[1, 2, 3, 4, 5]
.
Setup
No specific installation required.