Object-Oriented Programming Part 2
Data Hiding
A key part of object-oriented programming is encapsulation, which involves packaging of related variables and functions into a single easy-to-use object - an instance of a class. A related concept is data hiding, which states that implementation details of a class should be hidden, and a clean standard interface be presented for those who want to use the class. In other programming languages, this is usually done with private methods and attributes, which block external access to certain methods and attributes in a class. The Python philosophy is slightly different. It is often stated as "we are all consenting adults here", meaning that you shouldn't put arbitrary restrictions on accessing parts of a class. Hence there are no ways of enforcing a method or attribute be strictly private. However, there are ways to discourage people from accessing parts of a class, such as by denoting that it is an implementation detail, and should be used at their own risk.
Magic Methods
Magic methods are special methods which have double underscores at the beginning and end of their names. They are also known as dunders. So far, the only one we have encountered is __init__, but there are several others. They are used to create functionality that can't be represented as a normal method. One common use of them is operator overloading. This means defining operators for custom classes that allow operators such as + and * to be used on them. An example magic method is __add__ for +. class Vector2D: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Vector2D(self.x + other.x, self.y + other.y) first = Vector2D(5, 7) second = Vector2D(3, 9) result = first + second print(result.x) print(result.y) Result: >>> 8 16 >>>
More on Object Lifecycle
Object Lifecycle When an object is destroyed, the memory allocated to it is freed up, and can be used for other purposes. Destruction of an object occurs when its reference count reaches zero. Reference count is the number of variables and other elements that refer to an object. If nothing is referring to it (it has a reference count of zero) nothing can interact with it, so it can be safely deleted. In some situations, two (or more) objects can be referred to by each other only, and therefore can be deleted as well. The del statement reduces the reference count of an object by one, and this often leads to its deletion. The magic method for the del statement is __del__. The process of deleting objects when they are no longer needed is called garbage collection. In summary, an object's reference count increases when it is assigned a new name or placed in a container (list, tuple, or dictionary). The object's reference count decreases when it's deleted with del, its reference is reassigned, or its reference goes out of scope. When an object's reference count reaches zero, Python automatically deletes it. Example: a = 42 # Create object <42> b = a # Increase ref. count of <42> c = [a] # Increase ref. count of <42> del a # Decrease ref. count of <42> b = 100 # Decrease ref. count of <42> c[0] = -1 # Decrease ref. count of <42> Lower level languages like C don't have this kind of automatic memory management.
More on Magic Methods
Python also provides magic methods for comparisons. __lt__ for < __le__ for <= __eq__ for == __ne__ for != __gt__ for > __ge__ for >= If __ne__ is not implemented, it returns the opposite of __eq__. There are no other relationships between the other operators. Example: class SpecialString: def __init__(self, cont): self.cont = cont def __gt__(self, other): for index in range(len(other.cont)+1): result = other.cont[:index] + ">" + self.cont result += ">" + other.cont[index:] print(result) spam = SpecialString("spam") eggs = SpecialString("eggs") spam > eggs Result: >>> >spam>eggs e>spam>ggs eg>spam>gs egg>spam>s eggs>spam> >>> As you can see, you can define any custom behavior for the overloaded operators.
More on Data Hiding
Strongly private methods and attributes have a double underscore at the beginning of their names. This causes their names to be mangled, which means that they can't be accessed from outside the class. The purpose of this isn't to ensure that they are kept private, but to avoid bugs if there are subclasses that have methods or attributes with the same names. Name mangled methods can still be accessed externally, but by a different name. The method __privatemethod of class Spam could be accessed externally with _Spam__privatemethod. Example: class Spam: __egg = 7 def print_egg(self): print(self.__egg) s = Spam() s.print_egg() print(s._Spam__egg) print(s.__egg) Result: >>> 7 7 AttributeError: 'Spam' object has no attribute '__egg' >>> Basically, Python protects those members by internally changing the name to include the class name.
Object Lifecycle
The lifecycle of an object is made up of its creation, manipulation, and destruction. The first stage of the life-cycle of an object is the definition of the class to which it belongs. The next stage is the instantiation of an instance, when __init__ is called. Memory is allocated to store the instance. Just before this occurs, the __new__ method of the class is called. This is usually overridden only in special cases. After this has happened, the object is ready to be used. Other code can then interact with the object, by calling functions on it and accessing its attributes. Eventually, it will finish being used, and can be destroyed.
Magic Method 3 using it like containers
There are several magic methods for making classes act like containers. __len__ for len() __getitem__ for indexing __setitem__ for assigning to indexed values __delitem__ for deleting indexed values __iter__ for iteration over objects (e.g., in for loops) __contains__ for in There are many other magic methods that we won't cover here, such as __call__ for calling objects as functions, and __int__, __str__, and the like, for converting objects to built-in types. Example: import random class VagueList: def __init__(self, cont): self.cont = cont def __getitem__(self, index): return self.cont[index + random.randint(-1, 1)] def __len__(self): return random.randint(0, len(self.cont)*2) vague_list = VagueList(["A", "B", "C", "D", "E"]) print(len(vague_list)) print(len(vague_list)) print(vague_list[2]) print(vague_list[2]) Result: >>> 6 7 D C >>> We have overridden the len() function for the class VagueList to return a random number. The indexing function also returns a random item in a range from the list, based on the expression.
Data Hiding 2
Weakly private methods and attributes have a single underscore at the beginning. This signals that they are private, and shouldn't be used by external code. However, it is mostly only a convention, and does not stop external code from accessing them. Its only actual effect is that from module_name import * won't import variables that start with a single underscore. Example: class Queue: def __init__(self, contents): self._hiddenlist = list(contents) def push(self, value): self._hiddenlist.insert(0, value) def pop(self): return self._hiddenlist.pop(-1) def __repr__(self): return "Queue({})".format(self._hiddenlist) queue = Queue([1, 2, 3]) print(queue) queue.push(0) print(queue) queue.pop() print(queue) print(queue._hiddenlist) Result: >>> Queue([1, 2, 3]) Queue([0, 1, 2, 3]) Queue([0, 1, 2]) [0, 1, 2] >>> In the code above, the attribute _hiddenlist is marked as private, but it can still be accessed in the outside code. The __repr__ magic method is used for string representation of the instance
More magic methods for common operations
__sub__ for - __mul__ for * __truediv__ for / __floordiv__ for // __mod__ for % __pow__ for ** __and__ for & __xor__ for ^ __or__ for | The expression x + y is translated into x.__add__(y). However, if x hasn't implemented __add__, and x and y are of different types, then y.__radd__(x) is called. There are equivalent r methods for all magic methods just mentioned. Example: class SpecialString: def __init__(self, cont): self.cont = cont def __truediv__(self, other): line = "=" * len(other.cont) return "\n".join([self.cont, line, other.cont]) spam = SpecialString("spam") hello = SpecialString("Hello world!") print(spam / hello) Result: >>> spam ============ Hello world! >>> In the example above, we defined the division operation for our class SpecialString.