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))'''
OUTreturn 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()
# 0bingo()  # 因为重写了__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'] = clsif attrs:attr_str = ''.join(' %s="%s"' % (attr, value)for attr, valuein 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 = Noneif len(text) > max_len:space_before = text.rfind(' ', 0, max_len)if space_before >= 0:end = space_beforeelse:space_after = text.rfind(' ', max_len)if space_after >= 0:end = space_afterif 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 namedtupleCustomer = namedtuple('Customer', 'name fidelity')# 单类型产品订单
class LineItem:def __init__(self, product, quantity, price):self.product = productself.quantity = quantityself.price = pricedef total(self):return self.price * self.quantity# 上下文,顾客一次提交的所有订单
class Order:  # the Contextdef __init__(self, customer, cart, promotion=None):self.customer = customerself.cart = list(cart)self.promotion = promotiondef total(self):if not hasattr(self, '__total'):self.__total = sum(item.total() for item in self.cart)return self.__totaldef due(self):if self.promotion is None:discount = 0else:discount = self.promotion.discount(self)return self.total() - discountdef __repr__(self):fmt = '<Order total: {:.2f} due: {:.2f}>'return fmt.format(self.total(), self.due())# 促销活动(策略)顶层抽象基类
class Promotion(ABC):  # the Strategy: an Abstract Base Class@abstractmethoddef 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 = 0for item in order.cart:if item.quantity >= 20:discount += item.total() * .1return 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() * .07return 0

说明:

在python中,声明抽象基类最简单的方式是子类化abc.ABC

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

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

from collections import namedtupleCustomer = namedtuple('Customer', 'name fidelity')class LineItem:def __init__(self, product, quantity, price):self.product = productself.quantity = quantityself.price = pricedef total(self):return self.price * self.quantityclass Order:def __init__(self, customer, cart, promotion=None):self.customer = customerself.cart = cartself.promotion = promotiondef total(self):if not hasattr(self, '__total'):self.__total = sum(item.total() for item in self.cart)return self.__totaldef due(self):if self.promotion is None:discount = 0else:discount = self.promotion(self)return self.total() - discountdef __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 0def bulk_item_promo(order):"""单个商品为20个或以上时提供10%折扣"""discount = 0for item in order.cart:if item.quantity >= 20:discount += item.total() * .1return discountdef large_order_promo(order):"""订单中的不同商品达到10个或以上时提供7%折扣"""distinct_items = {item.product for item in order.cart}if len(distinct_items) >= 10:return order.total() * .07return 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 = 0for item in order.cart:if item.quantity >= 20:discount += item.total() * .1return 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() * .07return 0def 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 = 4f1(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 bprint(a)print(b)b = 4f1(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.0avg(11)  # 10.5avg(12)  # 11.0

使用函数式实现方式如下:

def make_average():series = []def average(new_value):series.append(new_value)total = sum(series)return total / len(series)return averageavg = 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 = 0total = 0def average(new_value):count += 1total += new_valuereturn total / countreturn averageavg = 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 = 0total = 0def average(new_value):nonlocal count, totalcount += 1total += new_valuereturn total / countreturn averageavg = make_average()
avg(10)  # output 10.0

7.7 实现一个简单的装饰器

实现一个装饰器,用来计算被修饰函数运算时间的功能

import timedef clock(func):def clocked(*args):t0 = time.perf_counter()result = func(*args)elapsed = time.perf_counter() - t0name = func.__name__arg_str = ','.join(repr(arg) for arg in args)print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))return resultreturn 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 functoolsdef clock(func):@functools.wraps(func)def clocked(*args, **kwargs):t0 = time.perf_counter()result = func(*args, **kwargs)elapsed = time.perf_counter() - t0name = 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 resultreturn 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 nreturn 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 nreturn 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 htmldef 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 funcreturn 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 += breturn ax = 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 = passengersdef 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 = passengersdef 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 weakrefclass Cheese:def __init__(self, kind):self.kind = kinddef __repr__(self):return 'Cheese(%r)' % self.kinestock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')]# 使用奶酪名字对奶酪对象建立弱引用
for cheese in catalog:stock[cheese.kind] = cheesesorted(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

流畅的Python阅读笔记(二)相关推荐

  1. 大型网站技术架构:核心原理与案例分析阅读笔记二

    大型网站技术架构:核心原理与案例分析阅读笔记二 网站架构设计时可能会存在误区,其实不必一味追随大公司的解决方案,也不必为了技术而技术,要根据本公司的实际情况,制定适合本公司发展的网站架构设计,否则会变 ...

  2. 《挑战程序设计竞赛》阅读笔记二 之 ALDS1_2_C Stable Sort

    <挑战程序设计竞赛>阅读笔记二 之 ALDS1_2_C Stable Sort 第三章 Sort I ALDS1_2_C Stable Sort 这道题目,就是为了说明 冒泡排序是稳定排序 ...

  3. python学习笔记(二) 基本运算

    python学习笔记(二) 基本运算 1. 条件运算 基本语法 if condition1: do somethings1elif condition2: do somethings2else: do ...

  4. 《逻辑思维简易入门》(第2版) 阅读笔记二

    <逻辑思维简易入门>(第2版) 阅读笔记二 本周阅读的是<逻辑思维简易入门>的第三章,也就是说,本书的第一部分就已经读完了. 第三章.信念的优点 信念和负信念是人们在接受一个事 ...

  5. 【流畅的Python学习笔记】2023.4.21

    此栏目记录我学习<流畅的Python>一书的学习笔记,这是一个自用笔记,所以写的比较随意 特殊方法(魔术方法) 不管在哪种框架下写程序,都会花费大量时间去实现那些会被框架本身调用的方法,P ...

  6. 流畅的Python读书笔记

    流畅的Python 说明 我发现流畅的python更适合我现在看,因为它写的很详细.而effective python知识点不是很连贯,我先看完这本书,再去过一遍effective python吧! ...

  7. 《Evaluate the Malignancy of Pulmonary Nodules Using the 3D Deep Leaky Noisy-or Network》阅读笔记(二)

    <Evaluate the Malignancy of Pulmonary Nodules Using the 3D Deep Leaky Noisy-or Network>阅读笔记–翻译 ...

  8. 【流畅的Python学习笔记】2023.4.29

    此栏目记录我学习<流畅的Python>一书的学习笔记,这是一个自用笔记,所以写的比较随意,随缘更新 泛映射类型 collections.abc 模块中有 Mapping 和 Mutable ...

  9. 【流畅的Python学习笔记】2023.4.22

    此栏目记录我学习<流畅的Python>一书的学习笔记,这是一个自用笔记,所以写的比较随意 元组 元组其实是对数据的记录:元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置.简 ...

最新文章

  1. Django 第三方引用富文本编辑器6.1
  2. Apache Doris在美团外卖数仓中的应用实践
  3. 从命令行列出所有环境变量?
  4. centos安装python3小白_在Linux CentOS7 下安装 python3
  5. 前端学习(1501):一次帮别人解决问题的案例
  6. [Leedcode][JAVA][第820题][字典树][Set]
  7. 机器学习笔记2 – sklearn之iris数据集
  8. python开根号函数图像_使用matplotlib / python的平方根刻度
  9. Redis,唯快不破!
  10. C++11 pair的使用
  11. html圆形圆心坐标,圆心坐标公式
  12. box-sizing
  13. 任天堂switch底座带网口全新方案分享
  14. ens33网卡出问题<BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
  15. 【洛谷】P2689 东南西北(dfs|贪心)
  16. 如何编辑eps格式的图片
  17. JS 使用find 查找数组中某个元素
  18. JSP详细DAO设计模式及应用(!)
  19. Unity3d 周分享(9期 2018.12.31)
  20. python笔记---(实验二)

热门文章

  1. 量化交易策略 - 优化均仓策略
  2. 解决安装rpm包依赖关系的烦恼 - yum工具介绍及本地源配置方法
  3. 深度学习之CNN卷积神经网络详解以及猫狗识别实战
  4. 信息系统项目管理师案例分析万金油
  5. 【附源码】计算机毕业设计java疫情期间优化旅游平台设计与实现
  6. PERCENT(SQL)
  7. Qt geometry
  8. 像李云迪那样爱 IT界那些“情”
  9. 灰度变换-分段线性函数
  10. 什么样的男生值得交往一生