link experiment
This commit is contained in:
335
Notes/05_Object_model/02_Classes_encapsulation.md
Normal file
335
Notes/05_Object_model/02_Classes_encapsulation.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# 5.2 Classes and Encapsulation
|
||||
|
||||
When writing classes, it is common to try and encapsulate internal details.
|
||||
This section introduces a very Python programming idioms for this including
|
||||
private variables and properties.
|
||||
|
||||
### Public vs Private.
|
||||
|
||||
One of the primary roles of a class is to encapsulate data an internal
|
||||
implementation details of an object. However, a class also defines a
|
||||
*public* interface that the outside world is supposed to use to
|
||||
manipulate the object. This distinction between implementation
|
||||
details and the public interface is important.
|
||||
|
||||
### A Problem
|
||||
|
||||
In Python, almost everything about classes and objects is *open*.
|
||||
|
||||
* You can easily inspect object internals.
|
||||
* You can change things at will.
|
||||
* There is no strong notion of access-control (i.e., private class members)
|
||||
|
||||
That is an issue when you are trying to isolate details of the *internal implementation*.
|
||||
|
||||
### Python Encapsulation
|
||||
|
||||
Python relies on programming conventions to indicate the intended use
|
||||
of something. These conventions are based on naming. There is a
|
||||
general attitude that it is up to the programmer to observe the rules
|
||||
as opposed to having the language enforce them.
|
||||
|
||||
### Private Attributes
|
||||
|
||||
Any attribute name with leading `_` is considered to be *private*.
|
||||
|
||||
```python
|
||||
class Person(object):
|
||||
def __init__(self, name):
|
||||
self._name = 0
|
||||
```
|
||||
|
||||
As mentioned earlier, this is only a programming style. You can still access and change it.
|
||||
|
||||
```python
|
||||
>>> p = Person('Guido')
|
||||
>>> p._name
|
||||
'Guido'
|
||||
>>> p._name = 'Dave'
|
||||
>>>
|
||||
```
|
||||
|
||||
### Simple Attributes
|
||||
|
||||
Consider the following class.
|
||||
|
||||
```python
|
||||
class Stock(object):
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
|
||||
s = Stock('GOOG', 100, 490.1)
|
||||
s.shares = 50
|
||||
```
|
||||
|
||||
Suppose later you want to add a validation.
|
||||
|
||||
```python
|
||||
s.shares = '50' # Raise a TypeError, this is a string
|
||||
```
|
||||
|
||||
How would you do it?
|
||||
|
||||
### Managed Attributes
|
||||
|
||||
You might introduce accessor methods.
|
||||
|
||||
```python
|
||||
class Stock(object):
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name self.set_shares(shares) self.price = price
|
||||
|
||||
# Function that layers the "get" operation
|
||||
def get_shares(self):
|
||||
return self._shares
|
||||
|
||||
# Function that layers the "set" operation
|
||||
def set_shares(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError('Expected an int')
|
||||
self._shares = value
|
||||
```
|
||||
|
||||
Too bad that this breaks all of our existing code. `s.shares = 50` becomes `s.set_shares(50)`
|
||||
|
||||
### Properties
|
||||
|
||||
There is an alternative approach to the previous pattern.
|
||||
|
||||
```python
|
||||
class Stock(object):
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
|
||||
@property
|
||||
def shares(self):
|
||||
return self._shares
|
||||
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError('Expected int')
|
||||
self._shares = value
|
||||
```
|
||||
|
||||
Normal attribute access now triggers the getter and setter under `@property` and `@shares.setter`.
|
||||
|
||||
```python
|
||||
class Stock(object):
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
|
||||
# Triggered with `s.shares`
|
||||
@property
|
||||
def shares(self):
|
||||
return self._shares
|
||||
|
||||
# Triggered with `s.shares = ...`
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError('Expected int')
|
||||
self._shares = value
|
||||
```
|
||||
|
||||
With this pattern, there are *no changes* needed to the source code.
|
||||
The new *setter* is also called when there is an assignment within the class.
|
||||
|
||||
```python
|
||||
class Stock(object):
|
||||
def __init__(self, name, shares, price):
|
||||
...
|
||||
# This assignment calls the setter below
|
||||
self.shares = shares
|
||||
...
|
||||
|
||||
...
|
||||
@shares.setter
|
||||
def shares(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise TypeError('Expected int')
|
||||
self._shares = value
|
||||
```
|
||||
|
||||
There is often a confusion between a property and the use of private names.
|
||||
Although a property internally uses a private name like `_shares`, the rest
|
||||
of the class (not the property) can continue to use a name like `shares`.
|
||||
|
||||
Properties are also useful for computed data attributes.
|
||||
|
||||
```python
|
||||
class Stock(object):
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self.shares * self.price
|
||||
...
|
||||
```
|
||||
|
||||
This allows you to drop the extra parantheses, hiding the fact that it's actually method:
|
||||
|
||||
```python
|
||||
>>> s = Stock('GOOG', 100, 490.1)
|
||||
>>> s.shares # Instance variable
|
||||
100
|
||||
>>> s.cost # Computed Value
|
||||
49010.0
|
||||
>>>
|
||||
```
|
||||
|
||||
### Uniform access
|
||||
|
||||
The last example shows how to put a more uniform interface on an object.
|
||||
If you don't do this, an object might be confusing to use:
|
||||
|
||||
```python
|
||||
>>> s = Stock('GOOG', 100, 490.1)
|
||||
>>> a = s.cost() # Method
|
||||
49010.0
|
||||
>>> b = s.shares # Data attribute
|
||||
100
|
||||
>>>
|
||||
```
|
||||
|
||||
Why is the `()` required for the cost, but not for the shares? A property
|
||||
can fix this.
|
||||
|
||||
### Decorator Syntax
|
||||
|
||||
The `@` syntax is known as *decoration".
|
||||
It specifies a modifier that's applied to the function definition that immediately follows.
|
||||
|
||||
```python
|
||||
...
|
||||
@property
|
||||
def cost(self):
|
||||
return self.shares * self.price
|
||||
```
|
||||
|
||||
It's kind of like a macro. More details in Section 7.
|
||||
|
||||
### `__slots__` Attribute
|
||||
|
||||
You can restrict the set of attributes names.
|
||||
|
||||
```python
|
||||
class Stock(object):
|
||||
__slots__ = ('name','_shares','price')
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
...
|
||||
```
|
||||
|
||||
It will raise an error for other attributes.
|
||||
|
||||
```python
|
||||
>>> s.price = 385.15
|
||||
>>> s.prices = 410.2
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
AttributeError: 'Stock' object has no attribute 'prices'
|
||||
```
|
||||
|
||||
It prevents errors and restricts usage of objects. It's actually used for performance and
|
||||
makes Python use memory more efficiently.
|
||||
|
||||
### Final Comments on Encapsulation
|
||||
|
||||
Don't go overboard with private attributes, properties, slots,
|
||||
etc. They serve a specific purpose and you may see them when reading
|
||||
other Python code. However, they are not necessary for most
|
||||
day-to-day coding.
|
||||
|
||||
## Exercises
|
||||
|
||||
### (a) Simple properties
|
||||
|
||||
Properties are a useful way to add "computed attributes" to an object.
|
||||
In Exercise 4.1, you created an object `Stock`. Notice that on your
|
||||
object there is a slight inconsistency in how different kinds of data
|
||||
are extracted:
|
||||
|
||||
```python
|
||||
>>> from stock import Stock
|
||||
>>> s = Stock('GOOG', 100, 490.1)
|
||||
>>> s.shares
|
||||
100
|
||||
>>> s.price
|
||||
490.1
|
||||
>>> s.cost()
|
||||
49010.0
|
||||
>>>
|
||||
```
|
||||
|
||||
Specifically, notice how you have to add the extra `()` to `cost` because it is a method.
|
||||
You can get rid of the extra `()` on `cost()` if you turn it into a property.
|
||||
Take your `Stock` class and modify it so that the cost calculation works like this:
|
||||
|
||||
```python
|
||||
>>> s.cost
|
||||
49010.0
|
||||
>>>
|
||||
```
|
||||
|
||||
Try calling `s.cost()` as a function and observe that it doesn’t work now that `cost` has been defined as a property.
|
||||
|
||||
```python
|
||||
>>> s.cost()
|
||||
... fails ...
|
||||
>>>
|
||||
```
|
||||
|
||||
### (b) Properties and Setters
|
||||
|
||||
Modify the `shares` attribute so that the value is stored in a private
|
||||
attribute and that a pair of property functions are used to ensure
|
||||
that it is always set to an integer value.
|
||||
Here is an example of the expected behavior:
|
||||
|
||||
```python
|
||||
>>> s = Stock('GOOG',100,490.10)
|
||||
>>> s.shares = 50
|
||||
>>> s.shares = 'a lot'
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: expected an integer
|
||||
>>>
|
||||
```
|
||||
|
||||
### (c) Adding slots
|
||||
|
||||
Modify the `Stock` class so that it has a `__slots__` attribute.
|
||||
Then, verify that new attributes can’t be added:
|
||||
|
||||
```python
|
||||
>>> from stock import Stock
|
||||
>>> s = Stock('GOOG', 100, 490.10)
|
||||
>>> s.name
|
||||
'GOOG'
|
||||
>>> s.blah = 42
|
||||
... see what happens ...
|
||||
>>>
|
||||
```
|
||||
|
||||
When you use `__slots__`, Python actually uses a more efficient internal representation of objects.
|
||||
What happens if you try to inspect the underlying dictionary of `s` above?
|
||||
|
||||
```python
|
||||
>>> s.__dict__
|
||||
... see what happens ...
|
||||
>>>
|
||||
```
|
||||
|
||||
It should be noted that `__slots__` is most commonly used as an
|
||||
optimization on classes that serve as data structures. Using slots
|
||||
will make such programs use far-less memory and run a bit faster.
|
||||
Reference in New Issue
Block a user