流畅的Python阅读笔记(二)

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中进行运算

image-20210529160408912

5.2 高阶函数

高阶函数就是接受函数作为参数, 或者把函数作为返回结果的函数. 如 map, filter , reduce 等.

比如调用 sorted 时, 将 len 作为参数传递:

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
# ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

比如,根据反向拼写给一个单词列表排序

image-20210529160753356

在Python3中,由于引入了列表推导和生成器表达式,高阶函数map、filter它们变得没那么重要了,因为列表推导更容易理解和简单。比如,计算阶乘列表:map和filter与列表推导比较

image-20210529161049988

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() 函数来检查是否为可调用对象:

image-20210529161835600

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集合来找出常规对象没有而函数有的属性

image-20210529162809824

对其中较为典型的属性进行简单说明:

  • __annotations__:参数和返回值得注解
  • __code__:编译成字节码的函数元数据和函数定义体
  • __defaults__:形式参数的默认值
  • __globals__:函数所在模块中的全局变量
  • __kwdefaults__:仅限关键字形式参数的默认值
  • __name__:函数名称

image-20210529163520148

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__属性中

image-20210529164841735

5.10 支持函数式编程的包

operator模块

在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用 sum 函数,但是求积则没有这样的函数。 我们可以使用 reduce 函数:

在 Python 2 中,reduce 是内置函数,但是在 Python 3 中放到 functools 模块里了。这个函数最常用于求和,自 2003 年发布的 Python 2.3 开始,最好使用内置的 sum 函数。在可读性和性能方面,这是一项重大改善:

image-20210529170013758

同理我们可以利用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))

image-20210529170302846

除此之外,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 创建的函数会在对象上调用参数指定的方法,如下所示:

image-20210529172440162

使用functools.partial冻结参数

functools 模块提供了一系列高阶函数,其中最为人熟知的或许是 reduce,余下的函数中,最有用的是 partial 及其变体partialmethod。

functools.partial 基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个参数的函数改编成需要回调的 API,这样参数更少。如下面,需要计算一个值乘上3的结果

image-20210529173018398

5.11 本章小结

  • 在python中,函数是一等对象,可以把函数赋值给变量、传给其他函数、存储在数据结构中,以及访问函数的属性;
  • 高阶函数表示可以将函数作为参数的函数;
  • Python中定义了7 种可调用对象,可以通过callable()函数来判断函数是否可以被调用;
  • Python中的函数即是对象,因此函数拥有诸多的属性,用户可以选择性的进行调用;
  • Python函数中的参数可以分为关键字参数和定位参数,关键字参数必须指定键名,一对一的理解,定位参数有*args,**keyargs两种,前者接收数组,后者接收字典;
  • 介绍了 operator 模块中的一些函数,以及 functools.partial 函数,了解了函数式编程的一些方法。

第六章 使用一等函数实现设计模式

6.1 案例分析:重构“策略”模式

电商领域有个功能明显可以使用“策略”模式,即根据客户的属性或订单 中的商品计算折扣,假如一个网店制定了下述折扣规则:

  • 有 1000 或以上积分的顾客,每个订单享 5% 折扣;
  • 同一订单中,单个商品的数量达到 20 个或以上,享 10% 折扣;
  • 订单中的不同商品达到 10 个或以上,享 7% 折扣。

“策略”模式的 UML 类图见图 6-1:

image-20210530150250887

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

利用上述代码进行测试如下:

image-20210530152704440

在上述案例的基础上,我们利用函数实现策略模式,重构如下

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

image-20210530155206161

然后我们定义一个最好的策略方法best_promo,用于筛选出折扣力度最大的策略

image-20210530155821004

上面的best_promo列表是我们自定义的,如果一旦新增新的策略方法,必须手动修改列表,如果可以利用Pyhton中的模块板房globals()自动加载策略:

image-20210530160919158

6.2 “命令”模式

“命令”模式的目的是解耦调用操作的对象(调用者)和提供实现的对象 (接收者)。调用者是图形应用程序中的菜单项,而接收者是被编辑的文档或应 用程序自身,UML图如下:

image-20210530161518917

这个模式的做法是,在二者之间放一个 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()用的,那么这个内部函数就称之为闭包

image-20210530181454925

检查闭包中的元素如下 :

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)

我们进行一系列的测试如下:

image-20210530201016308

现在如果想根据传入的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()根据不同的参数进行区别处理

image-20210530202056916

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 变量不是盒子

image-20210531213149637

变量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也一定会发生改变

image-20210531214032996

  • 相等性:我们定义一个和charles完全一样的变量alex,进行测试看看情况如何?

    image-20210531214529150

    上面的例子因为dict类的__eq__方法,使得两个变量==是为True,但是这两个变量所指向的是两个完全不一样的变量。

在Python中,== 运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识;通常,我们关注的是值,而不是标识,因此 Python代码中 == 出现的频率比is 高。

然而,在变量和单例值之间比较时,应该使用 is。目前,最常使用 is 检查变量绑定的值是不是 None。下面是推荐的写法:

x is None
x is not None

元组与多数Python集合(列表、字典、集,等等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。

image-20210531215938153

8.3 默认做浅复制

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。如下:可以发现复制的列表与原来的列表不一样,仅仅是重新制作了一份一模一样的对象而已。

image-20210531220338067

再来看一个例子:

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)]
"""

image-20210531221242741

分析一下上面的例子:

  • 首先利用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)

image-20210601195617058

如果定义的函数接受可变参数,应该考虑函数是否会修改传入的可变参数本体,可以看下面一个例子,定义一个校车,将篮球队的人放到校车上,当人下车后,相应的也从篮球队的列表中脱离了,这个现象一般情况下是不希望产生的。

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 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。下面看一个例子

image-20210601202142835

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值