1. A class is an idea (more or less abstract) which can be used to create a number of incarnations – such an incarnation is called an object.
2. When a class is derived from another class, their relation is named inheritance. The class which derives from the other class is named a subclass. The second side of this relation is named superclass. A way to present such a relation is an inheritance diagram, where:
-
- superclasses are always presented above their subclasses;
- relations between classes are shown as arrows directed from the subclass toward its superclass.

3. Objects are equipped with:
-
- a name which identifies them and allows us to distinguish between them;
- a set of properties (the set can be empty)
- a set of methods (can be empty, too)
4. To define a Python class, you need to use the class keyword. For example:
|
1 2 |
class This_Is_A_Class: pass |
5. To create an object of the previously defined class, you need to use the class as if it were a function. For example:
|
1 |
this_is_an_object = This_Is_A_Class() |
6. A stack is an object designed to store data using the LIFO model. It’s an abbreviation for a very clear description of the stack’s behavior: Last In – First Out. The coin that came last onto the stack will leave first. The stack usually performs at least two operations, named
-
push(when a new element is put on the top)pop(when an existing element is taken away from the top).

The stack – the procedural approach
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
stack = [] def push(val): stack.append(val) def pop(): val = stack[-1] del stack[-1] return val push(3) push(2) push(1) print(pop()) print(pop()) print(pop()) |
1
2
3
7. Implementing the stack in a procedural model raises several problems which can be solved by the techniques offered by OOP (Object Oriented Programming):
8. The part of the Python class responsible for creating new objects is called the constructor.
-
- the constructor’s name is always
__init__; - it has to have at least one parameter; the parameter is used to represent the newly created object – you can use the parameter to manipulate the object, and to enrich it with the needed properties; you’ll make use of this soon;
- the obligatory parameter is usually named
self– it’s only a convention, but you should follow it – it simplifies the process of reading and understanding your code.
- the constructor’s name is always
|
1 2 3 4 5 6 |
class Stack: # Defining the Stack class. def __init__(self): # Defining the constructor function. print("Hi!") stack_object = Stack() # Instantiating the object. |
9. To create a property inside a class it is used a dot notation, just like when invoking methods; this is the general convention for accessing an object’s properties you need to name the object, put a dot (.) after it, and specify the desired property’s name;
|
1 2 3 4 5 6 |
class Stack: def __init__(self): self.stack_list = [] stack_object = Stack() print(len(stack_object.stack_list)) |
10. If we want to hide any of a class’s components from the outside world, we should start its name with __. Such components are called private.
The ability to hide (protect) selected values against unauthorized access is called encapsulation; the encapsulated values can be neither accessed nor modified if you want to use them exclusively;
|
1 2 3 4 5 6 |
class Stack: def __init__(self): self.__stack_list = [] stack_object = Stack() print(len(stack_object.__stack_list)) |
Traceback (most recent call last):
File "main.py", line 7, in <module>
print(len(stack_object.__stack_list))
AttributeError: 'Stack' object has no attribute '__stack_list'
11. A class method is actually a function declared inside the class and able to access all the class’s components. Each class method declaration must contain at least one parameter (always the first one) usually referred to as self, and is used by the objects to identify themselves.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Stack: def __init__(self): self.__stack_list = [] def push(self, val): self.__stack_list.append(val) def pop(self): val = self.__stack_list[-1] del self.__stack_list[-1] return val stack_object = Stack() stack_object.push(3) stack_object.push(2) stack_object.push(1) print(stack_object.pop()) print(stack_object.pop()) print(stack_object.pop()) |
12. An instance variable is a property whose existence depends on the creation of an object. Every object can have a different set of instance variables.
Moreover, they can be freely added to and removed from objects during their lifetime. All object instance variables are stored inside a dedicated dictionary named __dict__, contained in every object separately.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class ExampleClass: def __init__(self, val = 1): self.first = val def set_second(self, val): self.second = val example_object_1 = ExampleClass() example_object_2 = ExampleClass(2) example_object_2.set_second(3) example_object_3 = ExampleClass(4) example_object_3.third = 5 print(example_object_1.__dict__) print(example_object_2.__dict__) print(example_object_3.__dict__) |
-
- the class named
ExampleClasshas a constructor, which unconditionally creates an instance variable namedfirst, and sets it with the value passed through the first argument (from the class user’s perspective) or the second argument (from the constructor’s perspective); note the default value of the parameter – any trick you can do with a regular function parameter can be applied to methods, too; - the class also has a method which creates another instance variable, named
second; - we’ve created three objects of the class ExampleClass, but all these instances differ:
- the class named
example_object_1 only has the property named first;
example_object_2 has two properties: first and second;
example_object_3 has been enriched with a property named third just on the fly, outside the class’s code – this is possible and fully permissible.
The program’s output clearly shows that our assumptions are correct – here it is:
{'first': 1}
{'second': 3, 'first': 2}
{'third': 5, 'first': 4}
There is one additional conclusion that should be stated here: modifying an instance variable of any object has no impact on all the remaining objects. Instance variables are perfectly isolated from each other.
13. An instance variable can be private when its name starts with __, but don’t forget that such a property is still accessible from outside the class using a mangled name constructed as _ClassName__PrivatePropertyName.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class ExampleClass: def __init__(self, val = 1): self.__first = val def set_second(self, val = 2): self.__second = val example_object_1 = ExampleClass() example_object_2 = ExampleClass(2) example_object_2.set_second(3) example_object_3 = ExampleClass(4) example_object_3.__third = 5 print(example_object_1.__dict__) print(example_object_2.__dict__) print(example_object_3.__dict__) |
It’s nearly the same as the previous one. The only difference is in the property names. We’ve added two underscores (__) in front of them.
Output:
{'_ExampleClass__first': 1}
{'_ExampleClass__first': 2, '_ExampleClass__second': 3}
{'_ExampleClass__first': 4, '__third': 5}
The name is now fully accessible from outside the class. You can run a code like this:
|
1 |
print(example_object_1._ExampleClass__first) |
14. A class variable is a property which exists in exactly one copy, and doesn’t need any created object to be accessible. Such variables are not shown as __dict__ content.
All a class’s class variables are stored inside a dedicated dictionary named __dict__, contained in every class separately.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class ExampleClass: counter = 0 def __init__(self, val = 1): self.__first = val ExampleClass.counter += 1 example_object_1 = ExampleClass() example_object_2 = ExampleClass(2) example_object_3 = ExampleClass(4) print(example_object_1.__dict__, example_object_1.counter) print(example_object_2.__dict__, example_object_2.counter) print(example_object_3.__dict__, example_object_3.counter) |
Running the code will cause the following output:
{'_ExampleClass__first': 1} 3
{'_ExampleClass__first': 2} 3
{'_ExampleClass__first': 4} 3
-
- class variables aren’t shown in an object’s
__dict__(this is natural as class variables aren’t parts of an object) but you can always try to look into the variable of the same name, but at the class level - a class variable always presents the same value in all class instances (objects)
- class variables aren’t shown in an object’s
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ExampleClass: __counter = 0 def __init__(self, val = 1): self.__first = val ExampleClass.__counter += 1 example_object_1 = ExampleClass() example_object_2 = ExampleClass(2) example_object_3 = ExampleClass(4) print(example_object_1.__dict__, example_object_1._ExampleClass__counter) print(example_object_2.__dict__, example_object_2._ExampleClass__counter) print(example_object_3.__dict__, example_object_3._ExampleClass__counter) |
{'_ExampleClass__first': 1} 3
{'_ExampleClass__first': 2} 3
{'_ExampleClass__first': 4} 3
15. A function named hasattr() can be used to determine if any object/class contains a specified property.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class ExampleClass: def __init__(self, val): if val % 2 != 0: self.a = 1 else: self.b = 1 example_object = ExampleClass(1) print(example_object.a) print(example_object.b) |
As you can see, accessing a non-existing object (class) attribute causes an AttributeError exception.
1
Traceback (most recent call last):
File ".main.py", line 11, in
print(example_object.b)
AttributeError: 'ExampleClass' object has no attribute 'b'
The try-except instruction gives you the chance to avoid issues with non-existent properties.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class ExampleClass: def __init__(self, val): if val % 2 != 0: self.a = 1 else: self.b = 1 example_object = ExampleClass(1) print(example_object.a) try: print(example_object.b) except AttributeError: pass |
or utlize the hasattr():
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class ExampleClass: def __init__(self, val): if val % 2 != 0: self.a = 1 else: self.b = 1 example_object = ExampleClass(1) print(example_object.a) if hasattr(example_object, 'b'): print(example_object.b) |
Example:
|
1 2 3 4 5 6 7 8 9 10 |
class Sample: gamma = 0 # Class variable. def __init__(self): self.alpha = 1 # Instance variable. self.__delta = 3 # Private instance variable. obj = Sample() obj.beta = 2 # Another instance variable (existing only inside the "obj" instance.) print(obj.__dict__) |
The code outputs:
{'alpha': 1, '_Sample__delta': 3, 'beta': 2}
Example
|
1 2 3 4 5 6 |
class A: def __init__(self, v): pass a = A(1) print(hasattr(a, 'A')) |
False
|
1 2 3 4 5 6 7 |
class A: A=1 def __init__(self): self.a = 0 print(hasattr(A, 'a')) |
False
|
1 2 3 4 5 6 7 |
class A: A=1 def __init__(self): self.a = 0 b=A() print(hasattr(A, 'a')) |
False
|
1 2 3 4 5 6 7 |
class A: A=1 def __init__(self): self.a = 0 b=A() print(hasattr(b, 'A')) |
True
|
1 2 3 4 5 6 7 |
class A: A=1 def __init__(self): self.a = 0 b=A() print(hasattr(b, 'a')) |
True
16. A method is a function embedded inside a class. The first (or only) parameter of each method is usually named self, which is designed to identify the object for which the method is invoked in order to access the object’s properties or invoke its methods. There is one fundamental requirement – a method is obliged to have at least one parameter (there are no such thing as parameterless methods – a method may be invoked without an argument, but not declared without parameters).
If you want the method to accept parameters other than self, you should: place them after self in the method’s definition; deliver them during invocation without specifying self (as previously) Just like here:
|
1 2 3 4 5 6 7 8 |
class Classy: def method(self, par): print("method:", par) obj = Classy() obj.method(1) obj.method(2) obj.method(3) |
The code outputs:
method: 1
method: 2
method: 3
The self parameter is used to obtain access to the object’s instance and class variables.
|
1 2 3 4 5 6 7 8 9 |
class Classy: varia = 2 def method(self): print(self.varia, self.var) obj = Classy() obj.var = 3 obj.method() |
The code outputs:
2 3
The self parameter is also used to invoke other object/class methods from inside the class.
|
1 2 3 4 5 6 7 8 9 10 |
class Classy: def other(self): print("other") def method(self): print("method") self.other() obj = Classy() obj.method() |
The code outputs:
method
other
17. If a class contains a constructor (a method named __init__) it cannot return any value and cannot be invoked directly.
Everything we’ve said about property name mangling applies to method names, too – a method whose name starts with __ is (partially) hidden. The example shows this effect:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Classy: def visible(self): print("visible") def __hidden(self): print("hidden") obj = Classy() obj.visible() try: obj.__hidden() except: print("failed") obj._Classy__hidden() |
visible
failed
hidden
18. Each Python class and each Python object is pre-equipped with a set of useful attributes which can be used to examine its capabilities.
- One of these – it’s the
__dict__property.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Classy: varia = 1 def __init__(self): self.var = 2 def method(self): pass def __hidden(self): pass obj = Classy() print(obj.__dict__) print(Classy.__dict__) |
Output:
|
1 2 3 |
{'var': 2} {'__module__': '__main__', 'varia': 1, '__init__': <function Classy.__init__ at 0x7fb94adea320>, 'method': <function Classy.method at 0x7fb94adeaef0>, '_Classy__hidden': <function Classy.__hidden at 0x7fb94adeaf80>, '__dict__': <attribute '__dict__' of 'Classy' objects>, '__weakref__': <attribute '__weakref__' of 'Classy' objects>, '__doc__': None} |
- Another built-in property worth mentioning is
__name__, which is a string. The property contains the name of the class. The__name__attribute is absent from the object – it exists only inside classes.
If you want to find the class of a particular object, you can use a function named type(), which is able (among other things) to find a class which has been used to instantiate any object.
|
1 2 3 4 5 6 |
class Classy: pass print(Classy.__name__) obj = Classy() print(type(obj).__name__) |
The code outputs:
Classy
Classy
Note that a statement like this one:
|
1 |
print(obj.__name__) |
will cause an error.
- Additionally, a property named
__module__stores the name of the module in which the class has been declared,
|
1 2 3 4 5 6 |
class Classy: pass print(Classy.__module__) obj = Classy() print(obj.__module__) |
The code outputs:
__main__
__main__
Any module named __main__ is actually not a module, but the file currently being run.
- the property named
__bases__is a tuple containing a class’s superclasses.
Note: only classes have this attribute – objects don’t.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class SuperOne: pass class SuperTwo: pass class Sub(SuperOne, SuperTwo): pass def printBases(cls): print('( ', end='') for x in cls.__bases__: print(x.__name__, end=' ') print(')') printBases(SuperOne) printBases(SuperTwo) printBases(Sub) |
It will output:
( object )
( object )
( SuperOne SuperTwo )
Note: a class without explicit superclasses points to object (a predefined Python class) as its direct ancestor.
Reflection and introspection
Introspection is the ability of a program to examine the type or properties of an object at runtime;
Reflection, which goes a step further, and is the ability of a program to manipulate the values, properties and/or functions of an object at runtime.

Example:
|
1 2 3 4 5 6 7 8 9 |
class Sample: def __init__(self): self.name = Sample.__name__ def myself(self): print("My name is " + self.name + " living in a " + Sample.__module__) obj = Sample() obj.myself() |
The code outputs:
My name is Sample living in a __main__
1. A method named __str__() is responsible for converting an object’s contents into a (more or less) readable string. You can redefine it if you want your object to be able to present itself in a more elegant form. For example:
|
1 2 3 4 5 6 7 8 9 |
class Mouse: def __init__(self, name): self.my_name = name def __str__(self): return self.my_name the_mouse = Mouse('mickey') print(the_mouse) |
mickey
If the __str()__ method is not defined for object:
|
1 2 3 4 5 6 7 8 9 |
class Jedi: def __init__(self, name): self.name = name def Print(self): return self.name Luke = Jedi('Luke') print(Luke) |
invocating the print() function on object Luke will invoke the default __str()__ method which returns the following string :
<__main__.Jedi object at 0x7f910c9d9390>
To get a result different from the default string above, the __str__() method would need to be defined in the Jedi class.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Jedi: def __init__(self, name): self.name = name def Print(self): return self.name class Sith: def __init__(self, name): self.name = name def __str__(self): # I defined the __str()__ method for class Sith return self.name Luke = Jedi('Luke') print(Luke) # <__main__.Jedi object at 0x7f910c9d9390> Vader = Sith('Vader') print(Vader) # Vader |
2. A function named issubclass(Class_1, Class_2) is able to determine if Class_1 is a subclass of Class_2. For example:
|
1 2 3 4 5 6 7 8 |
class Mouse: pass class LabMouse(Mouse): pass print(issubclass(Mouse, LabMouse), issubclass(LabMouse, Mouse)) # Prints "False True" |
3. A function named isinstance(Object, Class) returns True if the object is an instance of the class, or False otherwise.. For example:
|
1 2 3 4 5 6 7 8 9 |
class Mouse: pass class LabMouse(Mouse): pass mickey = Mouse() print(isinstance(mickey, Mouse), isinstance(mickey, LabMouse)) # Prints "True False". |
4. A operator called is checks if two variables refer to the same object. For example:
|
1 2 3 4 5 6 7 8 |
class Mouse: pass mickey = Mouse() minnie = Mouse() cloned_mickey = mickey print(mickey is minnie, mickey is cloned_mickey) # Prints "False True". |
Don’t forget that variables don’t store the objects themselves, but only the handles pointing to the internal Python memory.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class SampleClass: def __init__(self, val): self.val = val object_1 = SampleClass(0) print(object_1.val) object_2 = SampleClass(2) object_3 = object_1 print(object_1.val) object_3.val += 1 print(object_1.val) print(object_1 is object_2) print(object_2 is object_3) print(object_3 is object_1) print(object_1.val, object_2.val, object_3.val) string_1 = "Mary had a little " string_2 = "Mary had a little lamb" string_1 += "lamb" print(string_1 == string_2, string_1 is string_2) |
0
0
1
False
False
True
1 2 1
True False
The results prove that object_1 and object_3 are actually the same objects, while string_1 and string_2 aren’t, despite their contents being the same.
5. A parameterless function named super() returns a reference to the nearest superclass of the class. For example:
|
1 2 3 4 5 6 7 8 9 10 |
class Mouse: def __str__(self): return "Mouse" class LabMouse(Mouse): def __str__(self): return "Laboratory " + super().__str__() doctor_mouse = LabMouse(); print(doctor_mouse) # Prints "Laboratory Mouse". |
Note: you can use this mechanism not only to invoke the superclass constructor, but also to get access to any of the resources available inside the superclass.
6. Methods as well as instance and class variables defined in a superclass are automatically inherited by their subclasses. For example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Mouse: Population = 0 def __init__(self, name): Mouse.Population += 1 self.name = name def __str__(self): return "Hi, my name is " + self.name class LabMouse(Mouse): pass professor_mouse = LabMouse("Professor Mouser") print(professor_mouse, Mouse.Population) # Prints "Hi, my name is Professor Mouser 1" |
7. In order to find any object/class property, Python looks for it inside:
- the object itself;
- all classes involved in the object’s inheritance line from bottom to top;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Level1: var = 100 def fun(self): return 101 class Level2(Level1): var = 200 def fun(self): return 201 class Level3(Level2): pass obj = Level3() print(obj.var, obj.fun()) |
200 201
- if there is more than one class on a particular inheritance path, Python scans them from left to right;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Left: var = "L" var_left = "LL" def fun(self): return "Left" class Right: var = "R" var_right = "RR" def fun(self): return "Right" class Sub(Left, Right): pass obj = Sub() print(obj.var, obj.var_left, obj.var_right, obj.fun()) |
L LL RR Left
- if both of the above fail, the
AttributeErrorexception is raised.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Level1: variable_1 = 100 def __init__(self): self.var_1 = 101 def fun_1(self): return 102 class Level2(Level1): variable_2 = 200 def __init__(self): super().__init__() self.var_2 = 201 def fun_2(self): return 202 class Level3(Level2): variable_3 = 300 def __init__(self): super().__init__() self.var_3 = 301 def fun_3(self): return 302 obj = Level3() print(obj.variable_1, obj.var_1, obj.fun_1()) print(obj.variable_2, obj.var_2, obj.fun_2()) print(obj.variable_3, obj.var_3, obj.fun_3()) |
100 101 102
200 201 202
300 301 302
8. If any of the subclasses defines a method/class variable/instance variable of the same name as existing in the superclass, the new name overrides any of the previous instances of the name. For example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Mouse: def __init__(self, name): self.name = name def __str__(self): return "My name is " + self.name class AncientMouse(Mouse): def __str__(self): return "Meum nomen est " + self.name mus = AncientMouse("Caesar") print(mus) # Prints "Meum nomen est Caesar" |
9. The situation in which the subclass is able to modify its superclass behavior (just like in the example) is called polymorphism. Polymorphism helps the developer to keep the code clean and consistent.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class One: def do_it(self): print("do_it from One") def doanything(self): self.do_it() class Two(One): def do_it(self): print("do_it from Two") one = One() two = Two() one.doanything() two.doanything() |
do_it from One
do_it from Two
Example:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class A: def __init__(self, v=2): self.v = v def set(self, v=1): print("self.v=", self.v) self.v += v return self.v a = A() b = a print("b=", b.v) b.set() print("a=", a.v) |
b= 2
self.v= 2
a= 3
Exercise 1
Can you name one of your classes just “class”?
No, you can’t – class is a keyword!
Exercise 2
Assuming that there is a class named Snakes, write the very first line of the Python class declaration, expressing the fact that the new class is actually a subclass of Snake.
|
1 |
class Python(Snakes): |
Exercise 3
Something is missing from the following declaration – what?
|
1 2 3 |
class Snakes: def __init__(): self.sound = 'Sssssss' |
The __init__() constructor lacks the obligatory parameter (we should name it self to stay compliant with the standards).
Exercise 4
Modify the code to guarantee that the venomous property is private.
|
1 2 3 |
class Snakes: def __init__(self): self.venomous = True |
The code should look as follows:
|
1 2 3 |
class Snakes: def __init__(self): self.__venomous = True |
Exercise 5
Which of the Python class properties are instance variables and which are class variables? Which of them are private?
|
1 2 3 4 5 6 |
class Python: population = 1 victims = 0 def __init__(self): self.length_ft = 3 self.__venomous = False |
population and victims are class variables, while length and __venomous are instance variables (the latter is also private).
Exercise 6
You’re going to negate the __venomous property of the version_2 object, ignoring the fact that the property is private. How will you do this?
|
1 |
version_2 = Python() |
|
1 |
version_2._Python__venomous = not version_2._Python__venomous |
Exercise 7
Write an expression which checks if the version_2 object contains an instance property named constrictor (yes, constrictor!).
|
1 |
hasattr(version_2, 'constrictor') |
Exercise 8
The declaration of the Snake class is given below. Enrich the class with a method named increment(), adding 1 to the victims property.
|
1 2 3 |
class Snake: def __init__(self): self.victims = 0 |
|
1 2 3 4 5 6 |
class Snake: def __init__(self): self.victims = 0 def increment(self): self.victims += 1 |
Exercise 9
Redefine the Snake class constructor so that is has a parameter to initialize the victims field with a value passed to the object during construction.
|
1 2 3 |
class Snake: def __init__(self, victims): self.victims = victims |
Exercise 10
Can you predict the output of the following code?
|
1 2 3 4 5 6 7 8 |
class Snake: pass class Python(Snake): pass print(Python.__name__, 'is a', Snake.__name__) print(Python.__bases__[0].__name__, 'can be a', Python.__name__) |
Python is a Snake
Snake can be a Python
Exercises
Scenario
Assume that the following piece of code has been successfully executed:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Dog: kennel = 0 def __init__(self, breed): self.breed = breed Dog.kennel += 1 def __str__(self): return self.breed + " says: Woof!" class SheepDog(Dog): def __str__(self): return super().__str__() + " Don't run away, Little Lamb!" class GuardDog(Dog): def __str__(self): return super().__str__() + " Stay where you are, Mister Intruder!" rocky = SheepDog("Collie") luna = GuardDog("Dobermann") |
Exercise 1
What is the expected output of the following piece of code?
|
1 2 |
print(rocky) print(luna) |
Collie says: Woof! Don't run away, Little Lamb!
Dobermann says: Woof! Stay where you are, Mister Intruder!
Exercise 2
What is the expected output of the following piece of code?
|
1 2 |
print(issubclass(SheepDog, Dog), issubclass(SheepDog, GuardDog)) print(isinstance(rocky, GuardDog), isinstance(luna, GuardDog)) |
True False
False True
Exercise 3
What is the expected output of the following piece of code?
|
1 2 |
print(luna is luna, rocky is luna) print(rocky.kennel) |
True False
2
Exercise 4
Define a SheepDog‘s subclass named LowlandDog, and equip it with an __str__() method overriding an inherited method of the same name. The new dog’s __str__() method should return the string “Woof! I don’t like mountains!” .
|
1 2 3 |
class LowlandDog(SheepDog): def __str__(self): return Dog.__str__(self) + " I don't like mountains!" |

