Back to Notes

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

InstanceClass
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 be True only if obj.__class__ is int or some class derived from int.

  • Use issubclass() to check class inheritance: issubclass(bool, int) is True since bool is a subclass of int. However, issubclass(float, int) is False since float is not a subclass of int.

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.
OperationOperatorMethod
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_