《流畅的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-----------------------------------------------------

posted @ 2019-02-28 08:48  Alpha205  阅读(72)  评论(0编辑  收藏  举报