Naming Conventions
Python's Approach to Encapsulation
Python doesn't have true private attributes. Instead, it uses naming conventions:
class Person:
def __init__(self, name, age, ssn):
self.name = name # Public
self._age = age # Protected (convention only)
self.__ssn = ssn # Name-mangled
p = Person('Alice', 30, '123-45-6789')
print(p.name) # ✅ Alice
print(p._age) # ✅ 30 (accessible, but convention says don't)
# print(p.__ssn) # ❌ AttributeError
print(p._Person__ssn) # ✅ 123-45-6789 (name-mangled)
Naming Conventions
| Prefix | Convention | Example |
|---|---|---|
name |
Public | self.name |
_name |
Protected (internal use) | self._age |
__name |
Name-mangled (prevents subclass override) | self.__ssn |
__name__ |
Dunder/magic methods | self.__init__ |
When to Use Each
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public: anyone can access
self._balance = balance # Protected: internal use
self.__pin = '1234' # Private: sensitive data
def deposit(self, amount):
# Access protected attribute directly in class
self._balance += amount
return self._balance
account = BankAccount('Alice', 1000)
print(account.owner) # ✅ Alice
print(account._balance) # ✅ 1000 (works, but not recommended)
# print(account.__pin) # ❌ AttributeError
Properties
Basic Property
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = value
@property
def area(self):
"""Calculate area (read-only)."""
import math
return math.pi * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # ✅ Setter called
print(c.area) # 314.159...
# c.area = 100 # ❌ AttributeError (read-only)
# c.radius = -5 # ❌ ValueError: Radius cannot be negative
Property vs Getter/Setter
# ❌ Java-style getters/setters
class Person:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, name):
self._name = name
person = Person('Alice')
name = person.get_name() # Not Pythonic
# ✅ Python-style properties
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
person = Person('Alice')
name = person.name # Pythonic!
Computed Properties
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
t = Temperature(100)
print(t.fahrenheit) # 212.0
t.fahrenheit = 32
print(t.celsius) # 0.0
cached_property (Python 3.8+)
from functools import cached_property
class DataAnalyzer:
def __init__(self, data):
self.data = data
@cached_property
def statistics(self):
# Computed once, then cached
print('Computing statistics...')
return {
'mean': sum(self.data) / len(self.data),
'min': min(self.data),
'max': max(self.data)
}
analyzer = DataAnalyzer([1, 2, 3, 4, 5])
print(analyzer.statistics) # Computes and prints
print(analyzer.statistics) # Returns cached (no 'Computing' print)
Slots & Immutable Objects
slots for Memory Optimization
class PointWithDict:
def __init__(self, x, y):
self.x = x
self.y = y
class PointWithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# PointWithDict uses ~150 bytes per instance
# PointWithSlots uses ~56 bytes per instance
import sys
p1 = PointWithDict(1, 2)
p2 = PointWithSlots(1, 2)
print(sys.getsizeof(p1.__dict__)) # 104 bytes (dict)
# print(sys.getsizeof(p2.__dict__)) # ❌ AttributeError (no dict)
Benefits of slots
class SlotClass:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# ✅ Memory savings
# ✅ Faster attribute access
# ✅ Prevents accidental attribute creation
s = SlotClass(1, 2)
# s.z = 3 # ❌ AttributeError: 'SlotClass' object has no attribute 'z'
Immutable Objects
class ImmutablePoint:
__slots__ = ('_x', '_y')
def __init__(self, x, y):
object.__setattr__(self, '_x', x)
object.__setattr__(self, '_y', y)
@property
def x(self):
return self._x
@property
def y(self):
return self._y
def __hash__(self):
return hash((self._x, self._y))
p = ImmutablePoint(1, 2)
print(p.x, p.y) # 1 2
# p.x = 3 # ❌ AttributeError (no setter)
# Works as dictionary key or in set
locations = {p: 'origin'}
dataclass with frozen=True
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0)
# p.x = 3.0 # ❌ FrozenInstanceError
# Can be used as dict key
locations = {p: 'origin'}