Monday, May 11, 2020

OOP in Python


In this post I'm going to show some basics of OOP in Python.

1. Classes (which are actually objects) and access modifiers

To define class in Python we should unsurprisingly use keyword class:

class User:

To use constructor we should use "init" function:
def __init__(self, name, age):

The first parameter: self is a reference to object instance. Next parameters are actually constructor parameters. Thus we have 2 parameters here and we should define them in instance variable:

self.name = name
self.age = age


But what about visibility of them? public? private? Actually everything is public. We can use some "hacks" for private:
 - we can add leading underscore - it's just a convention that field is private, nothing is technically stopping us for accessing and changing this field
 - we can add 2 leading underscores - this is better option: Python will rename this field by pattern "_className__paramName". So field will not be visible by it's name, but will still be visible by pattern i just described.

Let's try:

class User:
def __init__(self, name, age):
self.name = name # public field
self._name = name # naive private field
self.__name = name # better private field
self.__age = age # field defined as "property" with custom SETTER

def sayHi(self):
print("Hello, I'm {} and I'm {} years old.".format(self.__name, self.__age))



And now let's play with it. Outputs are shown as comments:

joe = User("Joe", 25)
joe.sayHi() # Hello, I'm Joe and I'm 25 years old.

# playing with public field
print(joe.name) # Joe
joe.name = "Joe1"
print(joe.name) # Joe1

# playing with naive private field
print(joe._name) # Joe
joe._name = "Joe2"
print(joe._name) # Joe2


# playing with better private field
#print(joe.__name) # AttributeError: 'User' object has no attribute '__name'
print(joe._User__name)
joe._User__name = "Joe3"
print(joe._User__name) # Joe3



As you can see, everything is actually public but using 2 underscores we have at least some protection.

2. Addons provided by decorators

More advanced things are provided by decorators. For example if we need something like static method which of course don't have instance reference (self), we can do it this way:

@staticmethod
def say(msg):
print("I'm saying:", msg)


Let's try it:
User.say("Yo!") # I'm saying: Yo!


Also we can use additional decorators for having something like getter and setter:


@property
def age(self):
return self.__age

@age.setter
def age(self, newValue):
print("age can not be changed!")

That is more interesting from a "modifiers" point of view: we can not make variable invisible, but at least can protect them from being changed()!

Let's see:

print(joe.age) # 25
joe.age = 55 # age can not be changed!
print(joe.age) # 25

joe._User__age = 66
print(joe.age) # 66


When we tried to use field directly: protection worked fine. But when we tried to access used pattern ..... protection failed: we successfully changed the value.



3. Inheritance

To extend a class we should provide it name in bracers:
class SuperUser(User):


to call some method from patent classes we can use method "super()":

class SuperUser(User):
def __init__(self, name, age, role):
super().__init__(name, age)
self.__role = role

def sayHi(self):
super().sayHi()
print(" and my role is: {}".format(self.__role))

Let's try it:

admin = SuperUser("Sysdba", 44, "IT")
admin.sayHi() # Hello, I'm Sysdba and I'm 44 years old.
# and my role is: IT

With inheritance everything is more or less as expected.

4. The end

Of course, Python is not a fully OOP language but we still use some basics OOP stuff.