Python Tech Series… Day 2: Introduction To Python Programming. Python's Building Blocks wrt to CS* - Part 2

Hello Reader! Welcome to Day 2 Part 2. I hope the previous articles are good and laid great interest. In this Part 2, we will discuss variables and their memory references, mutability, Equality, Assignment, Reference counting, and Dynamic and static types.

Variables are Memory References: Consider a real-time scenario, when we want to send a letter to someone, we have to write the receiver's address, and content. Address registered on the mail corresponds to a Unique Mail address somewhere in the world, By writing the address, we make sure that the contents in the letters will get delivered to that mailbox. Likewise, Computer memory consists of a series of slots with a unique address called memory address, and when we store the data, data can be fit in a single slot or in multiple slots as long we know the start address of the object stored in memory (single or multiple slots) we can access the objects and its data.

image.png

An object with data 77 is stored in an 8b0 slot in the above image. object with data "Hey!" is stored in a 1f0 slot. Storing and retrieving the objects will be managed from Heap and is handled by "Python Memory Manager". Variable uses Free and Dynamic memory hence we store them in Heap, Function calls and local variables will be held at Stack.

What happens when we write a code: Variables in Python are always references & refer to the object in the memory & managed by Python Memory Manager. In a program, If we declare a variable say, my_var = 77. Once python executes this statement, Python creates an OBJect in memory at some address 8b0 in the above address, and value 77 will be stored inside that object. Now, my_var is a simple Alias/Name for the memory address where that object is stored. So, my_var is a reference to the object at address 8b0. Technically my_var is not equal to 77 but it is a reference to the object present in the memory location. if data overflows into multiple slots variable refers to starting address. when print(my_var) is executed, Python looks at the my_var & looks at the address that my_var is referencing, it looks at the object in that address and gets the value from the object and brought back to display it. We can find the memory address where the variable is referencing using the id() function. id() function return the memory address in decimal format. we can use hex() to convert the address from decimal to hex format.

To find the memory location where our variable is stored: using id() function. id() function return the base 10 format of the memory address and if we need it in hex format, we need to convert using the hex() function. id() function create a reference to the object and get the address and destroy the reference.
my_var = 77;
print(id(my_var)) #return the base 10 of the memory address. 
print(hex(id(my_var)) # return the hex format for memory address.
print(my_var);

Python Memory Manager keeps track of the life of objects. It tracks how many other variable points to the same objects or how many new variables are got created.

Reference count: while writing the program, when we declare a variable ex: my_var = 10. Python creates an object with the value 10 and stores it at a memory location say 0x1000 and my_var act as a pointer and refer to the memory address. For every object, we create while coding, PMM (Python Memory Manager) keeps track of the number of references to the object. The reference count for object in 0x1000 is 1.

In next line, new_var = my_var. Here instead of creating a new object and new_var refers to it, Python creates a new reference to the same memory location by sharing the reference from my_var. Here, the reference count for object in 0x1000 is 2.

Suppose I reassign new_var with a new value other than 10 say "hello". Now new object will be created in memory and new_var will be pointing to starting address of the new object. Now the reference count for new_var is 1 and the reference count of my_var decreases to 1.

Again, if I reassign my_var with another value say, '77' and python created a new object at x1234. Now my_var will point to x1234 and reference count at 0x1234 will be 1 and the reference count at 0x1000 will be down to zero.

Once reference count = 0, means there are no references pointing to a memory location, in our example, it's 0x1000, Python recognizes it as dummy/garbage and destroys the object, and frees up the memory spaces for reuse.

Module sys has a function (sys.getrefcounnt(variable_name)) to get count of reference or ctypes module has a function (ctypes.c_long.from_address(address).value).

Caution: getrefcount takes the variable name as a parameter and ctypes take the variable's address as a parameter.

import sys
sys.getrefcount(variable_name) #always return total no of references + 1. Reason: when we try to getrefcount, we will id() function to get the address for that variable, id() will create a reference to the variable, get the address, and using the address, getrefcount() will get all counts of the references. hence Total ref count + ref count because of id() indeed 1.  or using ctypes.
import ctypes
ctypes.c_long.from_address(address).value. #here we return the total no of references. we will pass the address of the variable instead of the variable name. hence id() function will happen before and reference will be released and reference count will not increase.a = [1,2,3];
print("total no of reference ", sys.getrefcount(a)-1)
print("total no of reference",ctypes.c_long.from_address(id(a)).value)### important experiment. 
b = a;  # one more reference to address where a is pointing too. print("total no of reference ", sys.getrefcount(a)-1)  #actually getref returns 3 (total no of ref + ID func ref), so we are substract one. 
print("total no of reference",ctypes.c_long.from_address(id(a)).value) #This return actual no of references so its 2. Total actual references pointing to memory where [1,2,3] starts is : 2
print("memory address pointing by a,b :- ", id(a), id(b));both points to same memory address. 
#memory address pointing by a,b :-  2513950743680 2513950743680 in my system it may varies in yours.
c = b
print("memory address pointing by a,b,c :- ", id(a), id(b), id(c))
#here c looks for ref of b and b is ref to a so at end all variables are ref to same address. #memory address pointing by a,b,c :-  2513950743680 2513950743680 2513950743680 in my system it may varies in yours. ref count is 3.

Important: let's say, i reassign c with a new value 10, A new address will be created for c, but b,a will still point to same memory and the ref count will be 2. if we re assign a,b with different values and checking the ref count by using address, if the last reference for that address got dropped or reset to another value, The Memory Maneger free up the memory and memory address will be available for something else.

Variable ReAssignment: If we declare a variable, my_var = 10, Python will create an object in a memory location say 0x1000, and my_var will be referencing to memory location. now, my_var is reassigned with a new value of 15 my_var = 15.. Take a moment and think about how python behaves.

Ideally, we think Value 10 at the memory location gets updated with 15. But Python will create a new object at another memory location say 0x1234 and now my_var will point to the new location and drops the reference to the old memory location. Now, the reference count to a new memory location is 1, and the count to the old location is downs to zero.

Consider a code, my_var = my_var + 5. Python first evaluates the expression, python creates an object first at a new memory location, gets the value from my_var and adds 5 to it, and stores it in a new memory location, and my_var will start referencing to the new memory location.

Int/Numbers are immutable objects and contents in Int object cannot be changed/modify.

my_var = 10
# memory location of my_var is 2691059941904
print("memory location of my_var is {}".format(id(my_var)))
# changes to 15
my_var = 15
# memory location of my_var with 15 is 2691059942064
print("memory location of my_var with 15 is {}".format(id(my_var)))
my_var = my_var + 15
# memory location of my_var after expression is 2691059942544
print("memory location of my_var after expression is {}".format(id(my_var)))

Dynamic and Static typing: Python is a Dynamic typing language. Static typed: The type of the variable will be validated at compiling level, in Java, we have to declare a variable with the type of the variable name of the variable, and value. ex: string my_var = 'Hello'; if we reassign the my_var with another type, the system will raise an exception. Examples of statically typed languages are Java, C++, C, etc.

Dynamic typed: The type of the variable will be validated at the runtime level, in languages such as Javascript, Python, etc we will declare a variable without any type. ex: my_var = 'Hello". Python creates an object with the type of the data as string and value 'Hello'. we can change the value say my_var = 10, An new object will be created with int type and value 10 at a new memory location, and my_var will be referencing the new object.

type() function discussed in part 1, will help to find the type of the variable.

my_var = 'Hello'
# location of my_var with string is 2782044009776
print('location of my_var with string is {}'.format(id(my_var)))
my_var = 10
# location of my_var with updated value 10 is 2782038983184
print('location of my_var with updated value 10 is {}'.format(id(my_var)))

Object Mutability & Immutability: when we declare a variable, An object is created in memory with type, data and changing the data inside the object is called modifying the internal state of the object. what is a Mutable: consider you have created a variable my_account and object is created with data type called bank account and has 2 properties. 1. Account no and balance. initially, you have 50 rs in the account and the value in balance is 50. now we have modified the balance from 50 to 100. Here the state of the balance field got changed but not its memory address. Modifying the internal state can say "object is Mutated". An object whose internal state can be changed is called a mutable and an object whose internal state cannot be changed is called an immutable object.

Data types which are mutable/immutable in python are: Mutable: Lists [], Dict & sets, User-defined classes. Immutable: Numbers, Strings, Tuples[immutable version of lists], Frozen sets, User-defined classes

  • User defined class is depends on the user how classes should behave *

Interesting Point: Tuples are Immutable containers and elements are immutable. But there is a catch when we actually use tuples with mutable objects.

#my_space is the name of the function and my_space() invokes the function.
import sys
a = 20
# 20 <class 'int'> - class int and a is the instance of the class int. we can get the information about any class using help(class) ex: help(int) it return the detailed description.
print(a, type(a))
def square(a):
    return a * a
# pass like a variable to get the memory location of the square function
print(id(square))
# pass like a variable - return the type of the function -<class 'function'>
print(type(square))
print("square of the variable:-", square(2))  # square of the variable:- 4
# get the reference count to square.
print(sys.getrefcount(square) - 1)  # current reference is 1.
# we can assign one function to another variable and it points to same function object in memory and perform same as square
full = square
print(sys.getrefcount(square) - 1)  # current reference is 2.
print("square of the variable with function full:-",
full(2))  # square of the variable:- 4
# pass function can be passed by another functions
def cube(a):
   return a**3
def square(a):
   return a*a
def select_function(fun_id):
  if fun_id == 1:
      return square  # returning the name of function as variable
  else:
      return cube  # returning the name of the function as a variable.
# now we got square as return and we can use full(2) to get the square of the number
full = select_function(1)
print(full is square)
print(full(2))
# pass functions from a function
def exec_func(fn, n):
   return fn(n)
print('Function to function:-', exec_func(cube,3))
""" how below statement executes. arg gets executed first - select function return cube and second parameter will invoke the function
with value 3 and return the value from the cube function.
"""
print(select_function(2)(3))  # return 27

References: Python 3: Deep Dive (Part 1 - Functional) - udemy

Next up: Number Systems.

Thanks for reading!. Keep Upskilling.