chore: clean repository history
This commit is contained in:
commit
e1fb2929c0
661 files changed
+167488
No files matched your search
@@ -0,0 +1,21 @@
|
||||
[Contents](../Contents.md) \| [Prev (6 Generators)](../06_Generators/00_Overview.md) \| [Next (8 Testing and Debugging)](../08_Testing_debugging/00_Overview.md)
|
||||
|
||||
# 7. Advanced Topics
|
||||
|
||||
In this section, we look at a small set of somewhat more advanced
|
||||
Python features that you might encounter in your day-to-day coding.
|
||||
Many of these topics could have been covered in earlier course
|
||||
sections, but weren't in order to spare you further head-explosion at
|
||||
the time.
|
||||
|
||||
It should be emphasized that the topics in this section are only meant
|
||||
to serve as a very basic introduction to these ideas. You will need
|
||||
to seek more advanced material to fill out details.
|
||||
|
||||
* [7.1 Variable argument functions](01_Variable_arguments.md)
|
||||
* [7.2 Anonymous functions and lambda](02_Anonymous_function.md)
|
||||
* [7.3 Returning function and closures](03_Returning_functions.md)
|
||||
* [7.4 Function decorators](04_Function_decorators.md)
|
||||
* [7.5 Static and class methods](05_Decorated_methods.md)
|
||||
|
||||
[Contents](../Contents.md) \| [Prev (6 Generators)](../06_Generators/00_Overview.md) \| [Next (8 Testing and Debugging)](../08_Testing_debugging/00_Overview.md)
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
|
||||
[Contents](../Contents.md) \| [Previous (6.4 Generator Expressions)](../06_Generators/04_More_generators.md) \| [Next (7.2 Anonymous Functions)](02_Anonymous_function.md)
|
||||
|
||||
# 7.1 Variable Arguments
|
||||
|
||||
This section covers variadic function arguments, sometimes described as
|
||||
`*args` and `**kwargs`.
|
||||
|
||||
### Positional variable arguments (*args)
|
||||
|
||||
A function that accepts *any number* of arguments is said to use variable arguments.
|
||||
For example:
|
||||
|
||||
```python
|
||||
def f(x, *args):
|
||||
...
|
||||
```
|
||||
|
||||
Function call.
|
||||
|
||||
```python
|
||||
f(1,2,3,4,5)
|
||||
```
|
||||
|
||||
The extra arguments get passed as a tuple.
|
||||
|
||||
```python
|
||||
def f(x, *args):
|
||||
# x -> 1
|
||||
# args -> (2,3,4,5)
|
||||
```
|
||||
|
||||
### Keyword variable arguments (**kwargs)
|
||||
|
||||
A function can also accept any number of keyword arguments.
|
||||
For example:
|
||||
|
||||
```python
|
||||
def f(x, y, **kwargs):
|
||||
...
|
||||
```
|
||||
|
||||
Function call.
|
||||
|
||||
```python
|
||||
f(2, 3, flag=True, mode='fast', header='debug')
|
||||
```
|
||||
|
||||
The extra keywords are passed in a dictionary.
|
||||
|
||||
```python
|
||||
def f(x, y, **kwargs):
|
||||
# x -> 2
|
||||
# y -> 3
|
||||
# kwargs -> { 'flag': True, 'mode': 'fast', 'header': 'debug' }
|
||||
```
|
||||
|
||||
### Combining both
|
||||
|
||||
A function can also accept any number of variable keyword and non-keyword arguments.
|
||||
|
||||
```python
|
||||
def f(*args, **kwargs):
|
||||
...
|
||||
```
|
||||
|
||||
Function call.
|
||||
|
||||
```python
|
||||
f(2, 3, flag=True, mode='fast', header='debug')
|
||||
```
|
||||
|
||||
The arguments are separated into positional and keyword components
|
||||
|
||||
```python
|
||||
def f(*args, **kwargs):
|
||||
# args = (2, 3)
|
||||
# kwargs -> { 'flag': True, 'mode': 'fast', 'header': 'debug' }
|
||||
...
|
||||
```
|
||||
|
||||
This function takes any combination of positional or keyword
|
||||
arguments. It is sometimes used when writing wrappers or when you
|
||||
want to pass arguments through to another function.
|
||||
|
||||
### Passing Tuples and Dicts
|
||||
|
||||
Tuples can be expanded into variable arguments.
|
||||
|
||||
```python
|
||||
numbers = (2,3,4)
|
||||
f(1, *numbers) # Same as f(1,2,3,4)
|
||||
```
|
||||
|
||||
Dictionaries can also be expanded into keyword arguments.
|
||||
|
||||
```python
|
||||
options = {
|
||||
'color' : 'red',
|
||||
'delimiter' : ',',
|
||||
'width' : 400
|
||||
}
|
||||
f(data, **options)
|
||||
# Same as f(data, color='red', delimiter=',', width=400)
|
||||
```
|
||||
|
||||
## Exercises
|
||||
|
||||
### Exercise 7.1: A simple example of variable arguments
|
||||
|
||||
Try defining the following function:
|
||||
|
||||
```python
|
||||
>>> def avg(x,*more):
|
||||
return float(x+sum(more))/(1+len(more))
|
||||
|
||||
>>> avg(10,11)
|
||||
10.5
|
||||
>>> avg(3,4,5)
|
||||
4.0
|
||||
>>> avg(1,2,3,4,5,6)
|
||||
3.5
|
||||
>>>
|
||||
```
|
||||
|
||||
Notice how the parameter `*more` collects all of the extra arguments.
|
||||
|
||||
### Exercise 7.2: Passing tuple and dicts as arguments
|
||||
|
||||
Suppose you read some data from a file and obtained a tuple such as
|
||||
this:
|
||||
|
||||
```
|
||||
>>> data = ('GOOG', 100, 490.1)
|
||||
>>>
|
||||
```
|
||||
|
||||
Now, suppose you wanted to create a `Stock` object from this
|
||||
data. If you try to pass `data` directly, it doesn't work:
|
||||
|
||||
```
|
||||
>>> from stock import Stock
|
||||
>>> s = Stock(data)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in <module>
|
||||
TypeError: __init__() takes exactly 4 arguments (2 given)
|
||||
>>>
|
||||
```
|
||||
|
||||
This is easily fixed using `*data` instead. Try this:
|
||||
|
||||
```python
|
||||
>>> s = Stock(*data)
|
||||
>>> s
|
||||
Stock('GOOG', 100, 490.1)
|
||||
>>>
|
||||
```
|
||||
|
||||
If you have a dictionary, you can use `**` instead. For example:
|
||||
|
||||
```python
|
||||
>>> data = { 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
|
||||
>>> s = Stock(**data)
|
||||
Stock('GOOG', 100, 490.1)
|
||||
>>>
|
||||
```
|
||||
|
||||
### Exercise 7.3: Creating a list of instances
|
||||
|
||||
In your `report.py` program, you created a list of instances
|
||||
using code like this:
|
||||
|
||||
```python
|
||||
def read_portfolio(filename):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float])
|
||||
|
||||
portfolio = [ Stock(d['name'], d['shares'], d['price'])
|
||||
for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
```
|
||||
|
||||
You can simplify that code using `Stock(**d)` instead. Make that change.
|
||||
|
||||
### Exercise 7.4: Argument pass-through
|
||||
|
||||
The `fileparse.parse_csv()` function has some options for changing the
|
||||
file delimiter and for error reporting. Maybe you'd like to expose those
|
||||
options to the `read_portfolio()` function above. Make this change:
|
||||
|
||||
```
|
||||
def read_portfolio(filename, **opts):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float],
|
||||
**opts)
|
||||
|
||||
portfolio = [ Stock(**d) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
```
|
||||
|
||||
Once you've made the change, trying reading a file with some errors:
|
||||
|
||||
```python
|
||||
>>> import report
|
||||
>>> port = report.read_portfolio('Data/missing.csv')
|
||||
Row 4: Couldn't convert ['MSFT', '', '51.23']
|
||||
Row 4: Reason invalid literal for int() with base 10: ''
|
||||
Row 7: Couldn't convert ['IBM', '', '70.44']
|
||||
Row 7: Reason invalid literal for int() with base 10: ''
|
||||
>>>
|
||||
```
|
||||
|
||||
Now, try silencing the errors:
|
||||
|
||||
```python
|
||||
>>> import report
|
||||
>>> port = report.read_portfolio('Data/missing.csv', silence_errors=True)
|
||||
>>>
|
||||
```
|
||||
|
||||
[Contents](../Contents.md) \| [Previous (6.4 Generator Expressions)](../06_Generators/04_More_generators.md) \| [Next (7.2 Anonymous Functions)](02_Anonymous_function.md)
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
[Contents](../Contents.md) \| [Previous (7.1 Variable Arguments)](01_Variable_arguments.md) \| [Next (7.3 Returning Functions)](03_Returning_functions.md)
|
||||
|
||||
# 7.2 Anonymous Functions and Lambda
|
||||
|
||||
### List Sorting Revisited
|
||||
|
||||
Lists can be sorted *in-place*. Using the `sort` method.
|
||||
|
||||
```python
|
||||
s = [10,1,7,3]
|
||||
s.sort() # s = [1,3,7,10]
|
||||
```
|
||||
|
||||
You can sort in reverse order.
|
||||
|
||||
```python
|
||||
s = [10,1,7,3]
|
||||
s.sort(reverse=True) # s = [10,7,3,1]
|
||||
```
|
||||
|
||||
It seems simple enough. However, how do we sort a list of dicts?
|
||||
|
||||
```python
|
||||
[{'name': 'AA', 'price': 32.2, 'shares': 100},
|
||||
{'name': 'IBM', 'price': 91.1, 'shares': 50},
|
||||
{'name': 'CAT', 'price': 83.44, 'shares': 150},
|
||||
{'name': 'MSFT', 'price': 51.23, 'shares': 200},
|
||||
{'name': 'GE', 'price': 40.37, 'shares': 95},
|
||||
{'name': 'MSFT', 'price': 65.1, 'shares': 50},
|
||||
{'name': 'IBM', 'price': 70.44, 'shares': 100}]
|
||||
```
|
||||
|
||||
By what criteria?
|
||||
|
||||
You can guide the sorting by using a *key function*. The *key
|
||||
function* is a function that receives the dictionary and returns the
|
||||
value of interest for sorting.
|
||||
|
||||
```python
|
||||
def stock_name(s):
|
||||
return s['name']
|
||||
|
||||
portfolio.sort(key=stock_name)
|
||||
```
|
||||
|
||||
Here's the result.
|
||||
|
||||
```python
|
||||
# Check how the dictionaries are sorted by the `name` key
|
||||
[
|
||||
{'name': 'AA', 'price': 32.2, 'shares': 100},
|
||||
{'name': 'CAT', 'price': 83.44, 'shares': 150},
|
||||
{'name': 'GE', 'price': 40.37, 'shares': 95},
|
||||
{'name': 'IBM', 'price': 91.1, 'shares': 50},
|
||||
{'name': 'IBM', 'price': 70.44, 'shares': 100},
|
||||
{'name': 'MSFT', 'price': 51.23, 'shares': 200},
|
||||
{'name': 'MSFT', 'price': 65.1, 'shares': 50}
|
||||
]
|
||||
```
|
||||
|
||||
### Callback Functions
|
||||
|
||||
In the above example, the key function is an example of a callback
|
||||
function. The `sort()` method "calls back" to a function you supply.
|
||||
Callback functions are often short one-line functions that are only
|
||||
used for that one operation. Programmers often ask for a short-cut
|
||||
for specifying this extra processing.
|
||||
|
||||
### Lambda: Anonymous Functions
|
||||
|
||||
Use a lambda instead of creating the function. In our previous
|
||||
sorting example.
|
||||
|
||||
```python
|
||||
portfolio.sort(key=lambda s: s['name'])
|
||||
```
|
||||
|
||||
This creates an *unnamed* function that evaluates a *single* expression.
|
||||
The above code is much shorter than the initial code.
|
||||
|
||||
```python
|
||||
def stock_name(s):
|
||||
return s['name']
|
||||
|
||||
portfolio.sort(key=stock_name)
|
||||
|
||||
# vs lambda
|
||||
portfolio.sort(key=lambda s: s['name'])
|
||||
```
|
||||
|
||||
### Using lambda
|
||||
|
||||
* lambda is highly restricted.
|
||||
* Only a single expression is allowed.
|
||||
* No statements like `if`, `while`, etc.
|
||||
* Most common use is with functions like `sort()`.
|
||||
|
||||
## Exercises
|
||||
|
||||
Read some stock portfolio data and convert it into a list:
|
||||
|
||||
```python
|
||||
>>> import report
|
||||
>>> portfolio = list(report.read_portfolio('Data/portfolio.csv'))
|
||||
>>> for s in portfolio:
|
||||
print(s)
|
||||
|
||||
Stock('AA', 100, 32.2)
|
||||
Stock('IBM', 50, 91.1)
|
||||
Stock('CAT', 150, 83.44)
|
||||
Stock('MSFT', 200, 51.23)
|
||||
Stock('GE', 95, 40.37)
|
||||
Stock('MSFT', 50, 65.1)
|
||||
Stock('IBM', 100, 70.44)
|
||||
>>>
|
||||
```
|
||||
|
||||
### Exercise 7.5: Sorting on a field
|
||||
|
||||
Try the following statements which sort the portfolio data
|
||||
alphabetically by stock name.
|
||||
|
||||
```python
|
||||
>>> def stock_name(s):
|
||||
return s.name
|
||||
|
||||
>>> portfolio.sort(key=stock_name)
|
||||
>>> for s in portfolio:
|
||||
print(s)
|
||||
|
||||
... inspect the result ...
|
||||
>>>
|
||||
```
|
||||
|
||||
In this part, the `stock_name()` function extracts the name of a stock from
|
||||
a single entry in the `portfolio` list. `sort()` uses the result of
|
||||
this function to do the comparison.
|
||||
|
||||
### Exercise 7.6: Sorting on a field with lambda
|
||||
|
||||
Try sorting the portfolio according the number of shares using a
|
||||
`lambda` expression:
|
||||
|
||||
```python
|
||||
>>> portfolio.sort(key=lambda s: s.shares)
|
||||
>>> for s in portfolio:
|
||||
print(s)
|
||||
|
||||
... inspect the result ...
|
||||
>>>
|
||||
```
|
||||
|
||||
Try sorting the portfolio according to the price of each stock
|
||||
|
||||
```python
|
||||
>>> portfolio.sort(key=lambda s: s.price)
|
||||
>>> for s in portfolio:
|
||||
print(s)
|
||||
|
||||
... inspect the result ...
|
||||
>>>
|
||||
```
|
||||
|
||||
Note: `lambda` is a useful shortcut because it allows you to
|
||||
define a special processing function directly in the call to `sort()` as
|
||||
opposed to having to define a separate function first.
|
||||
|
||||
[Contents](../Contents.md) \| [Previous (7.1 Variable Arguments)](01_Variable_arguments.md) \| [Next (7.3 Returning Functions)](03_Returning_functions.md)
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
[Contents](../Contents.md) \| [Previous (7.2 Anonymous Functions)](02_Anonymous_function.md) \| [Next (7.4 Decorators)](04_Function_decorators.md)
|
||||
|
||||
# 7.3 Returning Functions
|
||||
|
||||
This section introduces the idea of using functions to create other functions.
|
||||
|
||||
### Introduction
|
||||
|
||||
Consider the following function.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
def do_add():
|
||||
print('Adding', x, y)
|
||||
return x + y
|
||||
return do_add
|
||||
```
|
||||
|
||||
This is a function that returns another function.
|
||||
|
||||
```python
|
||||
>>> a = add(3,4)
|
||||
>>> a
|
||||
<function do_add at 0x6a670>
|
||||
>>> a()
|
||||
Adding 3 4
|
||||
7
|
||||
```
|
||||
|
||||
### Local Variables
|
||||
|
||||
Observe how the inner function refers to variables defined by the outer
|
||||
function.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
def do_add():
|
||||
# `x` and `y` are defined above `add(x, y)`
|
||||
print('Adding', x, y)
|
||||
return x + y
|
||||
return do_add
|
||||
```
|
||||
|
||||
Further observe that those variables are somehow kept alive after
|
||||
`add()` has finished.
|
||||
|
||||
```python
|
||||
>>> a = add(3,4)
|
||||
>>> a
|
||||
<function do_add at 0x6a670>
|
||||
>>> a()
|
||||
Adding 3 4 # Where are these values coming from?
|
||||
7
|
||||
```
|
||||
|
||||
### Closures
|
||||
|
||||
When an inner function is returned as a result, that inner function is known as a *closure*.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
# `do_add` is a closure
|
||||
def do_add():
|
||||
print('Adding', x, y)
|
||||
return x + y
|
||||
return do_add
|
||||
```
|
||||
|
||||
*Essential feature: A closure retains the values of all variables
|
||||
needed for the function to run properly later on.* Think of a
|
||||
closure as a function plus an extra environment that holds the values
|
||||
of variables that it depends on.
|
||||
|
||||
### Using Closures
|
||||
|
||||
Closure are an essential feature of Python. However, their use if often subtle.
|
||||
Common applications:
|
||||
|
||||
* Use in callback functions.
|
||||
* Delayed evaluation.
|
||||
* Decorator functions (later).
|
||||
|
||||
### Delayed Evaluation
|
||||
|
||||
Consider a function like this:
|
||||
|
||||
```python
|
||||
def after(seconds, func):
|
||||
import time
|
||||
time.sleep(seconds)
|
||||
func()
|
||||
```
|
||||
|
||||
Usage example:
|
||||
|
||||
```python
|
||||
def greeting():
|
||||
print('Hello Guido')
|
||||
|
||||
after(30, greeting)
|
||||
```
|
||||
|
||||
`after` executes the supplied function... later.
|
||||
|
||||
Closures carry extra information around.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
def do_add():
|
||||
print(f'Adding {x} + {y} -> {x+y}')
|
||||
return do_add
|
||||
|
||||
def after(seconds, func):
|
||||
import time
|
||||
time.sleep(seconds)
|
||||
func()
|
||||
|
||||
after(30, add(2, 3))
|
||||
# `do_add` has the references x -> 2 and y -> 3
|
||||
```
|
||||
|
||||
### Code Repetition
|
||||
|
||||
Closures can also be used as technique for avoiding excessive code repetition.
|
||||
You can write functions that make code.
|
||||
|
||||
## Exercises
|
||||
|
||||
### Exercise 7.7: Using Closures to Avoid Repetition
|
||||
|
||||
One of the more powerful features of closures is their use in
|
||||
generating repetitive code. If you refer back to [Exercise
|
||||
5.7](../05_Object_model/02_Classes_encapsulation), recall the code for
|
||||
defining a property with type checking.
|
||||
|
||||
```python
|
||||
class Stock:
|
||||
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
|
||||
...
|
||||
```
|
||||
|
||||
Instead of repeatedly typing that code over and over again, you can
|
||||
automatically create it using a closure.
|
||||
|
||||
Make a file `typedproperty.py` and put the following code in
|
||||
it:
|
||||
|
||||
```python
|
||||
# typedproperty.py
|
||||
|
||||
def typedproperty(name, expected_type):
|
||||
private_name = '_' + name
|
||||
@property
|
||||
def prop(self):
|
||||
return getattr(self, private_name)
|
||||
|
||||
@prop.setter
|
||||
def prop(self, value):
|
||||
if not isinstance(value, expected_type):
|
||||
raise TypeError(f'Expected {expected_type}')
|
||||
setattr(self, private_name, value)
|
||||
|
||||
return prop
|
||||
```
|
||||
|
||||
Now, try it out by defining a class like this:
|
||||
|
||||
```python
|
||||
from typedproperty import typedproperty
|
||||
|
||||
class Stock:
|
||||
name = typedproperty('name', str)
|
||||
shares = typedproperty('shares', int)
|
||||
price = typedproperty('price', float)
|
||||
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
```
|
||||
|
||||
Try creating an instance and verifying that type-checking works.
|
||||
|
||||
```python
|
||||
>>> s = Stock('IBM', 50, 91.1)
|
||||
>>> s.name
|
||||
'IBM'
|
||||
>>> s.shares = '100'
|
||||
... should get a TypeError ...
|
||||
>>>
|
||||
```
|
||||
|
||||
### Exercise 7.8: Simplifying Function Calls
|
||||
|
||||
In the above example, users might find calls such as
|
||||
`typedproperty('shares', int)` a bit verbose to type--especially if
|
||||
they're repeated a lot. Add the following definitions to the
|
||||
`typedproperty.py` file:
|
||||
|
||||
```python
|
||||
String = lambda name: typedproperty(name, str)
|
||||
Integer = lambda name: typedproperty(name, int)
|
||||
Float = lambda name: typedproperty(name, float)
|
||||
```
|
||||
|
||||
Now, rewrite the `Stock` class to use these functions instead:
|
||||
|
||||
```python
|
||||
class Stock:
|
||||
name = String('name')
|
||||
shares = Integer('shares')
|
||||
price = Float('price')
|
||||
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
```
|
||||
|
||||
Ah, that's a bit better. The main takeaway here is that closures and `lambda`
|
||||
can often be used to simplify code and eliminate annoying repetition. This
|
||||
is often good.
|
||||
|
||||
### Exercise 7.9: Putting it into practice
|
||||
|
||||
Rewrite the `Stock` class in the file `stock.py` so that it uses typed properties
|
||||
as shown.
|
||||
|
||||
[Contents](../Contents.md) \| [Previous (7.2 Anonymous Functions)](02_Anonymous_function.md) \| [Next (7.4 Decorators)](04_Function_decorators.md)
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
[Contents](../Contents.md) \| [Previous (7.3 Returning Functions)](03_Returning_functions.md) \| [Next (7.5 Decorated Methods)](05_Decorated_methods.md)
|
||||
|
||||
# 7.4 Function Decorators
|
||||
|
||||
This section introduces the concept of a decorator. This is an advanced
|
||||
topic for which we only scratch the surface.
|
||||
|
||||
### Logging Example
|
||||
|
||||
Consider a function.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
return x + y
|
||||
```
|
||||
|
||||
Now, consider the function with some logging added to it.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
print('Calling add')
|
||||
return x + y
|
||||
```
|
||||
|
||||
Now a second function also with some logging.
|
||||
|
||||
```python
|
||||
def sub(x, y):
|
||||
print('Calling sub')
|
||||
return x - y
|
||||
```
|
||||
|
||||
### Observation
|
||||
|
||||
*Observation: It's kind of repetitive.*
|
||||
|
||||
Writing programs where there is a lot of code replication is often
|
||||
really annoying. They are tedious to write and hard to maintain.
|
||||
Especially if you decide that you want to change how it works (i.e., a
|
||||
different kind of logging perhaps).
|
||||
|
||||
### Code that makes logging
|
||||
|
||||
Perhaps you can make a function that makes functions with logging
|
||||
added to them. A wrapper.
|
||||
|
||||
```python
|
||||
def logged(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
print('Calling', func.__name__)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
```
|
||||
|
||||
Now use it.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
logged_add = logged(add)
|
||||
```
|
||||
|
||||
What happens when you call the function returned by `logged`?
|
||||
|
||||
```python
|
||||
logged_add(3, 4) # You see the logging message appear
|
||||
```
|
||||
|
||||
This example illustrates the process of creating a so-called *wrapper function*.
|
||||
|
||||
A wrapper is a function that wraps around another function with some
|
||||
extra bits of processing, but otherwise works in the exact same way
|
||||
as the original function.
|
||||
|
||||
```python
|
||||
>>> logged_add(3, 4)
|
||||
Calling add # Extra output. Added by the wrapper
|
||||
7
|
||||
>>>
|
||||
```
|
||||
|
||||
*Note: The `logged()` function creates the wrapper and returns it as a result.*
|
||||
|
||||
## Decorators
|
||||
|
||||
Putting wrappers around functions is extremely common in Python.
|
||||
So common, there is a special syntax for it.
|
||||
|
||||
```python
|
||||
def add(x, y):
|
||||
return x + y
|
||||
add = logged(add)
|
||||
|
||||
# Special syntax
|
||||
@logged
|
||||
def add(x, y):
|
||||
return x + y
|
||||
```
|
||||
|
||||
The special syntax performs the same exact steps as shown above. A decorator is just new syntax.
|
||||
It is said to *decorate* the function.
|
||||
|
||||
### Commentary
|
||||
|
||||
There are many more subtle details to decorators than what has been presented here.
|
||||
For example, using them in classes. Or using multiple decorators with a function.
|
||||
However, the previous example is a good illustration of how their use tends to arise.
|
||||
Usually, it's in response to repetitive code appearing across a wide range of
|
||||
function definitions. A decorator can move that code to a central definition.
|
||||
|
||||
## Exercises
|
||||
|
||||
### Exercise 7.10: A decorator for timing
|
||||
|
||||
If you define a function, its name and module are stored in the
|
||||
`__name__` and `__module__` attributes. For example:
|
||||
|
||||
```python
|
||||
>>> def add(x,y):
|
||||
return x+y
|
||||
|
||||
>>> add.__name__
|
||||
'add'
|
||||
>>> add.__module__
|
||||
'__main__'
|
||||
>>>
|
||||
```
|
||||
|
||||
In a file `timethis.py`, write a decorator function `timethis(func)`
|
||||
that wraps a function with an extra layer of logic that prints out how
|
||||
long it takes for a function to execute. To do this, you'll surround
|
||||
the function with timing calls like this:
|
||||
|
||||
```python
|
||||
start = time.time()
|
||||
r = func(*args,**kwargs)
|
||||
end = time.time()
|
||||
print('%s.%s: %f' % (func.__module__, func.__name__, end-start))
|
||||
```
|
||||
|
||||
Here is an example of how your decorator should work:
|
||||
|
||||
```python
|
||||
>>> from timethis import timethis
|
||||
>>> @timethis
|
||||
def countdown(n):
|
||||
while n > 0:
|
||||
n -= 1
|
||||
|
||||
>>> countdown(10000000)
|
||||
__main__.countdown : 0.076562
|
||||
>>>
|
||||
```
|
||||
|
||||
Discussion: This `@timethis` decorator can be placed in front of any
|
||||
function definition. Thus, you might use it as a diagnostic tool for
|
||||
performance tuning.
|
||||
|
||||
[Contents](../Contents.md) \| [Previous (7.3 Returning Functions)](03_Returning_functions.md) \| [Next (7.5 Decorated Methods)](05_Decorated_methods.md)
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
[Contents](../Contents.md) \| [Previous (7.4 Decorators)](04_Function_decorators.md) \| [Next (8 Testing and Debugging)](../08_Testing_debugging/00_Overview.md)
|
||||
|
||||
# 7.5 Decorated Methods
|
||||
|
||||
This section discusses a few built-in decorators that are used in
|
||||
combination with method definitions.
|
||||
|
||||
### Predefined Decorators
|
||||
|
||||
There are predefined decorators used to specify special kinds of methods in class definitions.
|
||||
|
||||
```python
|
||||
class Foo:
|
||||
def bar(self,a):
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def spam(a):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def grok(cls,a):
|
||||
...
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
...
|
||||
```
|
||||
|
||||
Let's go one by one.
|
||||
|
||||
### Static Methods
|
||||
|
||||
`@staticmethod` is used to define a so-called *static* class methods
|
||||
(from C++/Java). A static method is a function that is part of the
|
||||
class, but which does *not* operate on instances.
|
||||
|
||||
```python
|
||||
class Foo(object):
|
||||
@staticmethod
|
||||
def bar(x):
|
||||
print('x =', x)
|
||||
|
||||
>>> Foo.bar(2) x=2
|
||||
>>>
|
||||
```
|
||||
|
||||
Static methods are sometimes used to implement internal supporting
|
||||
code for a class. For example, code to help manage created instances
|
||||
(memory management, system resources, persistence, locking, etc).
|
||||
They're also used by certain design patterns (not discussed here).
|
||||
|
||||
### Class Methods
|
||||
|
||||
`@classmethod` is used to define class methods. A class method is a
|
||||
method that receives the *class* object as the first parameter instead
|
||||
of the instance.
|
||||
|
||||
```python
|
||||
class Foo:
|
||||
def bar(self):
|
||||
print(self)
|
||||
|
||||
@classmethod
|
||||
def spam(cls):
|
||||
print(cls)
|
||||
|
||||
>>> f = Foo()
|
||||
>>> f.bar()
|
||||
<__main__.Foo object at 0x971690> # The instance `f`
|
||||
>>> Foo.spam()
|
||||
<class '__main__.Foo'> # The class `Foo`
|
||||
>>>
|
||||
```
|
||||
|
||||
Class methods are most often used as a tool for defining alternate constructors.
|
||||
|
||||
```python
|
||||
class Date:
|
||||
def __init__(self,year,month,day):
|
||||
self.year = year
|
||||
self.month = month
|
||||
self.day = day
|
||||
|
||||
@classmethod
|
||||
def today(cls):
|
||||
# Notice how the class is passed as an argument
|
||||
tm = time.localtime()
|
||||
# And used to create a new instance
|
||||
return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)
|
||||
|
||||
d = Date.today()
|
||||
```
|
||||
|
||||
Class methods solve some tricky problems with features like inheritance.
|
||||
|
||||
```python
|
||||
class Date:
|
||||
...
|
||||
@classmethod
|
||||
def today(cls):
|
||||
# Gets the correct class (e.g. `NewDate`)
|
||||
tm = time.localtime()
|
||||
return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)
|
||||
|
||||
class NewDate(Date):
|
||||
...
|
||||
|
||||
d = NewDate.today()
|
||||
```
|
||||
|
||||
## Exercises
|
||||
|
||||
### Exercise 7.11: Class Methods in Practice
|
||||
|
||||
In your `report.py` and `portfolio.py` files, the creation of a `Portfolio`
|
||||
object is a bit muddled. For example, the `report.py` program has code like this:
|
||||
|
||||
```python
|
||||
def read_portfolio(filename, **opts):
|
||||
'''
|
||||
Read a stock portfolio file into a list of dictionaries with keys
|
||||
name, shares, and price.
|
||||
'''
|
||||
with open(filename) as lines:
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float],
|
||||
**opts)
|
||||
|
||||
portfolio = [ Stock(**d) for d in portdicts ]
|
||||
return Portfolio(portfolio)
|
||||
```
|
||||
|
||||
and the `portfolio.py` file defines `Portfolio()` with an odd initializer
|
||||
like this:
|
||||
|
||||
```python
|
||||
class Portfolio:
|
||||
def __init__(self, holdings):
|
||||
self.holdings = holdings
|
||||
...
|
||||
```
|
||||
|
||||
Frankly, the chain of responsibility is all a bit confusing because the
|
||||
code is scattered. If a `Portfolio` class is supposed to contain
|
||||
a list of `Stock` instances, maybe you should change the class to be a bit more clear.
|
||||
Like this:
|
||||
|
||||
```python
|
||||
# portfolio.py
|
||||
|
||||
import stock
|
||||
|
||||
class Portfolio:
|
||||
def __init__(self):
|
||||
self.holdings = []
|
||||
|
||||
def append(self, holding):
|
||||
if not isinstance(holding, stock.Stock):
|
||||
raise TypeError('Expected a Stock instance')
|
||||
self.holdings.append(holding)
|
||||
...
|
||||
```
|
||||
|
||||
If you want to read a portfolio from a CSV file, maybe you should make a
|
||||
class method for it:
|
||||
|
||||
```python
|
||||
# portfolio.py
|
||||
|
||||
import fileparse
|
||||
import stock
|
||||
|
||||
class Portfolio:
|
||||
def __init__(self):
|
||||
self.holdings = []
|
||||
|
||||
def append(self, holding):
|
||||
if not isinstance(holding, stock.Stock):
|
||||
raise TypeError('Expected a Stock instance')
|
||||
self.holdings.append(holding)
|
||||
|
||||
@classmethod
|
||||
def from_csv(cls, lines, **opts):
|
||||
self = cls()
|
||||
portdicts = fileparse.parse_csv(lines,
|
||||
select=['name','shares','price'],
|
||||
types=[str,int,float],
|
||||
**opts)
|
||||
|
||||
for d in portdicts:
|
||||
self.append(stock.Stock(**d))
|
||||
|
||||
return self
|
||||
```
|
||||
|
||||
To use this new Portfolio class, you can now write code like this:
|
||||
|
||||
```
|
||||
>>> from portfolio import Portfolio
|
||||
>>> with open('Data/portfolio.csv') as lines:
|
||||
... port = Portfolio.from_csv(lines)
|
||||
...
|
||||
>>>
|
||||
```
|
||||
|
||||
Make these changes to the `Portfolio` class and modify the `report.py`
|
||||
code to use the class method.
|
||||
|
||||
[Contents](../Contents.md) \| [Previous (7.4 Decorators)](04_Function_decorators.md) \| [Next (8 Testing and Debugging)](../08_Testing_debugging/00_Overview.md)
|
||||
Reference in new issue
Block a user