Clean Code
Code that's easy for humans to understand is called "clean code".
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. -- Martin Fowler
Clean code does not
- Make your programs run faster
- Make your programs function correctly
- Only occur in object-oriented programming
Clean code does
- Make code easier to work with
- Make it easier to find and fix bugs
- Make the development process faster
- Help us retain our sanity
DRY (Don't Repeat Yourself)
Avoid writing same code multiple times. Repeating code multiple times can be bad because:
- If you need to update certain code then you need to update in multiple places
- It would be difficult if we want to rewrite the piece of code which is used in multiple places
Classes
"Classes" are custom new types that we define as the programmer. Each new instance of a class is an "object".
Methods
One thing that makes classes cool is that we can define methods on them. A method is a function that's tied directly to a class and has access to all its properties.
Self
Methods are nested within the class declaration. Their first parameter is always the instance of the class that the method is being called on. By convention, it's called "self". Because self is a reference to the object, you can use it to read and update the properties of the object.
Constructor
__init__ is a keyword method which is used as constructor for the python classes. This will be called when we create the object for the class.
Instance or multiple objects
In object-oriented programming, an instance is a concrete occurrence of any object... "Instance" is synonymous with "object" as they are each a particular value... "Instance" emphasizes the distinct identity of the object. The creation of an instance is called instantiation.
Class vs Instance Variables
| Instance | Class |
|---|---|
| Instance variables vary from object to object and are declared in the constructor. | Class variables remain the same between instances of the same class and are declared at the top level of a class definition.<br><br>In other languages these types of variables are often called static variables. |
class Wall:<br> def __init__(self):<br> self.height = 10<br><br>south_wall = Wall()<br>south_wall.height = 20 # only updates this instance of a wall<br>print(south_wall.height)<br># prints "20"<br><br>north_wall = Wall()<br>print(north_wall.height)<br># prints "10"<br> | class Wall:<br> height = 10<br><br>south_wall = Wall()<br>print(south_wall.height)<br># prints "10"<br><br>Wall.height = 20 # updates all instances of a Wall<br><br>print(south_wall.height)<br># prints "20" |
[!INFO] When to use what? Generally speaking, stay away from class variables. Just like global variables, class variables are usually a bad idea because they make it hard to keep track of which parts of your program are making updates. However, it is important to understand how they work because you may see them out in the wild.
Encapsulation
- Encapsulation is the practice of hiding complexity inside a "black box" so that it's easier to focus on the problem at hand.
[!IMPORTANT] Encapsulation is about organization, not security.
[!NOTE]
Encapsulation in Python
Python is a dynamic language, and that makes it difficult for the interpreter to enforce some of the safeguards that languages like Go do. That's why encapsulation in Python is achieved mostly by convention rather than by force.
Prefixing methods and properties with a double underscore is a strong suggestion to the users of your class that they shouldn't be touching that stuff. If a developer wants to break convention, there are ways to get around the double underscore rule.
Public and Private:
- By default, all properties and methods in a class are public. That means that you can access them with the
.operator - Private data members are how we encapsulate logic and data within a class. To make a property or method private, you just need to prefix it with two underscores.
class Wall:
def __init__(self, armor, magic_resistance):
self.__armor = armor
self.__magic_resistance = magic_resistance
def get_defense(self):
return self.__armor + self.__magic_resistance
front_wall = Wall(10, 20)
# This results in an error
print(front_wall.__armor)
# This works
print(front_wall.get_defense())
# 30
Abstraction:
Abstraction helps us handle complexity by hiding unnecessary details.
Abstraction vs encapsulation:
- Abstraction is about creating a simple interface for complex behavior. It focuses on what's exposed.
- Encapsulation is about hiding internal state. It focuses on tucking implementation details away so no one depends on them.
Abstraction is more about reducing complexity, encapsulation is more about maintaining the integrity of system internals.
Inheritance
- Inheritance allows one class, the "child" class, to inherit the properties and methods of another class, the "parent" class.
- This powerful language feature helps us avoid writing a lot of the same code twice. It allows us to DRY (don't repeat yourself) up our code.
class Animal:
# parent "Animal" class
def __init__(self, num_legs):
self.num_legs = num_legs
class Cow(Animal):
# child class "Cow" inherits "Animal"
def __init__(self):
# call the parent constructor to
# give the cow some legs
super().__init__(4)
Python has two built-in functions that work with inheritance:
-
Use
isinstance()to check an instance’s type:isinstance(obj, int)will beTrueonly ifobj.__class__isintor some class derived fromint. -
Use
issubclass()to check class inheritance:issubclass(bool, int)isTruesinceboolis a subclass ofint. However,issubclass(float, int)isFalsesincefloatis not a subclass ofint.
Python also supports multiple inheritance and to read more about it here
Polymorphism
- Polymorphism is the ability of a variable, function or object to take on multiple forms.
- Take a look at the Greek roots of the word "polymorphism".
- "poly"="many"
- "morph"="form".
For example, classes in the same hierarchical tree may have methods with the same name but different behaviors.
class Creature():
def move(self):
print("the creature moves")
class Dragon(Creature):
def move(self):
print("the dragon flies")
class Kraken(Creature):
def move(self):
print("the kraken swims")
for creature in [Creature(), Dragon(), Kraken()]:
creature.move()
# prints:
# the creature moves
# the dragon flies
# the kraken swims
Operator Overload Review
- operator overloading is the practice of defining custom behavior for standard Python operators. Here's a list of how the operators translate into method names.
| Operation | Operator | Method |
|---|---|---|
| Addition | + | _add_ |
| Subtraction | - | _sub_ |
| Multiplication | * | _mul_ |
| Power | ** | _pow_ |
| Division | / | _truediv_ |
| Floor Division | // | _floordiv_ |
| Remainder (modulo) | % | _mod_ |
| Bitwise Left Shift | << | _lshift_ |
| Bitwise Right Shift | >> | _rshift_ |
| Bitwise AND | & | _and_ |
| Bitwise OR | | | _or_ |
| Bitwise XOR | ^ | _xor_ |
| Bitwise NOT | ~ | _invert_ |
| Grater than | > | _gt_ |
| Equal | == | _eq_ |
| Less than | < | _lt_ |