Hi! Recently weeks, I have been learning a lot of Computer Science related topics (as you have seen in my last blog posts), last week I shared the code I implemented to practice some data structures and algorithms.
As I mentioned, I’m using Python as my main stack, and I have been learning language-specific concepts that are incredibly helpful to know if you want to master Python and have a bigger insight into the things that you can create with the language. My piece of advice if you are using Python: Learn them all and have them in mind while you code.
I would like to share these language-specific concepts with you.
Therefore, take this post as kind of a guide of some key concepts you should know about Python. I will be sharing also some good resources at the end so you can use them as references when necessary. Without further due, let’s begin.
While we code, we create different objects and data structures to accomplish tasks. Our data is stored in memory so we are able to access it whenever we need it. Along with this, it comes memory management. In the early years, programmers in past programming languages had to take care of this task, due to this, some problems were produced since one might forget to free memory or one might do it too soon.
After that, more modern programming languages such as Python, Java, and C# began to automate this task to eliminate these problems. These languages are called garbage-collected languages. With automatic memory management, programmers no longer needed to manage memory themselves. Rather, the runtime handled this for them.
Garbage collection is the process of finding data objects in a running program that cannot be accessed in the future, and reclaiming resources, particularly memory, used by those objects.
To get a better idea of this concept, we can use an example. In Python, when we eliminate a node in a linked list, we do so by changing the pointer of its previous node to point to its next node. This produces an orphan node, there aren’t nodes pointing to it. The garbage collector will recognize that this node cannot be accessed so it will reclaim the resources.
Iterators and Generators
Iterators in Python are objects that have __iter__() and __next__() methods. These functionalities allow us to loop over iterators, using for loops, for example.
A generator is an easy way to create an iterator. A generator uses the function call stack to implicitly store the state of the iterator. Generators provide a space-efficient method for data processing, as only parts of the file are handled at one given point in time.
A function that has the yield keyword to generate a value rather than the return keyword is a function generator. The yield statement suspends the function’s execution and sends a value back to the caller, but retains enough state to enable the function to resume where it is left off. When resumed, the function continues execution immediately after the last yield run.
Python support first-class functions. First-class simply means that functions can be treated as a value, that is you can assign them to variables, return them from functions, as well as pass them in as a parameter.
In addition, functions can be defined within other functions. These facts can simplify writing code.
Decorators are syntactic sugar of python that allows programmers to modify the behavior of function or class. Decorators allow us to wrap another function in order to extend the behavior of the wrapped function, without permanently modifying it. Check a very simple example in the image to the left.
Know the main difference between Lists, Tuples, and Sets
Lists, tuples, and sets are similar in that the three represent sequences. There are certainly specific points that differentiate the three data structures.
List vs Set
Sets are different from lists and tuples in that you cannot store duplicates in sets. Therefore, if your application needs this feature of not allowing duplicates or you know you won’t store duplicates, use sets!
Why? They are based on hash tables, just like dictionaries, hence they are incredibly fasts for searching values in their data.
List vs Tuple
Tuples are different from lists and tuples in that you cannot mutate (edit) the contents of tuples. Due to this fact, tuples are fixed-sized in nature, whereas lists are dynamic.
This fact makes tuples being faster than lists in that Python will require to do fewer operations to work with tuples. This makes looping through tuples faster than looping through lists, for example.
If you are going to store a constant sequence of values use tuples!
*args and **kwargs
Both are used to pass a variable number of arguments to a function in Python. The *args allows us to pass a variable-length list of arguments, while **kwargs is used when passing a variable number of key-value pairs to a function.
If we use regular arguments, *args and **kwargs must be placed after them. The important thing here is the “*” sign, the words args and kwargs are not necessary to be used as the names, but it is a good practice to name them like that.
The variable list or arguments is actually a tuple and the variable keyword arguments are translated into a dictionary.
Abstract classes in Python
Abstract classes are classes that have 1 or more abstract methods. Abstract methods are functions that have a declaration but not an implementation. In Python, we use the keyword “pass” to skip implementation.
Abstract classes are useful when we want to make the subclasses have and override a specific method.
Interfaces are abstract classes that have all abstract methods, non of their methods can be concrete methods. Additionally, it is possible to have interfaces without any methods. They are usually called marker interfaces.
Interfaces are not necessary for Python. This is because Python has proper multiple inheritance, and also duck typing, which means that the places where you must have interfaces, for example, in Java, you don’t have to have them in Python. Nevertheless, there are still several uses in Python for interfaces. That’s why we can create abstract classes with the abc module.
Encapsulation and private methods and attributes
In OOP (object-oriented programming) encapsulation means keeping all the state, variables, and methods private unless declared to be public, preventing anything outside the class from directly interacting with it. Interactions with attributes can happen through getter and setter methods.
So, how do we create private attributes or methods in python? We do this by adding a double underscore previous to the name of the variable. An example of this can be observed in the image. It is important to say that we are still able to access the variable, even if we make it private.
How can we access it? What Python does is that it renames that attribute or method to achieve that no one can interact directly with it, we are able to access it through the new name which is _name of the class__private method or variable. This is an underscore followed by the name of the class followed by a double underscore followed by the name of the private variable.
Talking about access modifiers, Python by default has all of its variables public if they are declared outside of functions and classes. As we saw we can achieve private variables by adding the double underscore. Finally, the protected modifier is implemented by adding a single underscore before the name of the variable or method. The single underscore won’t prevent us from accessing the variable, however, other developers will understand that the variable is protected when they see the single underscore.
This is something pretty easy to understand. This concept is normally expressed by the saying “If something behaves like a duck, it is a duck”. So this concept refers to the fact that in python we do not declare data types, the type or the class of an object is less important than the method it defines.
Using Duck Typing, we do not check types at all. Instead, we check for the presence of a given method or attribute. Python will know the type of an object by checking its functionalities and attributes.
Magic methods or dunder methods are methods built-in in all objects of Python, these methods do not need to be called to work, they are automatically called to perform different operations when we use our objects.
Some examples are __init__ (like constructor method in other languages), __str__, __repr__, __class__. All objects whether already included in Python (like strings, integers, lists, etc) or created by us through classes, inherit from the prebuilt “object” class, and from it, we extend the functionality of our classes through new methods. Magic methods have already an implementation that we can override in case we want or need to return something different.
Method resolution order and multiple inheritance
It is possible to inherit from multiple classes in python. We do so by adding more classes as parameters to the desired subclass. multiple inheritance is very useful since we can inherit the functionalities from more than one class, nonetheless, we might encounter some scenarios where we do not know which method is going to be called when performing an operation.
For example, if we have a class A, and a class B and a class C inherit from that class A, and after that, we have a class D that inherits both the class B and C. Having this situation, what happens if a method from class A is overridden in both classes B and C, and we do not define it in D but we know we will use it? From which class will we inherit that method?
These questions are answered through MRO or method resolution order. It denotes the way a programming language resolves a method or attribute, method resolution order defines the order in which the base classes are searched when executing a method.
So to answer the question, first, the method or attribute is searched within a class and then it follows the order we specified while inheriting. It is possible to force from where Python is going to look first to check from methods or attributes for an object after checking in itself.
To see the order in which python will search we can execute name_of_the_class.mro(). An example can be seen in the image. Python will search for the method print_hello first in D, then if not define or overridden, in the first inherited class which is B, then in C, and finally in the base class A, from the one that B and C inherit.
Additional topics to read
We have covered a lot of specific Python concepts, nevertheless, there are lots of things more to discover. You can also check the following concepts:
- Overriding and overloading
- Inheritance vs Composition
- staticmethod vs classmethod
- Modify strings
- Lambdas and dictionaries
- Shallow and deep copy
- Exception handling
- Pythonic style
- Function arguments
- Specify the type of a variable
- Specify return type of a function
Python is an amazing programming language, full of tools to simplify our code, it is important to know these tools in order to improve your programming skills in the language.
This is everything for this week, feel free to give any feedback in the comments and share any other concepts that are essential and interesting to know about Python. Have a great day, thank you for reading!
References and Interesting Resources
- Role of underscore
- Args and kwargs
- PEP 8
- Validate class attributes
- Property vs. Getters and Setters in Python
- Python Docstrings
- Python Design Patterns
- SOLID in Python
- Duck typing in Python
- Tuples vs. Lists vs. Sets in Python
- The four principles of OOP
- Python Access Modifiers
- Abstract Classes in Python
- Inheritance and Composition in Python
- Multiple Inheritance in Python
- Difference between Method Overloading and Method Overriding
- Dunder or magic methods in Python
- Method resolution order in Python Inheritance
- Comprehensions in Python
- Book: Elements of programming interviews in Python
Enjoy your learning!