2021年6月2日——yaco
流畅的Python5-8章内容
第5章:一等函数
在 Python 中,函数是一等对象,编程语言理论家把“一等对象”定义为满 足下述条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
在python中, 整数, 字符串, 列表, 字典都是一等对象,并python中的函数也是一等对象,所以简称为一等函数。
5.1 把函数作为对象
在python中,函数即是对象,具有属性,可以作为参数进行赋值,这里我们创建了一个函数, 然后读取它的 __doc__
属性, 并且确定函数对象其实是 function
类的实例:
def factorial(n):
'''
return n
'''
return 1 if n < 2 else n * factorial(n-1)
print(factorial.__doc__)
print(type(factorial))
print(factorial(3))
'''
OUT
return n
<class 'function'>
6
'''
同样的,我可以通过别的名称使用函数,再把函数作为参数传递到map中进行运算
5.2 高阶函数
高阶函数就是接受函数作为参数, 或者把函数作为返回结果的函数. 如 map
, filter
, reduce
等.
比如调用 sorted
时, 将 len
作为参数传递:
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
# ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
比如,根据反向拼写给一个单词列表排序
在Python3中,由于引入了列表推导和生成器表达式,高阶函数map、filter它们变得没那么重要了,因为列表推导更容易理解和简单。比如,计算阶乘列表:map和filter与列表推导比较
5.3 匿名函数
lambda
关键字是用来创建匿名函数. Python 简单的句法限制了 lambda 函数的定义体只能使用纯表达式。换句话说,lambda 函数的定义体中不能赋值,也不能使用 while 和 try 等语句。
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])
# ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
5.4 可调用对象
除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象 上。如果想判断对象能否调用,可以使用内置的 callable() 函数。 Python 数据模型文档列出了 7 种可调用对象。
- 用户定义的函数:使用def语句或lambda表达式创建,如def fun(): pass
- 内置函数:如len()
- 内置方法:如dict.get
- 方法:在类定义体中的函数
- 类:Class()时会运行类的 __ new __ 方法创建一个实例,然后运行
__ init __ 方法 - 类的实例: 如果类定义了
__ call __
, 那么它的实例可以作为函数调用 - 生成器函数: 使用
yield
关键字的函数或方法.
可以用callable() 函数来检查是否为可调用对象:
5.5 用户定义的可调用类型
不仅 Python 函数是真正的对象,任何 Python 对象都可以表现得像函 数。为此,只需实现实例方法 __call__
。这里定义了一个类BingoCage,并定义了 __call__
方法,所以可以直接用对象来进行函数调用。
import random
class BingoCage:
def __init__(self, items):
self._items = list(items)
random.shuffle(self._items)
def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick form empty BingoCage')
def __call__(self):
return self.pick()
bingo = BingoCage(range(3))
bingo.pick()
# 0
bingo() # 因为重写了__call__()方法,所以当调用bingo()时,相当于执行了call方法
# 1
5.6 函数内省
除了 doc,函数对象还有很多属性。使用 dir 函数可以探知,factorial 具有下述属性:
dir(factorial)
# OUT
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
我们使用dir函数和set集合来找出常规对象没有而函数有的属性
对其中较为典型的属性进行简单说明:
__annotations__
:参数和返回值得注解__code__
:编译成字节码的函数元数据和函数定义体__defaults__
:形式参数的默认值__globals__
:函数所在模块中的全局变量__kwdefaults__
:仅限关键字形式参数的默认值__name__
:函数名称
5.7 从定位参数到仅限关键字参数
定位参数就是可变参数,仅限关键字参数就是关键字参数
def fun(name, age, *args, **kwargs):
pass
其中 *args
和 **kwargs
都是可迭代对象, 展开后映射到单个参数. args是个元组, kwargs是字典。本文定义了一个tag函数用于生成HTML标签,使用名为cls的关键字参数传入“class”属性,其中cls为关键字参数,content为可变参数
def tag(name, *content, cls=None, **attrs):
"""Generate one or more HTML tags"""
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value)
for attr, value
in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' %
(name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)
进行测试,结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yEmPtoGl-1623152747488)(C:/Users/Origin41515/AppData/Roaming/Typora/typora-user-images/image-20210529164005773.png)]
5.9 函数注解
Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据。
def clip(text, max_len:'int > 0'=80) -> str:
"""在max_len前面或后面的第一个空格处阶段字符串的函数"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None:
end = len(text)
return text[:end].rstrip()
- 为参数添加注解:如果参数有默认值,注解放在参数名和=号之间。
- 注解返回值,在
)
和函数声明末尾的:
之间添加- >
和一个表达式 - 注解不会做任何处理,只是存储在函数的
__annotations__
属性中
5.10 支持函数式编程的包
operator模块
在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用 sum 函数,但是求积则没有这样的函数。 我们可以使用 reduce 函数:
在 Python 2 中,reduce 是内置函数,但是在 Python 3 中放到 functools 模块里了。这个函数最常用于求和,自 2003 年发布的 Python 2.3 开始,最好使用内置的 sum 函数。在可读性和性能方面,这是一项重大改善:
同理我们可以利用operate模块中mul来进行求阶乘的计算,
from functools import reduce
from operator import mul
def fact1(n):
return reduce(lambda a, b: a*b, range(1, n + 1))
def fcat(n):
return reduce(mul, range(1, n + 1))
除此之外,operator 模块中还有一类函数,能替代从序列中取出元素或读取对象 属性的 lambda 表达式:因此,itemgetter 和 attrgetter 其实会自行构建函数。
metro_data = [('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),]
from operator import itemgetter
# 按照列表元素的索引为1的位置进行排序
for city in sorted(metro_data, key=itemgetter(1)):
print(city)
"""OUT
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
"""
# 这个方法类似于
for city in sorted(metro_data, key=lambda fields: fields[1]):
print(city)
"""OUT
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
"""
如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成 的元组:
cc_name = itemgetter(1, 0)
for city in metro_data:
print(cc_name(city))
"""OUT
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')
"""
attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组。此外,如果参数名中包含 .(点号),attrgetter 会深 入嵌套对象,获取指定的属性。这些行为可以见下面的代码:
from collections import namedtuple
LatLong = namedtuple('LatLong', 'lat long')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')
metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long))
for name, cc, pop, (lat, long) in metro_data]
"""metro_areas:
[Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667)),
Metropolis(name='Delhi NCR', cc='IN', pop=21.935, coord=LatLong(lat=28.613889, long=77.208889)),
Metropolis(name='Mexico City', cc='MX', pop=20.142, coord=LatLong(lat=19.433333, long=-99.133333)),
Metropolis(name='New York-Newark', cc='US', pop=20.104, coord=LatLong(lat=40.808611, long=-74.020386)),
Metropolis(name='Sao Paulo', cc='BR', pop=19.649, coord=LatLong(lat=-23.547778, long=-46.635833))]
"""
from operator import attrgetter
name_lat = attrgetter('name', 'coord.lat')
# 首先将数组按照coord.lat进行排序
for city in sorted(metro_areas, key=attrgetter('coord.lat')):
# 执行输出对象
print(name_lat(city))
"""OUT:
('Sao Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
"""
我们最后介绍一下 methodcaller,它的作用与 attrgetter 和 itemgetter 类似,它会自行创建函 数。methodcaller 创建的函数会在对象上调用参数指定的方法,如下所示:
使用functools.partial冻结参数
functools 模块提供了一系列高阶函数,其中最为人熟知的或许是 reduce,余下的函数中,最有用的是 partial 及其变体partialmethod。
functools.partial 基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个参数的函数改编成需要回调的 API,这样参数更少。如下面,需要计算一个值乘上3的结果
5.11 本章小结
- 在python中,函数是一等对象,可以把函数赋值给变量、传给其他函数、存储在数据结构中,以及访问函数的属性;
- 高阶函数表示可以将函数作为参数的函数;
- Python中定义了7 种可调用对象,可以通过callable()函数来判断函数是否可以被调用;
- Python中的函数即是对象,因此函数拥有诸多的属性,用户可以选择性的进行调用;
- Python函数中的参数可以分为关键字参数和定位参数,关键字参数必须指定键名,一对一的理解,定位参数有*args,**keyargs两种,前者接收数组,后者接收字典;
- 介绍了 operator 模块中的一些函数,以及 functools.partial 函数,了解了函数式编程的一些方法。
第六章 使用一等函数实现设计模式
6.1 案例分析:重构“策略”模式
电商领域有个功能明显可以使用“策略”模式,即根据客户的属性或订单 中的商品计算折扣,假如一个网店制定了下述折扣规则:
- 有 1000 或以上积分的顾客,每个订单享 5% 折扣;
- 同一订单中,单个商品的数量达到 20 个或以上,享 10% 折扣;
- 订单中的不同商品达到 10 个或以上,享 7% 折扣。
“策略”模式的 UML 类图见图 6-1:
from abc import ABC, abstractmethod
from collections import namedtuple
Customer = namedtuple('Customer', 'name fidelity')
# 单类型产品订单
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
# 上下文,顾客一次提交的所有订单
class Order: # the Context
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = list(cart)
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# 促销活动(策略)顶层抽象基类
class Promotion(ABC): # the Strategy: an Abstract Base Class
@abstractmethod
def discount(self, order):
"""Return discount as a positive dollar amount"""
# 策略一
class FidelityPromo(Promotion): # first Concrete Strategy
"""5% discount for customers with 1000 or more fidelity points"""
def discount(self, order):
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
# 策略二
class BulkItemPromo(Promotion): # second Concrete Strategy
"""10% discount for each LineItem with 20 or more units"""
def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
# 策略三
class LargeOrderPromo(Promotion): # third Concrete Strategy
"""7% discount for orders with 10 or more distinct items"""
def discount(self, order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
说明:
在python中,声明抽象基类最简单的方式是子类化abc.ABC
利用上述代码进行测试如下:
在上述案例的基础上,我们利用函数实现策略模式,重构如下
from collections import namedtuple
Customer = namedtuple('Customer', 'name fidelity')
class LineItem:
def __init__(self, product, quantity, price):
self.product = product
self.quantity = quantity
self.price = price
def total(self):
return self.price * self.quantity
class Order:
def __init__(self, customer, cart, promotion=None):
self.customer = customer
self.cart = cart
self.promotion = promotion
def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self.cart)
return self.__total
def due(self):
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
fmt = '<Order total:{:.2f}due:{:.2f}>'
return fmt.format(self.total(), self.due())
def fidelity_promo(order):
"""为积分为1000或以上的顾客提供5%折扣"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
"""单个商品为20个或以上时提供10%折扣"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
def large_order_promo(order):
"""订单中的不同商品达到10个或以上时提供7%折扣"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
然后我们定义一个最好的策略方法best_promo,用于筛选出折扣力度最大的策略
上面的best_promo列表是我们自定义的,如果一旦新增新的策略方法,必须手动修改列表,如果可以利用Pyhton中的模块板房globals()自动加载策略:
6.2 “命令”模式
“命令”模式的目的是解耦调用操作的对象(调用者)和提供实现的对象 (接收者)。调用者是图形应用程序中的菜单项,而接收者是被编辑的文档或应 用程序自身,UML图如下:
这个模式的做法是,在二者之间放一个 Command 对象,让它实现只有 一个方法(execute)的接口,调用接收者中的方法执行所需的操作。 这样,调用者无需了解接收者的接口,而且不同的接收者可以适应不同的 Command 子类。调用者有一个具体的命令,通过调用 execute 方法 执行。
我们可以不为调用者提供一个 Command 实例,而是给它一个函数。此时,调用者不用调用 command.execute(),直接调用 command() 即 可。MacroCommand 可以实现成定义了__call__
方法的类。这样,MacroCommand 的实例就是可调用对象,各自维护着一个函数列表,供以后调用,代码如下所示:
class MacroCommand:
"""一个执行一组命令的命令"""
def __init__(self, commands):
self.commands = list(commands)
def __call__(self):
for command in self.commands:
command()
第七章 函数装饰器和闭包
7.1 装饰器基础知识
装饰器是可调用的对象,他的参数是另一个函数(被装饰的函数);装饰器会处理被装饰的函数,让后将其返回,或者将其替换成另一个函数或可调用的对象。
我们来看一种装饰器将被修饰函数替换位另一个函数的例子:
def deco(func):
def inner():
print('running inner()')
return inner
@deco
def target():
print('running target()')
target() # running inner()
target # <function __main__.deco.<locals>.inner>
如果稍微修改一样deco()函数,结果就会变得不一样:
def deco(func):
def inner():
print('running inner()')
return func
@deco
def target():
print('running target()')
target() # running target()
target # <function __main__.target>
7.2 Python何时执行装饰器
直接用一段代码展示:
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main')
print('registry ->', registry)
f1()
f2()
f3()
if __name__ == '__main__':
main()
"""OUT:
running register(<function f1 at 0x000001ACE91A6558>)
running register(<function f2 at 0x000001ACE91A6B88>)
running main
registry -> [<function f1 at 0x000001ACE91A6558>, <function f2 at 0x000001ACE91A6B88>]
running f1()
running f2()
running f3()
"""
- 函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行;
- 在实际使用中,装饰器通常在一个单独的模块中定义,然后应用到其它模块中的函数上;
7.3 使用装饰器改进“策略模式”
使用注册装饰器可以改进 6.1 节中的电商促销折扣示例
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func
@promotion
def fidelity(order):
"""为积分为1000或以上的顾客提供5%折扣"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item(order):
"""单个商品为20个或以上时提供10%折扣"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
@promotion
def large_order(order):
"""订单中的不同商品达到10个或以上时提供7%折扣"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
def best_promo(order):
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)
可以看到,使用装饰器会非常简单的将promos列表填满,并且,他含有下面几个方面的优点:
- 促销策略函数无需使用特殊的名称(即不用以_promo结尾)
- 装饰器突出了被装饰的函数的作用,还便于临时禁用 某个促销策略:只需把装饰器注释掉。
- 促销折扣策略可以在其他模块中定义,在系统中的任何地方都行, 只要使用 @promotion 装饰即可。
7.4 变量作用域规则
案例一:定义一个函数,分别打印局部变量和全局变量
b = 3
def f1(a):
print(a)
print(b)
f1(2) # 2 3
案例二:输出全局变量b之后,再给b赋值试试
b = 3
def f1(a):
print(a)
print(b)
b = 4
f1(2)
"""OUT:
2
UnboundLocalError: local variable 'b' referenced before assignment
"""
可以看出a输出了,但是输出b的时候报错了,因为在方法中出现b=4的指令,Python自动将变量b视为局部变量,在进行print(b)的时候,b的初值还没有被定义。因此这里就会报错;
如果想b任然被作为全局变量,则需要在变量前加上global进行修饰,如案例三所示
b = 3
def f1(a):
global b
print(a)
print(b)
b = 4
f1(2) # 2 3
7.5 闭包
闭包是指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。以案例来进行说明:
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值,常规写法如下:
class Average():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)
avg = Average()
avg(10) # 10.0
avg(11) # 10.5
avg(12) # 11.0
使用函数式实现方式如下:
def make_average():
series = []
def average(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return average
avg = make_average()
avg(10) # 10.0
avg(11) # 10.5
avg(12) # 11.0
从上面的代码中可以看出,make_average()方面并不需要参数,没有明确给出,但是在调用时,我们传入了值进去,而这个值是给内部函数average()用的,那么这个内部函数就称之为闭包
检查闭包中的元素如下 :
avg.__code__.co_varnames
# ('new_value', 'total')
avg.__code__.co_freevars
# ('series',)
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定, 这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
7.6 nonlocal声明
我们使用一种更为简单的方式实现前面avg()函数的功能:
def make_average():
count = 0
total = 0
def average(new_value):
count += 1
total += new_value
return total / count
return average
avg = make_average()
avg(10) # 报错
"""OUT:
Traceback (most recent call last): ...
UnboundLocalError: local variable 'count' referenced before assignment
"""
报错原因:当count是数字等不可变类型的时候,会将count作为局部变量,但是在average()函数中,并没有给局部变量附初始值。因此,这里产生了错误。
针对这种问题,Python引入了nonlocal声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。具体看下面的代码:
def make_average():
count = 0
total = 0
def average(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return average
avg = make_average()
avg(10) # output 10.0
7.7 实现一个简单的装饰器
实现一个装饰器,用来计算被修饰函数运算时间的功能
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ','.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
"""OUT:
**************************************** Calling snooze(.123)
[0.12799090s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000090s] factorial(1) -> 1
[0.00001660s] factorial(2) -> 2
[0.00002720s] factorial(3) -> 6
[0.00003670s] factorial(4) -> 24
[0.00004640s] factorial(5) -> 120
[0.00005730s] factorial(6) -> 720
6! = 720
"""
上述代码相当于将snooze和factorial两个函数放到了装饰器函数clocked()中进行了执行,所有检查被装置函数的函数名时可以发现,函数名均被修改成了clocked()
print(factorial.__name__) # clocked
print(snooze.__name__) # clocked
上述实现的clock装饰器有几个缺点:
- 不支持关键字参数
- 遮盖了被装置函数的
__name
和__doc__
属性
针对这样的情况,Python可以使用functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(",".join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
print(factorial.__name__)
print(snooze.__name__)
"""OUT:
**************************************** Calling snooze(.123)
[0.12930040s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000370s] factorial(1) -> 1
[0.00008910s] factorial(2) -> 2
[0.00013480s] factorial(3) -> 6
[0.00019110s] factorial(4) -> 24
[0.00025280s] factorial(5) -> 120
[0.00029870s] factorial(6) -> 720
6! = 720
factorial
snooze
"""
可以看出现在的运行结果和之前的有一定的区别。
7.8 标准库中的装饰器
Python 内置了三个用于装饰方法的函数:property、classmethod 和 staticmethod。另一个常见的装饰器是 functools.wraps,它的作用是协助构建行为良好的装饰器。前面已经给出了示例。这里介绍functools中另外两个装饰器,分别是 lru_cache 和全新的 singledispatch。
- functools.lru_cache 是非常实用的装饰器,它实现了备忘 (memoization)功能。LRU缓存算法实现
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__ == '__main__':
print(fibonacci(6))
"""OUT:
[0.00000050s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00002750s] fibonacci(2) -> 1
[0.00000030s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000020s] fibonacci(1) -> 1
[0.00001250s] fibonacci(2) -> 1
[0.00002410s] fibonacci(3) -> 2
[0.00006340s] fibonacci(4) -> 3
[0.00000010s] fibonacci(1) -> 1
...
...
8
"""
可以发现上述程序中,fibonacci(1) 调用了 8 次,fibonacci(2) 调用了 5 次,但是,如果增加两行代码,使用 lru_cache,性能会显著改善
@functools.lru_cache() # 注意这里必须加上(),因为还可以设置其它参数
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__ == '__main__':
print(fibonacci(6))
"""OUT:
[0.00000040s] fibonacci(0) -> 0
[0.00000050s] fibonacci(1) -> 1
[0.00003590s] fibonacci(2) -> 1
[0.00000060s] fibonacci(3) -> 2
[0.00004960s] fibonacci(4) -> 3
[0.00000040s] fibonacci(5) -> 5
[0.00006340s] fibonacci(6) -> 8
8
"""
这里应该可以看到明显的区别,所有的方法只运行了一次
- 使用functools.singledispatch装饰器
定义一个制作html标签的方法
import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
我们进行一系列的测试如下:
现在如果想根据传入的obj对象的类型来进行不同方式的解析并生成标签,该怎么做尼?这个使用就用到了functools.singledispatch装饰器
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str)
def _(text):
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
执行结果如下图所示: htmlize()根据不同的参数进行区别处理
7.9 叠放装饰器
将两个或以上的装饰器加到一个函数上,称之为叠放装饰器,把@d1 和 @d2 两个装饰器按顺序应用到 f 函数上,作用相当于 f = d1(d2(f))。
@d1
@d2
def f():
print('f')
7.10 参数化装饰器
给装饰器加上一个参数,决定装饰器的工作模式
registry = set()
def register(active=True):
def decorate(func):
print('running register(active=%s)->decorate(%s)' % (active, func))
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate
@register(active=False)
def f1():
print('running f1()')
@register()
def f2():
print('running f2()')
def f3():
print('running f3()')
if __name__ == '__main__':
f1()
f2()
f3()
print(registry)
"""OUT:
running register(active=False)->decorate(<function f1 at 0x000001ED09CE6B88>)
running register(active=True)->decorate(<function f2 at 0x000001ED09CE6C18>)
running f1()
running f2()
running f3()
{<function f2 at 0x000001ED09CE6C18>}
"""
第8章 对象引用、可变性和垃圾回收
8.1 变量不是盒子
变量a和b引用同一个列表,而不是列表的副本,因此对a进行改变的同时,b也发生了改变。
a = [1, 2, 3]
b = a
a.append(4)
b
"""OUT: [1, 2, 3, 4]"""
8.2 标识、相等性和别名
- 别名:见下面的例子,lewis 是 charles 的别名,一旦charles发生了改变,那么lewis也一定会发生改变
-
相等性:我们定义一个和charles完全一样的变量alex,进行测试看看情况如何?
上面的例子因为dict类的
__eq__
方法,使得两个变量==是为True,但是这两个变量所指向的是两个完全不一样的变量。
在Python中,== 运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识;通常,我们关注的是值,而不是标识,因此 Python代码中 == 出现的频率比is 高。
然而,在变量和单例值之间比较时,应该使用 is。目前,最常使用 is 检查变量绑定的值是不是 None。下面是推荐的写法:
x is None
x is not None
元组与多数Python集合(列表、字典、集,等等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
8.3 默认做浅复制
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。如下:可以发现复制的列表与原来的列表不一样,仅仅是重新制作了一份一模一样的对象而已。
再来看一个例子:
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)
l1.append(100)
l1[1].remove(55)
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22]
l2[2] += (10, 11)
print('l1:', l1)
print('l2:', l2)
"""
l1: [3, [66, 44], (7, 8, 9), 100]
l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
"""
分析一下上面的例子:
- 首先利用list(l1)做了一个浅层复制给l2
- 然后向l1中append一个值100,因为list可变,所以这个值只会影响l1,不影响l2
- 然后将l1[1]中移除参数55,因为两个列表的第二个元素均指向列表[66,55,44],所以移去55时,另一个列表也会发生改变
- l2[1] += [33, 22]:同样对列表进行操作,列表可变,所以两个列表l1和l2同时发生改变
- l2[2] += (10, 11):因为元素不可变,所以实行此语句时,会拼接成一个新的元素加给l2,l1并没有发生改变。
**copy 模块提供的 deepcopy 和 copy 函数能为任意对象做 深复制和浅复制。**用一个案例来看看两者的区别。
class Bus:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
import copy
bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3) # 可以发现三个对象均不一样
>>> (2034541420936, 2034541421256, 2034541421384)
bus1.drop('Bill')
>>> bus2.passengers
>>> ['Alice', 'Claire', 'David'] # bus2发生了变化,浅复制的弊端
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
>>> (2034541421064, 2034541421064, 2034540547592) # 浅复制列表没有复制,只是复制了外层对象
>>> bus3.passengers
>>> ['Alice', 'Bill', 'Claire', 'David']
8.4 函数的参数作为引用时
Python支持的参数传递模式是共享传参,共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。看一下下面的例子:
def f(a, b):
a += b
return a
x = 1
y = 2
f(x, y) # 3
x, y # (1, 2) 这里因为int类型不可变,所以x,y均没有发生改变
a = [1, 2]
b = [3, 4]
f(a, b) # [1, 2, 3, 4]
a, b # ([1, 2, 3, 4], [3, 4]) 这里因为list是可变类型,所以a发生了改变
t = (10, 20)
u = (30, 40)
f(t, u) # (10, 20, 30, 40)
t, u # ((10, 20), (30, 40)) tuple为不可变类型,t,u均为发生改变
Python定义函数时,应该避免使用可变的对象作为参数的默认值,通常只用None作为接受可变值的参数的默认值:
class HauntedBus:
"""备受幽灵乘客折磨的校车"""
def __init__(self, passengers=[]):
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
如果定义的函数接受可变参数,应该考虑函数是否会修改传入的可变参数本体,可以看下面一个例子,定义一个校车,将篮球队的人放到校车上,当人下车后,相应的也从篮球队的列表中脱离了,这个现象一般情况下是不希望产生的。
class TwilightBus:
"""让乘客销声匿迹的校车"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = TwilightBus(basketball_team)
bus.drop('Tina')
bus.drop('Pat')
basketball_team
"""OUT:
['Sue', 'Maya', 'Diana']
"""
**改造方法:**在init时,不要将直接赋值passengers,用list(passengers)生成一个新的对象进行赋值。
class TwilightBus:
"""让乘客销声匿迹的校车"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = TwilightBus(basketball_team)
bus.drop('Tina')
bus.drop('Pat')
basketball_team
"""OUT:
['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
"""
除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。如果不确定,那就创建副本。这样客户会少些麻烦
8.5 del和垃圾回收
del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。下面看一个例子
8.6 弱引用
弱引用不会增加对象的引用数量。引用的目标对象称为所指对象 (referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。下面用一个实例展示如何使用 weakref.ref 实例获取所指对象。如果对象存在,调用弱引用可以获取对象;否则返回 None
8.6.1 WeakValueDictionary简介
WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary 中删除。因此,WeakValueDictionary 经常用于缓存。我们来看一段代码:
import weakref
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)' % self.kine
stock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')]
# 使用奶酪名字对奶酪对象建立弱引用
for cheese in catalog:
stock[cheese.kind] = cheese
sorted(stock.keys())
'''['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']'''
del catalog
sorted(stock.keys())
'''['Parmesan']'''
del cheese
sorted(stock.keys())
'''[]'''
从上面的程序可以发现:
- 建立了弱引用之后,删除catalog引用之后,虚引用还剩一个Parmesan
- 产生这种现象的原因是,可能临时变量引用了对象,在上述代码中,可能最后的cheese变量引用了最后一个奶酪,所以最后剩下一个虚引用
- 然后将cheese释放之后,相应的虚引用全部解开
8.6.2 弱引用的局限
基本 的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题;set 实例可以作为所指对象,但是,int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行。这些局限基本上是 CPython 的实现细节,在其他 Python 解释器中情况可能不一样。
8.7 Python对不可变类型施加的把戏
pass