Properties and encapsulation
Encapsulation means an object looks after its own data: other code uses its methods instead of reaching in and changing values directly. That way the object can keep its data valid.
The problem
class Account:
def __init__(self, balance):
self.balance = balance
acct = Account(100)
acct.balance = -5000 # nothing stops this
print(acct.balance)Private by convention
Python has no truly private attributes. Instead, a leading underscore says "internal, don't touch from outside":
class Account:
def __init__(self, balance):
self._balance = balance # internal by convention
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
def get_balance(self):
return self._balance
acct = Account(100)
acct.deposit(50)
print(acct.get_balance())Programmers respect the underscore. (Two underscores, __balance, trigger "name mangling", which makes accidental access harder; it's rarely needed.)
@property: attributes with logic
get_balance() works, but acct.balance reads more naturally. A property looks like an attribute but runs a method:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self): # runs on acct.balance
return self._balance
@balance.setter
def balance(self, value): # runs on acct.balance = ...
if value < 0:
raise ValueError("Balance can't be negative")
self._balance = value
acct = Account(100)
print(acct.balance)
acct.balance = 250
print(acct.balance)
acct.balance = -5Code using the class still writes acct.balance, but every change goes through the validation.
Computed properties
A property can calculate its value every time it's read, so it's never out of date:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
r = Rectangle(3, 4)
print(r.area)
r.width = 10
print(r.area) # updated automaticallyWithout a setter, a property is read-only:
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def diameter(self):
return self.radius * 2
c = Circle(5)
print(c.diameter)
c.diameter = 20