chore: clean repository history

This commit is contained in:
xixu-me committed 2026-06-05 21:13:11 +08:00
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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)