Python Full Stack Web Developer Course

Week 3: Object-Oriented Programming Advanced Concepts

For JavaScript Developers: Advanced OOP Patterns Compared to JavaScript

Bridging the Gap: Python OOP vs. JavaScript OOP

As a JavaScript developer transitioning to Python, understanding the differences and similarities in object-oriented programming (OOP) patterns between the two languages is crucial. While both languages support object-oriented programming, they approach it in fundamentally different ways.

JavaScript uses a prototype-based object model, where objects can directly inherit from other objects. Python, on the other hand, uses a class-based object model more similar to languages like Java or C++. Despite these differences, many of the same OOP design patterns can be implemented in both languages, though the syntax and specific mechanisms might differ.

In this session, we'll explore advanced OOP patterns in Python, comparing them with their JavaScript counterparts. By highlighting these similarities and differences, you'll be able to leverage your existing JavaScript knowledge while embracing Python's unique approach to object-oriented programming.

Today's File Structure

For today's lesson, we'll create a new Python module in our project. Ensure you have the following directory structure:

project_root/
├── python_vs_js/
│   ├── __init__.py  (empty file to make the folder a package)
│   ├── class_vs_prototype.py
│   ├── inheritance_patterns.py
│   ├── encapsulation.py
│   ├── composition_mixins.py
│   ├── static_class_methods.py
│   └── design_patterns.py

All code examples will be saved in these files, allowing you to organize and revisit these concepts easily.

Classes vs. Prototypes: The Fundamental Difference

JavaScript and Python represent the two primary approaches to object-oriented programming: prototype-based (JavaScript) and class-based (Python). Let's explore this fundamental difference with code examples in both languages. Create a file named class_vs_prototype.py for the Python code, and we'll compare it with equivalent JavaScript.

Creating Objects and Defining Methods

JavaScript (Prototype-based):

// Traditional prototype-based approach in JavaScript
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
};

// ES6 class syntax (syntactic sugar over prototypes)
class PersonES6 {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
    }
}

// Creating instances
const john = new Person("John", 30);
const jane = new PersonES6("Jane", 25);

console.log(john.greet());  // Hello, my name is John and I'm 30 years old.
console.log(jane.greet());  // Hello, my name is Jane and I'm 25 years old.

// Even with ES6 classes, JavaScript's prototype nature allows:
// Adding methods to all existing instances
Person.prototype.sayGoodbye = function() {
    return `Goodbye from ${this.name}`;
};

console.log(john.sayGoodbye());  // Works even though defined after instantiation

Python (Class-based):

# File: python_vs_js/class_vs_prototype.py

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def greet(self):
        return f"Hello, my name is {self.name} and I'm {self.age} years old."


# Creating instances
john = Person("John", 30)
jane = Person("Jane", 25)

print(john.greet())  # Hello, my name is John and I'm 30 years old.
print(jane.greet())  # Hello, my name is Jane and I'm 25 years old.

# Python doesn't allow adding methods to existing instances directly.
# Instead, you would need to define a new class or use other techniques
# like monkey patching (not recommended in most cases)

# This is a simplified form of monkey patching - use with caution!
def say_goodbye(self):
    return f"Goodbye from {self.name}"

Person.say_goodbye = say_goodbye

print(john.say_goodbye())  # Works because we modified the class, not the instance


# A more 'Pythonic' approach would be to extend the class
class EnhancedPerson(Person):
    def say_goodbye(self):
        return f"Goodbye from {self.name}"

james = EnhancedPerson("James", 35)
print(james.greet())        # Inherited from Person
print(james.say_goodbye())  # Defined in EnhancedPerson

Key Differences:

Aspect JavaScript Python
Object Creation Constructor function or ES6 class Class definition only
Instance Creation new Person() Person() (without new)
Constructor constructor() or function itself __init__() method
Method Definition On prototype or within class Within class definition
Adding Methods at Runtime Easy via prototype (affects all instances) Possible but less idiomatic (monkey patching)
Self Reference Implicit this Explicit self parameter

Dynamic Properties in Both Languages

JavaScript:

// JavaScript objects can add properties dynamically
const john = new Person("John", 30);
john.location = "New York";  // Add a property only to this instance
console.log(john.location);  // "New York"

// Object property descriptors for advanced control
Object.defineProperty(john, 'fullName', {
    get: function() { return `${this.name} Doe`; },
    set: function(value) {
        const parts = value.split(' ');
        this.name = parts[0];
    },
    enumerable: true,
    configurable: true
});

console.log(john.fullName);  // "John Doe"
john.fullName = "Jane";
console.log(john.name);      // "Jane"

Python:

# Python objects can also add attributes dynamically
john = Person("John", 30)
john.location = "New York"  # Add an attribute only to this instance
print(john.location)        # "New York"

# Property decorators for getter/setter behavior
class PersonWithProperties:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        self._name = value
    
    @property
    def full_name(self):
        return f"{self._name} Doe"
    
    @full_name.setter
    def full_name(self, value):
        parts = value.split(' ')
        self._name = parts[0]

person = PersonWithProperties("John", 30)
print(person.full_name)     # "John Doe"
person.full_name = "Jane"
print(person.name)          # "Jane"

Summary:

Inheritance Patterns: Single, Multiple, and Mixin Inheritance

Inheritance is a core concept in OOP that allows classes to derive properties and methods from other classes. JavaScript and Python handle inheritance differently, especially when it comes to multiple inheritance. Create a file named inheritance_patterns.py for the Python code.

Single Inheritance

JavaScript:

// JavaScript single inheritance using ES6 classes
class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        return `${this.name} makes a noise.`;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);  // Call parent constructor
        this.breed = breed;
    }
    
    speak() {
        return `${this.name} barks!`;  // Override method
    }
    
    fetch() {
        return `${this.name} is fetching.`;  // Add new method
    }
}

const dog = new Dog("Rex", "German Shepherd");
console.log(dog.speak());  // "Rex barks!"
console.log(dog.fetch());  // "Rex is fetching."

Python:

# File: python_vs_js/inheritance_patterns.py

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a noise."


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Call parent constructor
        self.breed = breed
    
    def speak(self):
        return f"{self.name} barks!"  # Override method
    
    def fetch(self):
        return f"{self.name} is fetching."  # Add new method


dog = Dog("Rex", "German Shepherd")
print(dog.speak())  # "Rex barks!"
print(dog.fetch())  # "Rex is fetching."

Multiple Inheritance (Python-specific)

JavaScript doesn't support true multiple inheritance, but Python does:

# Python multiple inheritance

class Swimmer:
    def swim(self):
        return f"{self.name} is swimming."
    
    def speak(self):
        return f"{self.name} says glub glub."


class FlyingAnimal:
    def fly(self):
        return f"{self.name} is flying."
    
    def speak(self):
        return f"{self.name} says whoosh."


# Duck inherits from both Animal and Swimmer
class Duck(Animal, Swimmer, FlyingAnimal):
    def __init__(self, name):
        super().__init__(name)  # This calls Animal.__init__


duck = Duck("Daffy")
print(duck.speak())  # "Daffy makes a noise." - From Animal (first in MRO)
print(duck.swim())   # "Daffy is swimming." - From Swimmer
print(duck.fly())    # "Daffy is flying." - From FlyingAnimal

# Method Resolution Order (MRO) determines which method is called
print(Duck.__mro__)  # Shows the method resolution order

JavaScript Approach to Multiple Inheritance-Like Patterns

JavaScript - Using Composition:

// JavaScript doesn't have true multiple inheritance,
// but you can use composition to achieve similar results

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        return `${this.name} makes a noise.`;
    }
}

// Mixin functions
const swimmerMixin = {
    swim() {
        return `${this.name} is swimming.`;
    },
    speak() {
        return `${this.name} says glub glub.`;
    }
};

const flyerMixin = {
    fly() {
        return `${this.name} is flying.`;
    },
    speak() {
        return `${this.name} says whoosh.`;
    }
};

// Create a Duck class with composition
class Duck extends Animal {
    constructor(name) {
        super(name);
        // Mixin the swimmer and flyer capabilities
        Object.assign(this, swimmerMixin, flyerMixin);
    }
    
    // Duck's own method overrides all mixins
    speak() {
        return `${this.name} quacks!`;
    }
}

const duck = new Duck("Daffy");
console.log(duck.speak());  // "Daffy quacks!" - From Duck's own method
console.log(duck.swim());   // "Daffy is swimming." - From swimmerMixin
console.log(duck.fly());    // "Daffy is flying." - From flyerMixin

Python - Using Mixins:

# Python's equivalent to JavaScript mixins is... well, just multiple inheritance
# But we often use the term "mixin" for classes designed to be inherited from but not instantiated

class SwimmerMixin:
    """A mixin that provides swimming capabilities"""
    def swim(self):
        return f"{self.name} is swimming."


class FlyerMixin:
    """A mixin that provides flying capabilities"""
    def fly(self):
        return f"{self.name} is flying."


class Duck(Animal, SwimmerMixin, FlyerMixin):
    def __init__(self, name):
        super().__init__(name)
    
    def speak(self):
        return f"{self.name} quacks!"


duck = Duck("Daffy")
print(duck.speak())  # "Daffy quacks!" - From Duck's own method
print(duck.swim())   # "Daffy is swimming." - From SwimmerMixin
print(duck.fly())    # "Daffy is flying." - From FlyerMixin

The Diamond Problem and MRO

Python's Method Resolution Order (MRO) algorithm handles the "diamond problem" in multiple inheritance:

# The Diamond Problem in Python

class Base:
    def method(self):
        return "Base method"


class Left(Base):
    def method(self):
        return "Left method"


class Right(Base):
    def method(self):
        return "Right method"


class Diamond(Left, Right):
    pass  # Inherits method from both Left and Right


diamond = Diamond()
print(diamond.method())  # Prints "Left method" - Left comes first in MRO
print(Diamond.__mro__)   # Shows the method resolution order

Summary:

Encapsulation: Private, Protected, and Public Members

Encapsulation is the bundling of data and methods that operate on that data within a single unit (like a class), and restricting access to some of the object's components. JavaScript and Python have different approaches to encapsulation. Create a file named encapsulation.py for the Python code.

Access Modifiers

JavaScript:

// JavaScript - ES6 and earlier had no built-in private fields
class OldStylePerson {
    constructor(name, age) {
        this.name = name;          // Public
        this._age = age;           // Convention: protected (but still accessible)
        this._calculateBirthYear = function() {  // Convention: private method
            const year = new Date().getFullYear();
            return year - this._age;
        };
    }
    
    getAge() {
        return this._age;
    }
    
    getBirthYear() {
        return this._calculateBirthYear();
    }
}

// JavaScript - Modern private fields with # (ES2022)
class ModernPerson {
    #age;                          // Truly private field
    #calculateBirthYear;           // Private method
    
    constructor(name, age) {
        this.name = name;          // Public
        this.#age = age;           // Private
        this.#calculateBirthYear = function() {
            const year = new Date().getFullYear();
            return year - this.#age;
        };
    }
    
    getAge() {
        return this.#age;
    }
    
    getBirthYear() {
        return this.#calculateBirthYear();
    }
}

// Using the classes
const oldPerson = new OldStylePerson("John", 30);
console.log(oldPerson.name);          // "John" - accessible
console.log(oldPerson._age);          // 30 - accessible, but convention says not to use directly
console.log(oldPerson.getAge());      // 30 - proper access

const modernPerson = new ModernPerson("Jane", 25);
console.log(modernPerson.name);       // "Jane" - accessible
// console.log(modernPerson.#age);    // SyntaxError - private field not accessible
console.log(modernPerson.getAge());   // 25 - proper access

Python:

# File: python_vs_js/encapsulation.py

class Person:
    def __init__(self, name, age):
        self.name = name         # Public
        self._age = age          # Convention: protected (but still accessible)
        self.__birth_year = None  # Name mangling: more private (but still accessible with _Person__birth_year)
    
    def get_age(self):
        return self._age
    
    def __calculate_birth_year(self):  # Name mangling for methods too
        import datetime
        year = datetime.datetime.now().year
        return year - self._age
    
    def get_birth_year(self):
        if not self.__birth_year:
            self.__birth_year = self.__calculate_birth_year()
        return self.__birth_year


# Using the class
person = Person("John", 30)
print(person.name)                 # "John" - accessible
print(person._age)                 # 30 - accessible, but convention says not to use directly
print(person.get_age())            # 30 - proper access

# print(person.__birth_year)       # AttributeError - not directly accessible
print(person._Person__birth_year)  # None - accessible through name mangling
print(person.get_birth_year())     # 1993 (or current year - 30) - proper access

Property-Based Encapsulation

JavaScript:

// JavaScript - Using getters and setters
class PersonWithProperties {
    #age;
    
    constructor(name, age) {
        this._name = name;
        this.#age = age;
    }
    
    // Getter and setter for name
    get name() {
        return this._name;
    }
    
    set name(value) {
        if (typeof value !== 'string') {
            throw new Error("Name must be a string");
        }
        this._name = value;
    }
    
    // Getter and setter for age with validation
    get age() {
        return this.#age;
    }
    
    set age(value) {
        if (typeof value !== 'number' || value < 0 || value > 120) {
            throw new Error("Age must be a number between 0 and 120");
        }
        this.#age = value;
    }
    
    // Computed property
    get birthYear() {
        const year = new Date().getFullYear();
        return year - this.#age;
    }
}

// Using the class
const personWithProps = new PersonWithProperties("John", 30);
console.log(personWithProps.name);      // "John" - uses getter
personWithProps.name = "Jane";          // Uses setter
console.log(personWithProps.name);      // "Jane"
console.log(personWithProps.birthYear); // Computed property

Python:

# Python - Using property decorators
class PersonWithProperties:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    # Property for name
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("Name must be a string")
        self._name = value
    
    # Property for age with validation
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0 or value > 120:
            raise ValueError("Age must be an integer between 0 and 120")
        self._age = value
    
    # Computed property
    @property
    def birth_year(self):
        import datetime
        year = datetime.datetime.now().year
        return year - self._age


# Using the class
person_with_props = PersonWithProperties("John", 30)
print(person_with_props.name)       # "John" - uses getter
person_with_props.name = "Jane"     # Uses setter
print(person_with_props.name)       # "Jane"
print(person_with_props.birth_year) # Computed property

Key Differences:

Aspect JavaScript Python
Public Members Normal properties: this.name Normal attributes: self.name
Protected Members Convention: this._name (still accessible) Convention: self._name (still accessible)
Private Members Modern: this.#name (truly private)
Old: closure techniques or Symbol
Name mangling: self.__name (becomes self._ClassName__name)
Properties (Getters/Setters) get name() {} and set name(v) {} @property and @name.setter decorators
Privacy Enforcement Strong with # private fields Weak ("we're all consenting adults" philosophy)

Summary:

Composition vs. Inheritance and Mixin Patterns

Both JavaScript and Python support inheritance, but composition ("has-a" rather than "is-a" relationships) is often preferred for more flexible designs. Create a file named composition_mixins.py for the Python code.

Composition Patterns

JavaScript:

// JavaScript composition pattern
class Engine {
    constructor(type) {
        this.type = type;
    }
    
    start() {
        return `${this.type} engine starting...`;
    }
    
    stop() {
        return `${this.type} engine stopping...`;
    }
}

class Wheels {
    constructor(count) {
        this.count = count;
    }
    
    rotate() {
        return `${this.count} wheels rotating...`;
    }
}

// Car composes Engine and Wheels rather than inheriting from them
class Car {
    constructor(engineType, wheelCount) {
        this.engine = new Engine(engineType);
        this.wheels = new Wheels(wheelCount);
    }
    
    drive() {
        return [
            this.engine.start(),
            this.wheels.rotate(),
            "Car is moving!"
        ].join(" ");
    }
    
    park() {
        return [
            "Car is stopping.",
            this.engine.stop()
        ].join(" ");
    }
}

const car = new Car("V8", 4);
console.log(car.drive());  // "V8 engine starting... 4 wheels rotating... Car is moving!"
console.log(car.park());   // "Car is stopping. V8 engine stopping..."

Python:

# File: python_vs_js/composition_mixins.py

class Engine:
    def __init__(self, type):
        self.type = type
    
    def start(self):
        return f"{self.type} engine starting..."
    
    def stop(self):
        return f"{self.type} engine stopping..."


class Wheels:
    def __init__(self, count):
        self.count = count
    
    def rotate(self):
        return f"{self.count} wheels rotating..."


# Car composes Engine and Wheels rather than inheriting from them
class Car:
    def __init__(self, engine_type, wheel_count):
        self.engine = Engine(engine_type)
        self.wheels = Wheels(wheel_count)
    
    def drive(self):
        return " ".join([
            self.engine.start(),
            self.wheels.rotate(),
            "Car is moving!"
        ])
    
    def park(self):
        return " ".join([
            "Car is stopping.",
            self.engine.stop()
        ])


car = Car("V8", 4)
print(car.drive())  # "V8 engine starting... 4 wheels rotating... Car is moving!"
print(car.park())   # "Car is stopping. V8 engine stopping..."

Mixin Patterns

JavaScript:

// JavaScript mixin pattern
// Define behaviors as plain objects
const LoggerMixin = {
    log(message) {
        console.log(`[${this.constructor.name}] ${message}`);
    },
    
    error(message) {
        console.error(`[ERROR][${this.constructor.name}] ${message}`);
    }
};

const SerializableMixin = {
    serialize() {
        return JSON.stringify(this);
    },
    
    deserialize(json) {
        const data = JSON.parse(json);
        Object.assign(this, data);
        return this;
    }
};

// Apply mixins to a class
function applyMixins(targetClass, ...mixins) {
    mixins.forEach(mixin => {
        Object.getOwnPropertyNames(mixin).forEach(prop => {
            if (prop !== 'constructor') {
                targetClass.prototype[prop] = mixin[prop];
            }
        });
    });
}

// Use the mixins
class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
}

// Apply mixins to the User class
applyMixins(User, LoggerMixin, SerializableMixin);

const user = new User("John", "john@example.com");
user.log("User created");  // [User] User created

const serialized = user.serialize();
console.log(serialized);   // {"name":"John","email":"john@example.com"}

const newUser = new User().deserialize(serialized);
console.log(newUser.name); // John

Python:

# Python mixin pattern
class LoggerMixin:
    def log(self, message):
        print(f"[{self.__class__.__name__}] {message}")
    
    def error(self, message):
        print(f"[ERROR][{self.__class__.__name__}] {message}")


class SerializableMixin:
    def serialize(self):
        import json
        return json.dumps(self.__dict__)
    
    def deserialize(self, json_str):
        import json
        data = json.loads(json_str)
        self.__dict__.update(data)
        return self


# Use the mixins through multiple inheritance
class User(LoggerMixin, SerializableMixin):
    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email


user = User("John", "john@example.com")
user.log("User created")  # [User] User created

serialized = user.serialize()
print(serialized)        # {"name": "John", "email": "john@example.com"}

new_user = User().deserialize(serialized)
print(new_user.name)     # John

Key Differences:

Aspect JavaScript Python
Composition Explicit object creation and method delegation Similar explicit composition pattern
Mixins Plain objects with methods, applied with Object.assign or custom functions Classes used in multiple inheritance
Mixin Application Explicit application at runtime Part of class definition (inheritance list)
Method Resolution Last mixin applied wins Follows Method Resolution Order (MRO)

Summary:

Static and Class Methods

Both JavaScript and Python support static methods (associated with the class rather than instances), but Python also has the concept of class methods. Create a file named static_class_methods.py for the Python code.

Static Methods

JavaScript:

// JavaScript static methods
class MathUtils {
    constructor() {
        throw new Error("This class cannot be instantiated");
    }
    
    static add(a, b) {
        return a + b;
    }
    
    static subtract(a, b) {
        return a - b;
    }
    
    static multiply(a, b) {
        return a * b;
    }
    
    static divide(a, b) {
        if (b === 0) throw new Error("Division by zero");
        return a / b;
    }
}

// Using static methods
console.log(MathUtils.add(5, 3));       // 8
console.log(MathUtils.multiply(4, 2));  // 8

// JavaScript static fields (ES2022)
class Counter {
    static count = 0;
    
    constructor() {
        Counter.count++;
    }
    
    static getCount() {
        return Counter.count;
    }
}

const c1 = new Counter();
const c2 = new Counter();
console.log(Counter.getCount());  // 2

Python:

# File: python_vs_js/static_class_methods.py

class MathUtils:
    def __init__(self):
        raise NotImplementedError("This class cannot be instantiated")
    
    @staticmethod
    def add(a, b):
        return a + b
    
    @staticmethod
    def subtract(a, b):
        return a - b
    
    @staticmethod
    def multiply(a, b):
        return a * b
    
    @staticmethod
    def divide(a, b):
        if b == 0:
            raise ZeroDivisionError("Division by zero")
        return a / b


# Using static methods
print(MathUtils.add(5, 3))       # 8
print(MathUtils.multiply(4, 2))  # 8

# Python class variables and methods
class Counter:
    count = 0  # Class variable
    
    def __init__(self):
        Counter.count += 1
    
    @classmethod
    def get_count(cls):  # cls refers to the class
        return cls.count


c1 = Counter()
c2 = Counter()
print(Counter.get_count())  # 2

Class Methods in Python (Factory Pattern)

Python's class methods provide unique capabilities not directly available in JavaScript:

# Python class methods as factory methods
class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age
    
    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"
    
    @staticmethod
    def is_adult(age):
        return age >= 18
    
    @classmethod
    def from_full_name(cls, full_name, age):
        """Factory method to create a Person from a full name"""
        first_name, last_name = full_name.split(" ", 1)
        return cls(first_name, last_name, age)
    
    @classmethod
    def create_anonymous(cls, age):
        """Factory method to create an anonymous Person"""
        return cls("Anonymous", "User", age)


# Using regular constructor
person1 = Person("John", "Doe", 30)
print(person1.full_name)  # John Doe

# Using factory methods
person2 = Person.from_full_name("Jane Smith", 25)
print(person2.full_name)  # Jane Smith

person3 = Person.create_anonymous(40)
print(person3.full_name)  # Anonymous User

# Static method
print(Person.is_adult(16))  # False
print(Person.is_adult(21))  # True

# Inheritance with class methods
class Employee(Person):
    def __init__(self, first_name, last_name, age, employee_id):
        super().__init__(first_name, last_name, age)
        self.employee_id = employee_id
    
    @classmethod
    def create_anonymous(cls, age):
        """Override the factory to create anonymous employees"""
        anonymous = super().create_anonymous(age)
        # Create a new instance with the added employee_id
        return cls(anonymous.first_name, anonymous.last_name, age, "A-0000")


# The factory method is inherited and respects the derived class
employee = Employee.create_anonymous(35)
print(employee.full_name)     # Anonymous User
print(employee.employee_id)   # A-0000

JavaScript Equivalent (Factory Pattern):

// JavaScript factory methods (similar to Python's class methods)
class Person {
    constructor(firstName, lastName, age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
    
    static isAdult(age) {
        return age >= 18;
    }
    
    // Factory method to create a Person from a full name
    static fromFullName(fullName, age) {
        const [firstName, ...lastNameParts] = fullName.split(" ");
        const lastName = lastNameParts.join(" ");
        return new this(firstName, lastName, age);  // 'this' refers to the class
    }
    
    // Factory method to create an anonymous Person
    static createAnonymous(age) {
        return new this("Anonymous", "User", age);
    }
}

// Using regular constructor
const person1 = new Person("John", "Doe", 30);
console.log(person1.fullName);  // John Doe

// Using factory methods
const person2 = Person.fromFullName("Jane Smith", 25);
console.log(person2.fullName);  // Jane Smith

const person3 = Person.createAnonymous(40);
console.log(person3.fullName);  // Anonymous User

// Static method
console.log(Person.isAdult(16));  // false
console.log(Person.isAdult(21));  // true

// Inheritance with factory methods
class Employee extends Person {
    constructor(firstName, lastName, age, employeeId) {
        super(firstName, lastName, age);
        this.employeeId = employeeId;
    }
    
    // Override the factory to create anonymous employees
    static createAnonymous(age) {
        const anonymous = new Person("Anonymous", "User", age);
        return new this(anonymous.firstName, anonymous.lastName, age, "A-0000");
    }
}

// The factory method works with the derived class
const employee = Employee.createAnonymous(35);
console.log(employee.fullName);     // Anonymous User
console.log(employee.employeeId);   // A-0000

Key Differences:

Aspect JavaScript Python
Static Methods static methodName() {} @staticmethod decorator
Static Variables static variableName = value (ES2022) Class variables defined at class level
Class Methods No direct equivalent, but can use this in static methods @classmethod decorator with cls parameter
Factory Pattern Static methods with new this() Class methods with cls()
Inheritance Static methods inherited but not bound to subclass Class methods inherited and properly bound to subclass

Summary:

Common Design Patterns in Both Languages

Many classic design patterns can be implemented in both JavaScript and Python, though the implementation details may differ. Create a file named design_patterns.py for the Python code.

Singleton Pattern

JavaScript:

// JavaScript Singleton pattern
class Singleton {
    static instance;
    
    constructor() {
        if (Singleton.instance) {
            return Singleton.instance;
        }
        
        // Initialize the singleton instance
        this.timestamp = new Date();
        this.config = {
            apiUrl: "https://api.example.com",
            timeout: 5000
        };
        
        Singleton.instance = this;
    }
    
    getConfig() {
        return this.config;
    }
    
    setConfig(key, value) {
        this.config[key] = value;
    }
}

// Usage
const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2);  // true, they are the same instance

instance1.setConfig("timeout", 10000);
console.log(instance2.getConfig().timeout);  // 10000, change reflects in all instances

Python:

# File: python_vs_js/design_patterns.py

class Singleton:
    _instance = None
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            # Initialize the singleton instance
            cls._instance.timestamp = __import__("datetime").datetime.now()
            cls._instance.config = {
                "api_url": "https://api.example.com",
                "timeout": 5000
            }
        return cls._instance
    
    def get_config(self):
        return self.config
    
    def set_config(self, key, value):
        self.config[key] = value


# Usage
instance1 = Singleton()
instance2 = Singleton()

print(instance1 is instance2)  # True, they are the same instance

instance1.set_config("timeout", 10000)
print(instance2.get_config()["timeout"])  # 10000, change reflects in all instances

Observer Pattern

JavaScript:

// JavaScript Observer pattern
class Subject {
    constructor() {
        this.observers = [];
    }
    
    addObserver(observer) {
        this.observers.push(observer);
    }
    
    removeObserver(observer) {
        const index = this.observers.indexOf(observer);
        if (index !== -1) {
            this.observers.splice(index, 1);
        }
    }
    
    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}

class Observer {
    constructor(name) {
        this.name = name;
    }
    
    update(data) {
        console.log(`${this.name} received: ${data}`);
    }
}

// Usage
const subject = new Subject();

const observer1 = new Observer("Observer 1");
const observer2 = new Observer("Observer 2");

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notify("Hello, observers!");
// Observer 1 received: Hello, observers!
// Observer 2 received: Hello, observers!

subject.removeObserver(observer1);
subject.notify("Hello again!");
// Observer 2 received: Hello again!

Python:

# Python Observer pattern
class Subject:
    def __init__(self):
        self.observers = []
    
    def add_observer(self, observer):
        self.observers.append(observer)
    
    def remove_observer(self, observer):
        if observer in self.observers:
            self.observers.remove(observer)
    
    def notify(self, data):
        for observer in self.observers:
            observer.update(data)


class Observer:
    def __init__(self, name):
        self.name = name
    
    def update(self, data):
        print(f"{self.name} received: {data}")


# Usage
subject = Subject()

observer1 = Observer("Observer 1")
observer2 = Observer("Observer 2")

subject.add_observer(observer1)
subject.add_observer(observer2)

subject.notify("Hello, observers!")
# Observer 1 received: Hello, observers!
# Observer 2 received: Hello, observers!

subject.remove_observer(observer1)
subject.notify("Hello again!")
# Observer 2 received: Hello again!

Factory Method Pattern

JavaScript:

// JavaScript Factory Method pattern
class Vehicle {
    constructor(make, model) {
        this.make = make;
        this.model = model;
    }
    
    getInfo() {
        return `${this.make} ${this.model}`;
    }
}

class Car extends Vehicle {
    constructor(make, model, doors) {
        super(make, model);
        this.doors = doors;
        this.type = "Car";
    }
    
    getInfo() {
        return `${super.getInfo()}, ${this.doors} doors, type: ${this.type}`;
    }
}

class Truck extends Vehicle {
    constructor(make, model, payload) {
        super(make, model);
        this.payload = payload;
        this.type = "Truck";
    }
    
    getInfo() {
        return `${super.getInfo()}, payload: ${this.payload}kg, type: ${this.type}`;
    }
}

// Factory class
class VehicleFactory {
    static createVehicle(type, make, model, option) {
        switch (type.toLowerCase()) {
            case "car":
                return new Car(make, model, option);  // option is doors
            case "truck":
                return new Truck(make, model, option);  // option is payload
            default:
                throw new Error(`Vehicle type not supported: ${type}`);
        }
    }
}

// Usage
const car = VehicleFactory.createVehicle("car", "Toyota", "Corolla", 4);
const truck = VehicleFactory.createVehicle("truck", "Ford", "F-150", 1500);

console.log(car.getInfo());   // Toyota Corolla, 4 doors, type: Car
console.log(truck.getInfo()); // Ford F-150, payload: 1500kg, type: Truck

Python:

# Python Factory Method pattern
class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def get_info(self):
        return f"{self.make} {self.model}"


class Car(Vehicle):
    def __init__(self, make, model, doors):
        super().__init__(make, model)
        self.doors = doors
        self.type = "Car"
    
    def get_info(self):
        return f"{super().get_info()}, {self.doors} doors, type: {self.type}"


class Truck(Vehicle):
    def __init__(self, make, model, payload):
        super().__init__(make, model)
        self.payload = payload
        self.type = "Truck"
    
    def get_info(self):
        return f"{super().get_info()}, payload: {self.payload}kg, type: {self.type}"


# Factory class
class VehicleFactory:
    @staticmethod
    def create_vehicle(type, make, model, option):
        if type.lower() == "car":
            return Car(make, model, option)  # option is doors
        elif type.lower() == "truck":
            return Truck(make, model, option)  # option is payload
        else:
            raise ValueError(f"Vehicle type not supported: {type}")


# Usage
car = VehicleFactory.create_vehicle("car", "Toyota", "Corolla", 4)
truck = VehicleFactory.create_vehicle("truck", "Ford", "F-150", 1500)

print(car.get_info())   # Toyota Corolla, 4 doors, type: Car
print(truck.get_info()) # Ford F-150, payload: 1500kg, type: Truck

Summary:

Key Takeaways

Best Practices When Transitioning from JavaScript to Python

  1. Embrace Python's Class Syntax: While JavaScript's ES6 classes might look familiar, understand that Python's classes operate on different principles. Embrace Python's approach rather than trying to force JavaScript patterns.
  2. Use Multiple Inheritance Judiciously: Python allows multiple inheritance, but use it thoughtfully. Prefer composition and mixins for reusable behavior.
  3. Follow Python's Naming Conventions: Use snake_case for methods and attributes rather than camelCase as in JavaScript. Follow Python's conventions for indicating private/protected members.
  4. Leverage Properties for Clean APIs: Use Python's property decorators instead of explicit getter/setter methods for a cleaner API that feels more Pythonic.
  5. Understand Method Resolution Order: When using multiple inheritance in Python, understand how the MRO works to avoid unexpected behavior.
  6. Use Class Methods for Factory Patterns: Take advantage of Python's class methods for creating factory methods that work well with inheritance.
  7. Respect Python's "We're All Consenting Adults" Philosophy: Python doesn't enforce strict privacy; it relies on conventions. Respect these conventions but understand that true encapsulation is a matter of documentation and convention, not enforcement.
  8. Prefer Explicit Self: Unlike JavaScript's implicit this, Python requires explicit self as the first parameter in instance methods. This can be confusing at first but leads to clearer code.
  9. Understand the Global Object Difference: JavaScript has a global object (window in browsers, global in Node.js), while Python uses modules as namespaces. This changes how you structure your code.
  10. Use Docstrings: Python uses docstrings for documentation rather than JSDoc-style comments. Follow this convention for better integration with Python tools.

Assignment: Convert a JavaScript Application to Python

For this assignment, you'll convert a small JavaScript application to Python, applying the OOP patterns you've learned.

Requirements:

  1. Start with the following JavaScript code:
  2. // Task Management System
    
    // Task class
    class Task {
        constructor(title, description, dueDate, priority = "medium") {
            this.title = title;
            this.description = description;
            this.dueDate = dueDate;
            this.priority = priority;
            this.completed = false;
            this.createdAt = new Date();
        }
        
        complete() {
            this.completed = true;
        }
        
        updatePriority(newPriority) {
            this.priority = newPriority;
        }
        
        isOverdue() {
            return new Date() > new Date(this.dueDate) && !this.completed;
        }
    }
    
    // TaskList class
    class TaskList {
        constructor(name) {
            this.name = name;
            this.tasks = [];
        }
        
        addTask(task) {
            this.tasks.push(task);
        }
        
        removeTask(taskIndex) {
            if (taskIndex >= 0 && taskIndex < this.tasks.length) {
                this.tasks.splice(taskIndex, 1);
                return true;
            }
            return false;
        }
        
        getCompletedTasks() {
            return this.tasks.filter(task => task.completed);
        }
        
        getIncompleteTasks() {
            return this.tasks.filter(task => !task.completed);
        }
        
        getOverdueTasks() {
            return this.tasks.filter(task => task.isOverdue());
        }
    }
    
    // TaskManager singleton
    class TaskManager {
        static instance;
        
        constructor() {
            if (TaskManager.instance) {
                return TaskManager.instance;
            }
            
            this.taskLists = [];
            TaskManager.instance = this;
        }
        
        createTaskList(name) {
            const newList = new TaskList(name);
            this.taskLists.push(newList);
            return newList;
        }
        
        getTaskList(name) {
            return this.taskLists.find(list => list.name === name);
        }
        
        getAllTaskLists() {
            return this.taskLists;
        }
    }
    
    // Usage example
    const manager = new TaskManager();
    const personalTasks = manager.createTaskList("Personal");
    const workTasks = manager.createTaskList("Work");
    
    personalTasks.addTask(new Task("Buy groceries", "Get milk, eggs, and bread", "2023-12-20", "high"));
    personalTasks.addTask(new Task("Call mom", "Weekly check-in", "2023-12-22"));
    workTasks.addTask(new Task("Finish report", "Complete quarterly report", "2023-12-15", "high"));
    
    console.log(`Incomplete personal tasks: ${personalTasks.getIncompleteTasks().length}`);
    console.log(`Overdue work tasks: ${workTasks.getOverdueTasks().length}`);
    
  3. Convert this JavaScript code to Python, applying Pythonic OOP patterns:
    • Use proper Python naming conventions (snake_case)
    • Implement the Singleton pattern using Python's approach
    • Use property decorators for appropriate attributes
    • Use class methods where they make sense
    • Add type hints for better readability
    • Add proper docstrings
    • Implement any additional features that would make the code more Pythonic
  4. Extend the Python version with at least two of the following features:
    • Task categories with filtering capabilities
    • Subtasks that can be nested within tasks
    • User assignment for tasks with a User class
    • Task search functionality
    • Task export/import to JSON
  5. Write a simple command-line interface to demonstrate the functionality
  6. Include comments explaining your design decisions, especially where they differ from the JavaScript version

Bonus Challenges:

  1. Implement a notification system using the Observer pattern
  2. Add a decorator pattern for task logging or validation
  3. Create a factory method for different types of tasks
  4. Implement a simple persistence layer to save/load tasks from a file
  5. Add unit tests for your Python implementation

Submit your work as a Python module with clear organization. Be prepared to explain how you applied Pythonic OOP patterns and how they compare to the JavaScript original.

Further Reading and Resources