Object Oriented Programming in Python
Alexey Ulashchick
EPAM Systems
March 2014
Agenda
1. Classes and Instances
2. Attribute accessing
3. Descriptors
4. OOP Principles
5. Desing Patterns
A user-defined prototype for an object that defines a set of attributes that characterize any object of the class
CLASS
OBJECT
A unique instance of a data structure that's defined by its class
- Each class statement generates a new class object.
- Each time a class is called, it generates a new instance object.
- Instances are automatically linked to the classes from which they are created.
- Classes are automatically linked to their superclasses according to the way we list them in parentheses in a class header line; the left-to-right order there gives the order in the tree.
Object & Class init cycle
- Class has a documentation string, which can be accessed via ClassName.__doc__.
- Class body consists of all the component statements defining class members, data attributes and functions.
Class Creating
class ClassName:
'Optional class documentation string'
pass
- The variable empCount is a class variable whose value would be shared among all instances of a this class. This can be accessed as Employee.empCount from inside the class or outside the class.
- The first method __init__() is a special method, which is called class constructor or initialization method that Python calls when you create a new instance of this class.
- You declare other class methods like normal functions with the exception that the first argument to each method is self. Python adds the self argument to the list for you; you don't need to include it when you call the methods.
Class Example
class Employee:
'Common base class for all employees'
empCount = 0
def __init__(self, name, salary):
self.name = name
self.salary = salary
Employee.empCount += 1
def displayCount(self):
print "Total Employee %d" % \
Employee.empCount
def displayEmployee(self):
print "Name : ", self.name, \
", Salary: ", self.salaryInstance Methods
class Inst:
def __init__(self, name):
self.name = name
def introduce(self):
print "Hello, my name is " + self.nameWhen creating an instance method, the first parameter is always self. You can name it anything you want, but the meaning will always be the same, and you should use self since it's the naming convention. self is (usually) passed hiddenly when calling an instance method; method has access to other instance variables and methods using self
Class Methods
class Cls:
name = 'World'
@classmethod
def introduce(cls):
print "Hello, I am %s!" % cls.nameIn class methods we pass the class itself as a first parameter.
Class method doesn't have access to instance
Should be called using class name:
Cls.introduce()Static Methods
class Cls:
@staticmethod
def introduce():
print "Hello, I am static method"Static method mounted to the class but doesn't have access neither to class, no instance.
Cls.introduce()__dict__ : Dictionary containing the class's namespace.
__doc__ : Class documentation string or None if undefined.
__name__: Class name.
__module__: Module name in which the class is defined. This attribute is "__main__" in interactive mode.
__bases__: A possibly empty tuple containing the base classes, in the order of their occurrence in the base class list
Built-In Class Attributes
Instance Creation & Attributes accessing
"This would create first object of Employee class"
emp1 = Employee("Zara", 2000)
"This would create second object of Employee class"
emp2 = Employee("Manni", 5000)
emp1.displayEmployee()
emp2.displayEmployee()
print "Total Employee %d" % Employee.empCountgetattr(obj, name[, default]) : to access the attribute of object.
hasattr(obj,name) : to check if an attribute exists or not.
setattr(obj,name,value) : to set an attribute. If attribute does not exist, then it would be created.
delattr(obj, name) : to delete an attribute.
Alternative element accessing
hasattr(emp1, 'salary') # Returns true if 'salary' attribute exists
getattr(emp1, 'salary') # Returns value of 'salary' attribute
getattr(emp1, 'salary', 0) # Returns value of 'salary' attribute when exists or 0
setattr(emp1, 'salary', 8) # Set attribute 'salary' at 8
delattr(emp1, 'salary') # Delete attribute 'salary'
getattr(emp1, 'empCount') # Search attribute in instance. When fail,
# continue search in classAttributes searching
instance
__dict__
instance class
__dict__
class ancestor
__dict__
Attribute accessing pitfall
class Car:
wheels = 4
def __init__(self, color):
self.color = color
car1 = Car('blue')
car2 = Car('red')
print car1.wheels # Prints 4
print car2.wheels # Prints 4
car1.wheels = 10 # Set wheels count to 10
print car1.wheels # Prints 10
print car2.wheels # 4
print Car.wheels # 4Object-Oriented Approach
OOP
Encapsulation
Polymorphism
Inheritance
Object-oriented programming (OOP) is a programming language model organized around objects rather than "actions" and data rather than logic
Encapsulation
class Person:
def __init__(self, name):
self._name = name
def set_name(self, name):
self._name = 'Mr. '+ name
def get_name(self, name):
return ['<', self._name, '>'].join(' ')Control attributes via @property
class Deck(object):
def __init__(self):
self._cards = []
@property
def card( self ):
return self._cards
@card.setter
def card( self, aCard ):
self._cards.append( aCard )
@card.deleter
def card( self ):
self._cards.pop(-1)Since setter (and deleter) properties are created from the getter property, we must always define a getter property first
Descriptors
- A descriptor is a class that mediates attribute access.
- The descriptor class can be used to get, set, or delete attribute values.
- Descriptor objects are built inside a class at class definition time.
To be recognized as a descriptor, a class must implement any combination of the following three methods.
__get__(self, instance, owner) → object
__set__(self, instance, value)
__delete__(self, instance)
Descriptors usage example
class Descriptor(object):
def __init__(self):
self._name = ''
def __get__(self, instance, owner):
print "Getting: %s" % self._name
return self._name
def __set__(self, instance, name):
print "Setting: %s" % name
self._name = name.title()
def __delete__(self, instance):
print "Deleting: %s" %self._name
del self._name
class Person(object):
name = Descriptor()Inheritance
Inheritance is when an object or class is based on another object or class, using the same implementation (inheriting from a class) specifying implementation to maintain the same behavior
Inheritance is used to indicate that one class will get most or all of its features from a parent class.
When you are doing this kind of specialization, there are three ways that the parent and child classes can interact:
- Actions on the child imply an action on the parent.
- Actions on the child override the action on the parent.
- Actions on the child alter the action on the parent.
Implicit Inheritance
Method defined in parent, but not in child
class Parent(object):
def implicit(self):
print "PARENT implicit()"
class Child(Parent):
pass
dad = Parent()
son = Child()
dad.implicit() # print PARENT implicit()
son.implicit() # print PARENT implicit()Child will just extend Parent and add some behavior
(Methods, Properties)
Override Explicitly - Polymorphism
class Parent(object):
def override(self):
print "PARENT"
class Child(Parent):
def override(self):
print "CHILD"
dad = Parent()
son = Child()
dad.override() #print PARENT
son.override() #print CHILDThe problem with having functions called implicitly is sometimes you want the child to behave differently. In this case you want to override the function in the child, effectively replacing the functionality. To do this just define a function with the same name in Child.
Alter Before or After
class Parent(object):
def altered(self):
print "PARENT"
class Child(Parent):
def altered(self):
print "BEFORE ",
super(Child, self).altered()
print " AFTER",
dad = Parent()
son = Child()
dad.altered() #print PARENT
son.altered() #print BEFORE PARENT AFTERThe third way to use inheritance is a special case of overriding where you want to alter the behavior before or after the Parent class's version runs. You first override the function just like in the last example, but then you use a Python built-in function named super to get the Parent version to call
Multiple inheritance
class A:
def show_message(self):
print "I’m class A"
class B:
def show_message(self):
print "I’m class B"
class C(A,B):
pass
instance = C()
instance.show_message()Class C that inherits from classes A and B at the same time.
In this case, whenever you have implicit actions on instance, Python has to look-up the possible function in the class hierarchy for both A and B subclasses, but it needs to do this in a consistent order. To do this Python uses "method resolution order" (MRO) and an algorithm called C3 to get it straight.
Method Resolution Order
class A: x = 'a'
class B(A): pass
class C(A): x = 'c'
class D(B, C): pass
print D.x # print 'a'class A(object): x = 'a'
class B(A): pass
class C(A): x = 'c'
class D(B, C): pass
print D.x # print 'a’MRO: D - B - A - C – A
MRO: D - B – C – A
So when looking up D.x, A is the first base in resolution order to solve it, thereby hiding the definition in C
A forced to come in resolution order only once and after all of its subclasses
You able to use __class__.mro() in new-style classes to observe resolution order
Patterns
Singleton
class Singleton(object):
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
if __name__ == '__main__':
s1=Singleton()
s2=Singleton()
if s1 == s2:
print "Same"
else:
print "Different"Singleton pattern is a design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system.
Chaining method
class Person:
def change_name(self, value):
self.name = value
return self
def set_age(self, value):
self.age = value
return self
def introduce(self):
print "Hello, my name is", self.name, "and I am", self.age, "years old."
person = Person()
person.change_name("Peter").set_age(21).introduce()Each method returns an object, allowing the calls to be chained together in a single statement.
Chaining is syntactic sugar which eliminates the need for intermediate variables.
Factory: Encapsulating Object Creation
class HugeBusinessObject(object):
def __init__(self, name, surname, job_title):
self.name, self.surname, self.job_title = name, surname, job_title
def transform(self, condition):
# some transformation logic
pass
class Factory(object):
@staticmethod
def get_simple_person():
return HugeBusinessObject('Mike', 'Petrov', 'cleaner')
@staticmethod
def get_complex_person():
obj = HugeBusinessObject('Vasya', 'Ivanov', 'manager')
obj.transform()
return objPattern strive to aggregate object creation and initialization in separated factory classes.
Decorator
def make_bold(fn):
def wrapped(*args, **kwargs):
return ''.join(['<b>', fn(*args, **kwargs), '</b>'])
return wrapped
class Dom:
def __init__(self, text):
self.text = text
@make_bold
def capitalized(self):
return self.text.upper()
print Dom('hello').capitalized() # <b>HELLO</b>
Decorator pattern (also known as Wrapper) is a design pattern that allows behavior to be added to an object
Memoization decorator
def memoize(f):
def wrapped(*args, **kwargs):
if hasattr(wrapped, '_cached_val'):
return wrapped._cached_val
result = f(*args, **kwargs)
wrapped._cached_val = result
return result
return wrapped
@memoize
def expensive_function():
print "Computing expensive function..."
Memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
Strategy
class StrategyExample :
def __init__(self, func=None) :
if func :
self.execute = func
def execute(self) :
print "Original execution"
def executeReplacement1() :
print "Strategy 1"
def executeReplacement2() :
print "Strategy 2"
if __name__ == "__main__" :
strat0 = StrategyExample()
strat1 = StrategyExample(executeReplacement1)
strat2 = StrategyExample(executeReplacement2)
strat0.execute()
strat1.execute()
strat2.execute()
Strategy pattern (also known as the policy pattern) is a software design pattern that enables an algorithm's behavior to be selected at runtime. The strategy pattern
- defines a family of algorithms,
- encapsulates each algorithm, and
- makes the algorithms interchangeable within that family.
Best practices
- test inheritance and data-objects inheritance
- just 2 line test steps
- multi-layer architecture
- build a service layer
- separate things a much as possible
Test inheritance
class BaseTest(object):
def before(self):
# Initialization (open connection, crete huge instances, prepare data)
pass
def after(self):
# Finalization (clos connections, destory instances)
pass
class Test1(BaseTest):
def test_check_1(self):
pass
def test_check_2(self):
pass
def test_check_3(self):
pass
"2 line" tests
class Test1(BaseTest):
def test_check_1(self):
result = make_action()
assert.check_data(expected, result)
def test_check_2(self):
result = make_action_2()
assert.check_data(expected, result)
Multi-layer structure
Test
Service
API
Testing object
Questions?
Thank you!
PythonOOP
By Alexey Ulashchick
PythonOOP
- 2,095