In [1]:
class ClassWithInit:
def __init__(self):
pass
class ClassWithoutInit:
pass
In [2]:
class A:
def __init__(self):
self.attr1 = 42
def method(self):
self.attr2 = 43
a = A()
print(a.attr1)
# print(a.attr2) # raises AttributeError
a.method()
print(a.attr2)
In [3]:
a.attr3 = 11
print(a.attr3)
In [4]:
a2 = A()
# a2.attr3 # raises AttributeError
In [5]:
class InitWithArguments:
def __init__(self, value, value_with_default=42):
self.attr = value
self.solution_of_the_world = value_with_default
class InitWithVariableNumberOfArguments:
def __init__(self, *args, **kwargs):
self.val1 = args[0]
self.val2 = kwargs.get('important_param', 42)
In [6]:
obj1 = InitWithArguments(41)
obj2 = InitWithVariableNumberOfArguments(1, 2, 3, param4="apple", important_param=23)
print(obj1.attr, obj1.solution_of_the_world,
obj2.val1, obj2.val2)
In [7]:
class A:
def foo(self):
print("foo called")
def bar(self, param):
print("bar called with parameter {}".format(param))
In [8]:
c = A()
c.foo()
c.bar(42)
A.foo(c)
A.bar(c, 43)
In [9]:
', '.join(A.__dict__)
Out[9]:
__classname_attrname
In [10]:
class A:
def __init__(self):
self.__private_attr = 42
def foo(self):
self.__private_attr += 1
a = A()
a.foo()
# print(a.__private_attr) # raises AttributeError
a.__dict__
print(a._A__private_attr) # name mangled
a.__dict__
Out[10]:
In [11]:
class A:
class_attr = 42
In [12]:
a1 = A()
a1.class_attr
Out[12]:
In [13]:
A.class_attr
Out[13]:
In [14]:
a1 = A()
a2 = A()
print(a1.class_attr, a2.class_attr)
A.class_attr = 43
a1.class_attr, a2.class_attr
Out[14]:
In [15]:
a1 = A()
a2 = A()
a1.class_attr = 11
a2.class_attr
Out[15]:
because this assignment creates a new attribute in the instance's namespace.
In [16]:
a1.__dict__
Out[16]:
each object has a __class__
magic attribute that accesses the class object.
We can use this to access the class attribute:
In [17]:
a1.__class__.class_attr
Out[17]:
a2
has not shadowed class_attr
, so we can access it through the instance
In [18]:
a2.__dict__, a2.class_attr
Out[18]:
In [19]:
class A:
pass
class B(A):
pass
a = A()
b = B()
print(isinstance(a, B))
print(isinstance(b, A))
print(issubclass(B, A))
print(issubclass(A, B))
object
or one of its predecessors subclasses object
object
The differences between old style and new style classes are listed here: https://wiki.python.org/moin/NewClassVsClassicClass
In [20]:
%%python2
class OldStyleClass:
pass
class NewStyleClass(object):
pass
class ThisIsAlsoNewStyleClass(NewStyleClass):
pass
In [21]:
class A: pass
class B(object): pass
print(issubclass(A, object))
print(issubclass(B, object))
In [22]:
class A(object):
def foo(self):
print("A.foo was called")
def bar(self):
print("A.bar was called")
class B(A):
def foo(self):
print("B.foo was called")
b = B()
b.foo()
b.bar()
Since data attributes can be created anywhere, they are only inherited if the code in the base class' method is called.
In [23]:
class A(object):
def foo(self):
self.value = 42
class B(A):
pass
b = B()
print(b.__dict__)
a = A()
print(a.__dict__)
a.foo()
print(a.__dict__)
In [24]:
class A(object):
def __init__(self):
print("A.__init__ called")
class B(A):
def __init__(self):
print("B.__init__ called")
class C(A): pass
b = B()
c = C()
The base class's methods can be called in at least two ways:
In [25]:
class A(object):
def __init__(self):
print("A.__init__ called")
class B(A):
def __init__(self):
A.__init__(self)
print("B.__init__ called")
class C(B):
def __init__(self):
super().__init__()
print("C.__init__ called")
print("Instantiating B")
b = B()
print("Instantiating C")
c = C()
super
's usage was more complicated in Python 2
In [26]:
%%python2
class A(object):
def __init__(self):
print("A.__init__ called")
class B(A):
def __init__(self):
A.__init__(self)
print("B.__init__ called")
class C(A):
def __init__(self):
super(C, self).__init__()
print("B.__init__ called")
print("Instantiating B")
b = B()
print("Instantiating C")
c = C()
A complete example using super in the subclass's init:
In [27]:
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return "{0}, age {1}".format(self.name, self.age)
class Employee(Person):
def __init__(self, name, age, position, salary):
self.position = position
self.salary = salary
super().__init__(name, age)
def __str__(self):
return "{0}, position: {1}, salary: {2}".format(super().__str__(), self.position, self.salary)
e = Employee("Jakab Gipsz", 33, "manager", 450000)
print(e)
print(Person(e.name, e.age))
"In computer programming, duck typing is an application of the duck test in type safety. It requires that type checking be deferred to runtime, and is implemented by means of dynamic typing or reflection." -- Wikipedia
"If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck." -- Wikipedia
In [28]:
class Cat(object):
def make_sound(self):
self.mieuw()
def mieuw(self):
print("Mieuw")
class Dog(object):
def make_sound(self):
self.bark()
def bark(self):
print("Vau")
animals = [Cat(), Dog()]
for animal in animals:
# animal must have a make_sound method
animal.make_sound()
In [29]:
class A(object):
def foo(self):
raise NotImplementedError()
class B(A):
def foo(self):
print("Yay.")
class C(A): pass
b = B()
b.foo()
c = C()
# c.foo() # NotImplementedError why does this happen?
In [30]:
a = A()
In [31]:
class ClassWithoutStr(object):
def __init__(self, value=42):
self.param = value
class ClassWithStr(object):
def __init__(self, value=42):
self.param = value
def __str__(self):
return "My id is {0} and my parameter is {1}".format(
id(self), self.param)
print("Printint a class that does not __str__: {}".format(ClassWithoutStr(345)))
print("Printint a class that defines __str__: {}".format(ClassWithStr(345)))
__len__
: defines the behavior of len(obj)
__abs__
: defines the behavior of abs(obj)
In [32]:
class Complex(object):
def __init__(self, real=0.0, imag=0.0):
self.real = real
self.imag = imag
def __abs__(self):
return (self.real**2 + self.imag**2) ** 0.5
def __eq__(self, other): # right hand side
return self.real == other.real and self.imag == other.imag
def __gt__(self, other):
return abs(self) > abs(other)
c1 = Complex()
c2 = Complex(1, 1)
abs(c2), c1 == c2
Out[32]:
How can we define comparison between different types?
Let's define a comparison between Complex
and strings. We can check for the right operand's type:
In [33]:
class Complex(object):
def __init__(self, real=0.0, imag=0.0):
self.real = real
self.imag = imag
def __abs__(self):
return (self.real**2 + self.imag**2) ** 0.5
def __eq__(self, other): # right hand side
return self.real == other.real and self.imag == other.imag
def __gt__(self, other):
if isinstance(other, str):
return abs(self) > len(other)
return abs(self) > abs(other)
c1 = Complex()
c2 = Complex(1, 1)
abs(c2), c1 == c2, c2 > "a", c2 > "ab"
Out[33]:
if the built-in type is the left-operand for which comparison against Complex
is not defined, the operands are automatically swithced:
In [34]:
"a" < c2
Out[34]:
Defining __gt__
does not automatically define __lt__
:
In [35]:
# "a" > c2 # raises TypeError
Attributes can be set, get and deleted. 4 magic methods govern these:
__setattr__
: called when we set an attribute,__delattr__
: called when we delete an attribute using del
or delattr
__getattribute__
: called when accessing attributes__getattr__
: called when the 'usual' attribute lookup fails (for example the attribute is not present in the object's namespace
In [36]:
class Noisy(object):
def __setattr__(self, attr, value):
print("Setting [{}] to value [{}]".format(attr, value))
super().__setattr__(attr, value)
def __getattr__(self, attr):
print("Getting (getattr) [{}]".format(attr))
super().__getattr__(attr)
def __getattribute__(self, attr):
print("Getting (getattribute) [{}]".format(attr))
super().__getattribute__(attr)
def __delattr__(self, attr):
print("You wish")
getting an attribute that doesn't exist yet calls
In [37]:
a = Noisy()
try:
a.dog
except AttributeError:
print("AttributeError raised")
setting an attribute
In [38]:
a.dog = "vau" # equivalent to setattr(a, "dog", "vau")
getting an existing attribute
In [39]:
a.dog # equivalent to getattr(a, "dog")
modifying an attribute also calls __setattr__
In [40]:
a.dog = "Vau" # equivalent to setattr(a, "dog", "Vau")
deleting an attribute
In [41]:
del a.dog # equivalent to delattr(a, "dog")
In [42]:
class DictLike(object):
def __init__(self):
self.d = {}
def __setitem__(self, item, value):
print("Setting {} to {}".format(item, value))
self.d[item] = value
def __getitem__(self, item):
print("Getting {}".format(item))
return self.d.get(item, None)
def __iter__(self):
return iter(self.d)
d = DictLike()
d["a"] = 1
d["b"] = 2
for k in d:
print(k)
In [43]:
l1 = [[1, 2], [3, 4, 5]]
l2 = l1
id(l1[0]) == id(l2[0])
Out[43]:
In [44]:
l1[0][0] = 10
l2
Out[44]:
In [45]:
from copy import copy
l1 = [[1, 2], [3, 4, 5]]
l2 = copy(l1)
id(l1) == id(l2), id(l1[0]) == id(l2[0])
Out[45]:
In [46]:
l1[0][0] = 10
l2
Out[46]:
In [47]:
from copy import deepcopy
l1 = [[1, 2], [3, 4, 5]]
l2 = deepcopy(l1)
id(l1) == id(l2), id(l1[0]) == id(l2[0])
Out[47]:
In [48]:
l1[0][0] = 10
l2
Out[48]:
In [49]:
from copy import copy, deepcopy
class ListOfLists(object):
def __init__(self, lists):
self.lists = lists
self.list_lengths = [len(l) for l in self.lists]
def __copy__(self):
print("ListOfLists copy called")
return ListOfLists(self.lists)
def __deepcopy__(self, memo):
print("ListOfLists deepcopy called")
return ListOfLists(deepcopy(self.lists))
l1 = ListOfLists([[1, 2], [3, 4, 5]])
l2 = copy(l1)
l1.lists[0][0] = 12
print(l2.lists)
l3 = deepcopy(l1)
However, these are very far from complete implementations. We need to take care of preventing infinite loops and support for pickling (serialization module).
__new__
and the __del__
methodThe __new__
method is called to create a new instance of a class. __new__
is a static method that takes the class object as a first parameter.
Typical implementations create a new instance of the class by invoking the superclass’s __new__()
method using super(currentclass, cls).__new__(cls[, ...])
with appropriate arguments and then modifying the newly-created instance as necessary before returning it.
__new__
has to return an instance of cls
, on which __init__
is called.
The __del__
method is called when an object is about to be destroyed.
Although technically a destructor, it is handled by the garbage collector.
It is not guaranteed that __del__()
methods are called for objects that still exist when the interpreter exits.
In [50]:
class A(object):
@classmethod
def __new__(cls, *args, **kwargs):
instance = super().__new__(cls)
print("A.__new__ called")
return instance
def __init__(self):
print("A.__init__ called")
def __del__(self):
print("A.__del__ called")
try:
super(A, self).__del__()
except AttributeError:
print("parent class does not have a __del__ method")
a = A()
del a
In [51]:
class A(object):
var = 12
def __init__(self, value):
self.value = value
def foo(self):
print("bar")
", ".join(dir(A))
Out[51]:
Class A does not have a value attribute, since it is bounded to an instance. However, it does have the class global var attribute.
An instance of A has both:
In [52]:
", ".join(dir(A(12)))
Out[52]:
In [53]:
class A(object):
pass
class B(A):
pass
b = B()
a = A()
print(isinstance(a, A))
print(isinstance(a, B))
print(isinstance(b, A))
print(isinstance(b, object))
Every object has a __code__ attribute, which contains everything needed to call the function.
In [54]:
def evaluate(x):
a = 12
b = 3
return a*x + b
print(evaluate.__code__)
#dir(evaluate.__code__)
In [55]:
evaluate.__code__.co_varnames, evaluate.__code__.co_freevars, evaluate.__code__.co_stacksize
Out[55]:
The inspect module provides further code introspection tools, including the getsourcelines function, which returns the source code itself.
In [56]:
from inspect import getsourcelines
getsourcelines(evaluate)
Out[56]:
In [57]:
class A(object):
instance_count = 0
def __init__(self, value=42):
self.value = value
A.increase_instance_count()
@staticmethod
def increase_instance_count():
A.instance_count += 1
a1 = A()
print(A.instance_count)
a2 = A()
print(A.instance_count)
In [58]:
class Complex(object):
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __str__(self):
return '{0}+j{1}'.format(self.real, self.imag)
@classmethod
def from_str(cls, complex_str):
real, imag = complex_str.split('+')
imag = imag.lstrip('ij')
print("Instantiating {}".format(cls.__name__))
return cls(float(real), float(imag))
class ChildComplex(Complex): pass
c1 = Complex.from_str("3.45+j2")
print(c1)
c2 = Complex(3, 4)
print(c2)
c1 = ChildComplex.from_str("3.45+j2")
In [59]:
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, age):
try:
if 0 <= age <= 150:
self._age = age
except TypeError:
pass
def __str__(self):
return "Name: {0}, age: {1}".format(self.name, self.age)
p = Person("John", 12)
print(p)
p.age = "abc"
print(p)
p.age = 85
print(p)
In [60]:
p = Person("Pete", 17)
",".join(dir(p))
Out[60]:
In [61]:
class A(object):
def __init__(self, value):
print("A init called")
self.value = value
class B(object):
def __init__(self):
print("B init called")
class C(A, B):
def __init__(self, value1, value2):
print("C init called")
self.value2 = value2
super(C, self).__init__(value1)
class D(B, A): pass
print("Instantiating C")
c = C(1, 2)
print("Instantiating D")
d = D()