OOP, Encapsulation, @Propeties
In python all attributes are public.
[5]:
class P:
def __init__(self,x):
self.x = x # self.x -> public attribute
p = P(10)
p.x # accessing the public attribute
[5]:
10
You will sometimes see an attribute with a single or a double underscore prior to the name
self._x = x
When you read about this it will be described as a private “like” variable, and then a long explanation about that there is not realy anything private in python and so on.
In reality it is all just public attributes and some of them with a funny looking syntax.
[9]:
class P:
def __init__(self,x):
self._x = x
p = P(10)
p._x
[9]:
10
In order to work with attributes the right way in python you need to understand that all attributes are public, and you need to accept and understand that this is actually a good thing, and it is NOT a limitation Forget all about the Java approach with private attributes and getters and setters. There is another logic behind that approach.
@Property
Read about : Properties vs. Getters and Setters
[4]:
class Number:
def __init__(self, value):
self.x = value
[5]:
num = Number(12)
[7]:
num.x
[7]:
12
Q: What if vaalue should be between 0 and 100?
What about encapsulation?
Problem could be that data is not encapsulated
Using the getter and setter and having encapsulated data looks like this
The Java style approach
[8]:
class P:
def __init__(self, x):
self.set_x(x)
def get_x(self):
return self._x
def set_x(self, x):
if x > 1000:
self._x = 1000
elif x < 0:
self._x = 0
else:
self._x = x
[9]:
p1 = P(3)
p2 = P(1000)
[10]:
p1.set_x(p1.get_x() + p2.get_x())
[11]:
p1.get_x()
[11]:
1000
But, This is much cleaner and more pythonic
p1.x = p1.x + p2.x
than this:
p1.set_x(p1.get_x() + p2.get_x())
@properties solves the problem
[13]:
class P:
def __init__(self, x):
self.x = x # self.x is now the @x.setter
@property
def x(self):
return self._x # this is a private variable
@x.setter
def x(self, x):
if x > 1000:
self._x = 1000
elif x < 0:
self._x = 0
else:
self._x = x
[14]:
p1 = P(-1)
p2 = P(1001)
[15]:
p1.x
[15]:
0
[16]:
p2.x
[16]:
1000
[17]:
p1.x = 101
p1.x = p1.x + p2.x
p1.x
[17]:
1000
Datamodel
Read about this: A Guide to Python’s Magic Methods
Protocol:
Top-level function or top-level syntax has a corosponding __method__()
When you initialize and object, there is a corosponding __init__() method that is called
[1]:
class S:
def __init__(self):
pass
s = S()
If you want a string representation of the object (the objects state), you call the top-level function repr() which has a corosponding implementation of __repr__
[20]:
class S:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'{self.__dict__}'
s = S('Claus')
repr(s)
[20]:
"{'name': 'Claus'}"
Read about this: str() vs repr() in Python
[2]:
class S:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'{self.__dict__}'
def __str__(self):
return f'Name: {self.name}'
s = S('Anna')
str(s)
[2]:
'Name: Anna'
[5]:
print(s)
Name: Anna
[4]:
repr(s)
[4]:
"{'name': 'Anna'}"
[6]:
s
[6]:
{'name': 'Anna'}
2 lists can be added together by using the + operator
[3]:
l1 = [1, 2, 3]
l2 = [4, 5, 6]
l1 + l2
[3]:
[1, 2, 3, 4, 5, 6]
[4]:
class S:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'{self.__dict__}'
def __str__(self):
return f'Name: {self.name}'
def __add__(self, other):
return S(f'{self.name} {other.name}')
s = S('Claus')
s2 = S('Anna')
str(s + s2)
[4]:
'Name: Claus Anna'
A list in python can be accessed like with this syntax:
[5]:
l1[2]
[5]:
3
This is because a list implements the method
[6]:
def __getitem__(self):
pass
Your own object, if implementing this method can have the same behaviour
[1]:
class Deck:
def __init__(self):
self.cards = ['A', 'K', 4, 7]
def __getitem__(self, key):
return self.cards[key]
def __repr__(self):
return f'{self.__dict__}'
def __len__(self):
return len(self.cards)
[2]:
d = Deck()
d[3]
[2]:
7
[ ]: