Instance attributes vs static attributes

self.[ATTR] are defined only when an object of the class is instantiated

In [31]:
class SimpleClass():
    x = 1 #Static. exists without instatiation
    
    #This method is executed only when class is instantiated
    def __init__(self):
        print(type(self))
        self.y = 2

Get the class WITHOUT instantiating. Note that there is no paranthesis

In [32]:
#Get the class WITHOUT instantiating. Note that there is no paranthesis
x = SimpleClass
#__init__ is not run
x 
Out[32]:
__main__.SimpleClass
In [17]:
#This attribute is defined eventhough we have not instantiated. These are static attributes
x.x
Out[17]:
1
In [18]:
#y is not defined, as we have not yet instantiated
x.y
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-18-b64aacb8851f> in <module>
      1 #y is not defined, as we have not yet instantiated
----> 2 x.y

AttributeError: type object 'SimpleClass' has no attribute 'y'

Instantiate the class

In [33]:
#Instantiate the class
y = SimpleClass()
#runs __init__
<class '__main__.SimpleClass'>
In [20]:
y.x
Out[20]:
1
In [21]:
#Now y is defined
y.y
Out[21]:
2

.dict

In [24]:
#Shows only instance attributes
y.__dict__
Out[24]:
{'y': 2}
In [25]:
#Shows class attributes
x.__dict__
Out[25]:
mappingproxy({'__dict__': <attribute '__dict__' of 'SimpleClass' objects>,
              '__doc__': None,
              '__init__': <function __main__.SimpleClass.__init__(self)>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'SimpleClass' objects>,
              'x': 1})

Inheritence

The super class constructor is not called by default, when the child class is instantiated. The super class constructor should be explicitly called to be executed

In [38]:
class Extend(SimpleClass):
    def __init__(self):
        #Does not run the __init__ of super class
        self.z = 3
In [39]:
z = Extend()
#The init of SimpleClass is not run. So y is not defined
z.__dict__
Out[39]:
{'z': 3}
In [41]:
#But x is still inherited
z.x
Out[41]:
1
In [43]:
class Extend2(SimpleClass):
    def __init__(self):
        #Super class constructor is explicitly clalled
        super().__init__()
        self.z = 3
In [45]:
w = Extend2()
#Now the super class __init__ is run
w.__dict__
<class '__main__.Extend2'>
Out[45]:
{'y': 2, 'z': 3}

Annotations

In [1]:
def f(x:int,s:str) -> float:
    print(s)
    return float(x)

Can be used to check the return type and argument types expected

In [14]:
f
Out[14]:
<function __main__.f(x:int, s:str) -> float>
In [5]:
f.__annotations__
Out[5]:
{'return': float, 's': str, 'x': int}
In [6]:
f(2,"Aravind")
Aravind
Out[6]:
2.0

However, it does not check if the correct types are passed. For this, you can create a custom assertion with annotation

In [7]:
f(1,2)
2
Out[7]:
1.0

To specify types that are structured, use typing module

In [8]:
from typing import Tuple
In [9]:
def g() -> Tuple[int,str,float]:
    return (1,"hi",3.0)
In [10]:
g()
Out[10]:
(1, 'hi', 3.0)
In [11]:
g.__annotations__
Out[11]:
{'return': typing.Tuple[int, str, float]}

Can also pass a custom struct

In [12]:
fd = {'type':int, 'units':'Joules','help':"bla bla bla"}
def h() -> fd:
    return 2
In [13]:
h
Out[13]:
<function __main__.h() -> {'help': 'bla bla bla', 'type': <class 'int'>, 'units': 'Joules'}>

Yield

Useful when you have a large dataset which does not fit in memory when used as list

Used to build generator functions

In [23]:
def generator():
    a = [1,2,3,4,5]
    # returns 1 when generator() is called first time
    # returns 2 when generator() is called second time
    # ....
    for i in a:
        print("called!")
        yield i
In [24]:
for i in generator():
    print(i)
called!
1
called!
2
called!
3
called!
4
called!
5