Difficulty: Intermediate
Explain OOP in Python. How do classes, objects, and encapsulation work?
Python supports OOP with classes as blueprints and objects as instances.
Key concepts: - Class: template defining attributes and methods - Object/Instance: concrete realization of a class - __init__: constructor method, called when creating an instance - self: reference to the current instance (explicit in Python) - Encapsulation: controlling access via naming conventions (_private, __mangled)
Python uses naming conventions rather than access modifiers: - public: any_name (accessible from anywhere) - protected: _name (convention: internal use, still accessible) - private: __name (name mangling: _ClassName__name)
class BankAccount:
# Class variable: shared by all instances
bank_name = "Python Bank"
_total_accounts = 0
def __init__(self, owner, balance=0):
# Instance variables: unique to each instance
self.owner = owner
self._balance = balance # protected by convention
self.__pin = 1234 # name-mangled (private)
BankAccount._total_accounts += 1
def deposit(self, amount):
if amount > 0:
self._balance += amount
return f"Deposited ${amount}. Balance: ${self._balance}"
raise ValueError("Amount must be positive")
def __str__(self):
return f"{self.owner}'s account: ${self._balance}"
@classmethod
def get_total_accounts(cls):
return cls._total_accounts
@staticmethod
def validate_amount(amount):
return isinstance(amount, (int, float)) and amount > 0
acc = BankAccount("Alice", 1000)
print(acc) # Alice's account: $1000
print(acc.deposit(500)) # Deposited $500. Balance: $1500
print(BankAccount.get_total_accounts()) # 1
print(BankAccount.validate_amount(100)) # True
Class variables are shared across instances. Instance variables (set in __init__) are unique per object. @classmethod receives the class, @staticmethod receives nothing.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius # store internally
@property
def celsius(self):
"""Getter: accessed like an attribute."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter: validates before setting."""
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Computed property: read-only."""
return self._celsius * 9/5 + 32
t = Temperature(25)
print(t.celsius) # 25 (calls getter)
print(t.fahrenheit) # 77.0 (computed)
t.celsius = 100 # calls setter
print(t.fahrenheit) # 212.0
# t.celsius = -300 # ValueError: Below absolute zero!
# t.fahrenheit = 50 # AttributeError: can't set (no setter)
@property lets you use methods like attributes, adding validation and computed values. This is Python's preferred encapsulation pattern over explicit getters/setters.
class Secret:
def __init__(self):
self.public = "visible"
self._protected = "convention only"
self.__private = "name-mangled"
s = Secret()
print(s.public) # 'visible'
print(s._protected) # 'convention only' (still accessible!)
# print(s.__private) # AttributeError!
print(s._Secret__private) # 'name-mangled' (mangled name)
# dir() shows all attributes
print([attr for attr in dir(s) if not attr.startswith('__')])
# ['_Secret__private', '_protected', 'public']
# isinstance and type checking
print(isinstance(s, Secret)) # True
print(type(s)) # <class '__main__.Secret'>
# __dict__ shows instance attributes
print(s.__dict__)
# {'public': 'visible', '_protected': 'convention only', '_Secret__private': 'name-mangled'}
Python has no true private attributes. __name becomes _ClassName__name (name mangling). The convention is: _ means 'please don't touch', __ means 'really don't touch'.
Classes, Objects, Encapsulation, Class vs Instance, __init__, Properties