《流畅的python》之 设计模式, 装饰器
如果合理利用作为一等对象的函数,某些设计模式可以简化,例如“策略模式”
假设商店制定了如下的销售规则:
1. 1000以上积分的用户,每单享受5%折扣
2. 同一订单中,单个商品的数量达到20以上,享受10%折扣
3. 订单中不同商品达到10件以上,7%折扣
4.一个订单一次只能享受一次折扣
from abc import abstractmethod # 装饰器
from abc import 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.quantity * self.price
class Order: # 上下文 订单类
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) # 这里discount的self参数就是order本身
return self.total() - discount
def __repr__(self): # 控制输出的格式
fmt = '<order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# def print_info(self): # 控制输出的格式
# fmt = '<order total: {:.2f} due: {:.2f}>'
# print(fmt.format(self.total(), self.due()))
class Promotion(ABC): # 策略,抽象基类
@abstractmethod
def discount(self, order): # 装饰器 抽象方法
"""返回折扣金额"""
class FidelityPromo(Promotion):
"""
策略 1
为积分超过1000的用户打折
"""
def discount(self, order):
return order.total()*0.05 if order.customer.fidelity >= 1000 else 0
class BulkItemPromo(Promotion):
"""
策略 2
为单个商品超过20件的用户打折
"""
def discount(self, order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * 0.01
return discount
class LargeOrderPromo(Promotion):
"""
策略 3
为不同商品超过10件以上的用户打折
"""
def discount(self, order):
discount_items = {item.product for item in order.cart}
if len(discount_items) >= 10:
return order.total() * 0.07
return 0
if __name__ == "__main__":
joe = Customer("Joe D", 0) # 无积分顾客
ann = Customer("Ann jane", 1200) # 有积分顾客
cart = [LineItem("banana", 18, 0.5), # 购物车信息
LineItem("apple", 6, 1.5),
LineItem("melon", 8, 1.2)]
order1 = Order(ann, cart, FidelityPromo())
order2 = Order(joe, cart, BulkItemPromo())
print(order1)
print(order2)
# order1.print_info()
# print(order1)
对上述代码的改进:
直接通过定义函数实现不同的策略:
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.quantity * self.price
class Order: # 上下文 订单类
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(self) # 此时折扣直接使用promotion计算,self此时是order
return self.total() - discount
def __repr__(self): # 控制输出的格式
fmt = '<order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# def print_info(self): # 控制输出的格式
# fmt = '<order total: {:.2f} due: {:.2f}>'
# print(fmt.format(self.total(), self.due()))
# 定义各个策略为函数
def fidelity_promo(order):
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total()*0.1
return discount
def large_order_promo(order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
# 最佳折扣方式
def best_promo(order):
promos = [fidelity_promo, bulk_item_promo, large_order_promo] # 将函数作为对象,构建函数存储数据结构
return max(prom(order) for prom in promos) # 返回最大的折扣
if __name__ == "__main__":
joe = Customer("Joe D", 900) # 无积分顾客
ann = Customer("Ann jane", 1200) # 有积分顾客
cart = [LineItem("banana", 23, 0.5), # 购物车信息
LineItem("apple", 6, 1.5),
LineItem("melon", 8, 1.2)]
order1 = Order(ann, cart, fidelity_promo)
order2 = Order(joe, cart, best_promo) # 在这里使用best_promo()函数得到最大的折扣
print(order1)
print(order2)
这里用到了装饰器:
class Promotion(ABC): # 策略,抽象基类
@abstractmethod
def discount(self, order): # 装饰器 抽象方法
"""返回折扣金额"""
函数装饰器和闭包:
函数装饰器用于在源码中标记函数,以某种方式增强函数的行为:装饰器是可调用的对象,其参数是另一个函数(被装饰的函数),装饰器可能会处理被装饰的函数,将他返回,或者将其替换成另一个函数或者可调用对象。
# 装饰器
@decorate
def target():
print("running target")
# 等价的写法
def target():
print("running target")
target = decorate(target) # 返回的target不一定是原来的target
装饰器通常把函数替换成另一个函数:
def deco(func):
def inner():
print("running inner")
return inner # 返回inner函数对象
@deco # 装饰器
def target():
print("running target")
if __name__ == "__main__":
target() # 调用target实际会运行inner
运行结果:
装饰器的两大特性:
1.能把被装饰的函数替换成其他函数
2.装饰器在加载模块时立即执行(被装饰的函数定义之后):
registry = []
def register(func): # define a decorate
print("running register{}".format(func)) # 输出被装饰的函数
registry.append(func)
return func # decorate必须返回函数
@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()
运行结果:
在import的时候,会执行装饰器,或者在被装饰的函数顶以后,也会执行装饰器
f1(), f2(),f3()只有在main()中明确调用时才会执行
装饰器的用处:
在上面电商折扣的例子中,定义了best_promo()函数,它的作用时返回最大的折扣策略:但是有一个缺陷,如果系统中新加了折扣方案,则需要手动将折扣方案函数的引用添加到折扣方案列表promos中,否则系统会忽略新定义的折扣方案,而且不报错,这就为系统引入了不易察觉的缺陷:】
解决方案:可以通过装饰器将折扣方案的函数添加到promos,这种称为注册装饰器
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.quantity * self.price
class Order: # 上下文 订单类
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(self) # 此时折扣直接使用promotion计算,self此时是order
return self.total() - discount
def __repr__(self): # 控制输出的格式
fmt = '<order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())
# def print_info(self): # 控制输出的格式
# fmt = '<order total: {:.2f} due: {:.2f}>'
# print(fmt.format(self.total(), self.due()))
# 定义一个注册装饰器
promos = []
def promotion(promo_func):
promos.append(promo_func) # 注册新定义的折扣函数
return promo_func
# 定义各个策略为函数
@promotion
def fidelity_promo(order):
return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item_promo(order):
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total()*0.1
return discount
@promotion
def large_order_promo(order):
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0
# 利用了装饰器后,定义完的函数自动添加到promos中,不用再手动添加
# 最佳折扣方式
def best_promo(order):
return max(prom(order) for prom in promos) # 返回最大的折扣
if __name__ == "__main__":
joe = Customer("Joe D", 900) # 无积分顾客
ann = Customer("Ann jane", 1200) # 有积分顾客
cart = [LineItem("banana", 23, 0.5), # 购物车信息
LineItem("apple", 6, 1.5),
LineItem("melon", 8, 1.2)]
order1 = Order(ann, cart, fidelity_promo)
order2 = Order(joe, cart, best_promo) # 在这里使用best_promo()函数得到最大的折扣
print(order1)
print(order2)
闭包
1.变量的作用域:
b = 6
def f1(a):
print(a)
print(b)
b = 9
f1(2)
这段代码报错,虽然b定义在函数的前面,但是b并不是全局变量,因为在函数中为b进行了赋值,所以python在编译函数体的时候判断b为局部变量。
正确的写法如下:使用global声明
b = 6
def f1(a):
global b
print(a)
print(b)
b = 9
f1(2)
print(b)
输出269
2.闭包:
闭包实际指延伸了作用域的函数,其中包含函数定义体中引用,但不在定义体中定义的非全局变量,关键是能访问定义体之外定义的非全局变量。
例如: 定义average()函数,作用是计算不断增加的系列值的均值
1. 通过类实现:
class Average:
def __init__(self):
self.series = []
def __call__(self, new_value): # 可调用模拟
self.series.append(new_value)
return sum(self.series)/len(self.series)
if __name__ == "__main__":
avg = Average()
print(avg(1))
print(avg(2))
print(avg(3))
高阶函数实现:
def make_average(): # 定义高阶函数
series = []
def average(new_value):
series.append(new_value)
return sum(series)/len(series)
return average
if __name__ == "__main__":
avg = make_average()
print(avg(1))
print(avg(2))
print(avg(3))
注: 在make_average()函数中,series是局部变量,在调用avg(n)时,make_average()函数已经返回了,而他的本地作用域不在了。在average()函数中,series称为自由变量,指未在本地作用域中绑定的变量,average的闭包延伸到它的作用域之外所以讲“ 闭包实际指延伸了作用域的函数,其中包含函数定义体中引用,但不在定义体中定义的非全局变量,关键是能访问定义体之外定义的非全局变量。 ”
3.python3中的nonlocal
上面的make_average()的另一种实现方式,只保留sum,和count,而不用每次更新列表的指:
一种错误的写法:
def make_average(): # 定义高阶函数
count = 0
total = 0
def average(new_value):
count += 1
total += new_value
return total / count
return average
if __name__ == "__main__":
avg = make_average()
print(avg(1))
print(avg(2))
print(avg(3))
这里报错:
UnboundLocalError: local variable 'count' referenced before assignment
这里的count,total不再是自由变量了,因为在average()中给他们赋值,所以他们变成了局部变量。当count,total是数字或不可变对象(string, tuple)时,都会出现这种错误,上述series没有报这种错误是因为列表是可变对象。
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
if __name__ == "__main__":
avg = make_average()
print(avg(1))
print(avg(2))
print(avg(3))
闭包,自由变量,可变对象。
--------------------------------------------------end-----------------------------------------------------