Access modifiers
You can't have oop without clases, python does support clases, it's support many features of oop, so can you guess if it support access modifiers like, private, public, protected?
You guess it, it doesn't. In python you don't have private, protected or public.
What we have are naming conventions:
class A: _a = 4 __b =5 print(A._a) # 4 pprint(A.__dict__) # mappingproxy({'_A__b': 5, # '__dict__': <attribute '__dict__' of 'A' objects>, # '__doc__': None, # '__module__': '__main__', # '__weakref__': <attribute '__weakref__' of 'A' objects>, # '_a': 4})print(A._A__b) # 5print(A.__b) # raises AttributeError
A variable with one single underscore can be used for private, but it's not hiding anything we can always access it, after all is just a naming convention but it has one usecase when we import from a module:
_single_leading_underscore: weak "internal use" indicator. E.g. from M import * does not import objects whose names start with an underscore. ( PEP 8)
From our example A.__b raises an exception, because the attrite name is _A__b and we get an 5.
Variable with double leading underscores in classes become _classname__variablename.
__double_leading_underscore: when naming a class attribute, invokes name mangling ( PEP 8)
Encapsulation
Polymorphism
import java.util.ArrayList; class A {} class B extends A {} class C extends A {} public class Example{ public static void main(String []args){ ArrayList<A> objects = new ArrayList<>(); objects.add(new A()); objects.add(new B()); objects.add(new C()); System.out.println(objects.size()); } }
The output is 3, meaning that the code is compiling and running. We have an array named objects, that we specify by using generics that only objects of type A should be allowed in this list. Because of polymorphism, classes that inherit from class A, can also be added to the objects array.
In Java we need to specify every variable's data type because is a strong typed language, python is dynamically typed language. We can easily do the following in python:
my_list = [HttpServer(), 1, True, "string", DatabaseConnection(), MysqlConnector(), open('/etc/file.txt')]
In a list in python we can add what we want, primiteves, all kind of objects, nobody stops us.
So, polymorphism in python can't be proven, because is dynamically typed language.
Inheritance, diamond problem and mro
class A: def hello(self): print('A') class B(A): pass B().hello() # A print(isinstance(B(), A)) # True
Class B, doesn't have any method defined, so hello() is inheritaded from A, there is printing A.
Using isinstance() we can see that, instances of B are also instances of A, becauses B inherits from A and this is good, using inheritanecs and Liskov substitution principle we can build better software, the principles says that subclasses can replace the parent clases without breaking anything.
Let's try the diamond problem in Python and see what happens:
class A: def hello(self): print('A') class B(A): def hello(self): print('B') class C(A): def hello(self): print('C') class D(B, C): pass D().hello() # B help(D) #output from help(D) # Help on class D in module __main__: # # class D(B, C) # | Method resolution order: # | D # | B # | C # | A # | builtins.object # | # | Methods inherited from B: # | # | hello(self) # | # | ---------------------------------------------------------------------- # | Data descriptors inherited from A: # | # | __dict__ # | dictionary for instance variables (if defined) # | # | __weakref__ # | list of weak references to the object (if defined) print(D.__mro__) # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
The code runs and calling hello() on D, prints B. How does python knows what method to call from B or C? It's using the C3 linearization algorithm to determine the method resolution order (mro).
We can run help() and the Method resolution order is printed or we can use the __mro__ attribute.
So this is all nice and interesting but can we do something usefull with this?
Dependency injection using multiple inheritance
class FileStorage: def store_data(self, data): print(f"Writing {data} to file") class Message(FileStorage): def __init__(self, text, encoding): self.text = text self.encoding = encoding def save(self): """ Writes the message to a persistent storage :return: """
# do some processing, calcualtion, add to a queue and then store it
super().store_data(json.dumps({
'text': self.text.decode('utf-8'),
'encoding': self.encoding
}))
message = Message(text=base64.b64encode(b'AAAa'), encoding='base64')
message.save()
# Writing {"text": "QUFBYQ==", "encoding": "base64"} to file
We have a class Message, that has a method save(). In save() we can do some processing and then we call super().store_data() to save the message to a persistant storage system. Very simple.
What if we want now to save the message to a temporary file directory?
class TemporaryFileStorage(FileStorage): def store_data(self, data): print(f"Writing {data} to temp storage") class TemporaryStoredMessage(Message, TemporaryFileStorage): """A message that is stored into the temp storage""" message = TemporaryStoredMessage(text=base64.b64encode(b'AAAa'), encoding='base64') message.save() # Writing {"text": "QUFBYQ==", "encoding": "base64"} to temp storage
We create our TemporaryFileStorage class and our TemporaryStoredMessage inherits from both Message and TemporaryFileStorage.
The call to super() inside Message.save() will search the method in the mro chain and when found it calls it, that's why the output is saying "to temp storage". Let's print the mro chain:
pprint(TemporaryStoredMessage.__mro__) # (<class '__main__.TemporaryStoredMessage'>, # <class '__main__.Message'>, # <class '__main__.TemporaryFileStorage'>, # <class '__main__.FileStorage'>, # <class 'object'>)
First is our child class TemporaryStoredMessage, then from left to right, Message and then our TemporaryFileStorage. When we are in the save() method in Message(), super() will begin it's search at TemporaryFileStorage and uses the method from there instead from FileStorage class.
Abstraction
class C(ABC): @abstractmethod def my_abstract_method(self, ...): ... @classmethod @abstractmethod def my_abstract_classmethod(cls, ...): ... @staticmethod @abstractmethod def my_abstract_staticmethod(...): ... @property @abstractmethod def my_abstract_property(self): ... @my_abstract_property.setter @abstractmethod def my_abstract_property(self, val): ... @abstractmethod def _get_x(self): ... @abstractmethod def _set_x(self, val): ... x = property(_get_x, _set_x)
We inherit from the ABC class and the we use decorators like @abstractmethod to mark an method abstract that the implementation has to be provided by the child class. You can also have abstractproperty.
Besides inheritance we can also use register() but there is something to keep in mind about register.
from abc import * class StorageSystem(ABC): @abstractmethod def read(self): pass class Database: def write(self, data): print(f"Writing {data} to database") class FileStorage(StorageSystem): def read(self, file): print(f"Reading data from {file}")
So let's say that we have those classes. Let's play around a little bit.
StorageSystem.register(Database) database = Database() print(StorageSystem.__subclasses__()) # [<class '__main__.FileStorage'>] print(isinstance(database, StorageSystem)) # True database.read('data') # AttributeError: 'Database' object has no attribute 'read'
First is the call to register(). Now the class Database is what python calls a "“virtual subclass” of StorageSystem. This means that our isinstance() returns True when we ask if our database object is a instance of type StorageSystem.
But virtual subclasses are not part of __subclasses__(), they are not returned in the list only subclasses using inheritance are.
Another imporant thing is that now we have Database being child of StorageSystem but StorageSystem has read() implemented as abstract and we don't have it in Database and we don't have to have it, there is no checking of the method existing, the call to read() fails with an AttributeError.
When using register() there is no checking being done by python.
Class method, variables and @staticmethod
class User: count = 0 def __init__(self, name): self.name = name User.count += 1 @classmethod def number_of_users(cls): return cls.count @classmethod def from_dict(cls, user): return cls(user['name']) @staticmethod def is_valid(name): return len(name) > 3 user1 = User('user1') user2 = User('user2') user3 = User.from_dict({ 'name': 'user3' }) print(User.number_of_users()) # 3 print(User.is_valid('aa')) # False
We have classmethod and staticmethod. Classmethod is want in another language we defined by using the static keyword. In python all @classmethod have first argument the class. (name convention cls).
Also the count variable is a class variable not instance. Self is used to refer to instance variable and the name of the class User.count or cls.count is used to refer to class variable.
In number_of_users we have cls.count that returns the class variable.
In __init__ we use the class name because we can't access the class is another way.
In from_dict we return a instance of the class from a dict, using cls(), classmethods are great for doing factory methods like from_something.
The static methods don't have the cls argument, they are just function that are related to the class but work independently of the class, they could be in a separate file but are in the class because they relate to the logic of class, so is better to have them in the same file, you can always transform it into a class method.
Python doesn't have interfaces, we can use the abc module and define a method as abstractmethod and use that as a replacement for a interface.
If you want a video on super() you can look at Raymond Hettinger (python core developer) talk at pycon2015
Raymond Hettinger - Super considered super! - PyCon 2015
Comments
Post a Comment