Python 元编程

Decorator装饰器

装饰器就是函数,接受函数作为参数并返回新的函数

  • 装饰器不会修改原始函数签名、返回值,但是大部分返回的 新函数不是原始函数,看起来像是函数元信息发生改变
  • 新函数也可能是单纯的修改原函数的元信息、然后返回

装饰器设计

保留函数元信息

装饰器作用在函数上时,原函数的重要元信息会丢失

  • 名字:func.__name__
  • 文档字符串:func.__doc__
  • 注解:
  • 参数签名:func.__annotations__
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
from functools import wraps

def timethis(func):
@wraps(func)
def wrapper(*args, **kargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end - start)
return result
return wrapper

@timethis
def countdown(n):
while n > 0:
n -= 1

def countdown(n):
pass
countdown = timethis(countdown)
# 这个和上面装饰器写法效果一致
  • @wraps能够复制原始函数的元信息,并赋给装饰器返回的函数 ,即被@wraps装饰的函数

  • 装饰后函数拥有__wrapped__属性

    • 直接用于访问被包装函数,即解除装饰器
      • 有多个包装器__wrapped__的行为是不可预知的, 可能会因为python版本有差,应该避免
    • 让装饰器函数正确暴露底层参数签名信息
    1
    2
    3
    countdown.__wrapped__(10000)
    from inspect import signature
    print(signature(countdown))

自定义属性

在装饰器中引入访问函数,访问函数中使用nolocal修改内部 变量t

  • 访问函数可在多层装饰器间传播,如果所有的装饰中的wrapper 都使用了@functools.wraps注解(装饰)
  • 可以使用lambda匿名函数,修改访问函数属性改变其行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from functools import wraps
import logging

def logged(level, name=None, message=None):
def decorator(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__

@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)

@attach_wrapper(wrapper)
# attach setter function
def set_level(newlevel):
nonlocal level
level = newlevel

@attach_wrapper(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg = newmsg

return wrapper
return decorator

@logged(logging.DEBUG)
def add(x, y):
return x + y

@logged(logging.CRITICAL, "example")
def spam():
print("Spam")

@timethis
@logged(logging.DEBUG)
# 使用多层装饰器,访问函数可以在多层装饰器间传播
def countdown(n):
while n > 0:
n -= 1

带参装饰器

带参装饰器就是接受参数、处理,再返回一个第一个参数为函数 的函数(内部装饰器)

三层函数

  • 最外层套一层接受参数的函数
  • 内部装饰器函数可以访问这些参数,并在“存储”在内部, 相当于一个闭包
  • 返回使用参数处理后的装饰器函数,再装饰函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from functools import wraps
import logging

def logged(level, name=None, message=None):

r"""Decorator that allows logging
:param level: logging level
:param name: logger name, default function's module
:param message: log message, default function's name
:return :decorator
"""

def decorator(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__

@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper

return decorator

@logged(logging.DEBUG)
def add(x, y):
return x + y

@logged(loggin.CRITICAL, "example")
def spam():
print("spam")

def spam():
pass
spam = logged(logging.CRITICAL, "example")(spam)
# 这样调用和之前的装饰器语句效果相同

functools.partial

partial接受一个函数作参数,并返回设置了部分参数默认值的 函数,而最外层函数就只是用于“获取”参数,因此可以使用此技巧 减少一层函数嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import partial

def attach_wrapper(obj, func=None):

r"""Decorator to attach function to obj as an attr
:param obj: wapper to be attached to
:param func: function to be attached as attr
:return: wrapper
"""

if func is None:
return partial(attach_wrapper, obj)
setattr(obj, func.__name__, func)
return func

参数可选

三层函数

这种形式的带可选参数的装饰器,即使不传递参数,也必须使用 调用形式装饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from functools import wraps
import logging

def logged(level=logging.DEBUG, name=None, message=None):

r"""Decorator that allows logging
:param level: logging level
:param name: logger name, default function's module
:param message: log message, default function's name
:return :decorator
"""

def decorator(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__

@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper

return decorator

@logged()
# 这种方式实现的默认参数装饰器必须使用`@logged()`调用的
# 形式,从装饰器形式就可以看出,必须调用一次才能返回内部
# 装饰器
# 这种形式的装饰器不符合用户习惯,不用传参也必须使用的
# 调用形式
def add(x, y):
return x + y

@logged(level=logging.CRITICAL, name="example")
def add(x, y):
return x + y
partial

这种形式的装饰器,不传参时可以像无参数装饰器一样使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from functools import wraps, partial
import logging

def logged(
func=None,
*,
level=logging.DEBUG,
name=None,
message=None):
if func is None:
return partial(logged, level=level, name=name, message=message)
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__

@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)

return wrapper

@logged
# 使用`partial`函数形式的带默认参数的装饰器,可以不用
# 调用形式
def add(x, y):
return x + y

@logged(level=logging.CRITICAL, name="example")
def spam():
print("spam")

用途示例

强制检查参数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def typeasssert(*ty_args, **ty_kwargs):

r"""Decorator that assert the parameters type
:param *ty_args: parameters type indicated with position
:param **ty_kwargs: parameters type indicated with keywords
:return: wrapper
"""

def decorator(func):
# if in optimized mode, disable type checking
if not __debug__:
return func

sig = signature(func)
# map function argument names to asserted types
bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments

@wraps(func)
def wrapper(*args, **kwargs):
# map function argument names to paraments
bound_values = sig.bind(*args, **kwargs).argument
for name, val in bound_values.items():
if name in bound_types:
if not isinstance(value, bound_types[name]):
raise TypeError(
"Argument {} must be {}.".format(name, bound_types[name])

return func(*args, **kwargs)
return wrapper
return decorator

装饰器类

为了定义类装饰器,类需要实现__call____get__方法,然后 就可以当作普通的的装饰器函数使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import types
from functools improt wraps

class Profiled:
def __init__(self, func):
wraps(func)(self)
# 获取`func`的元信息赋给实例
self.ncalls = 0

def __call__(self, *args, **kwargs):
self.nacalls += 1
return self.__wrapped__(*args, **kwargs)
# 解除装饰器

def __get__(self, instance, cls):
if intance is None:
return self
else:
return types.MethodType(self, instance)

装饰器方法

在类中定义装饰器方法,可以将多个装饰器关联于同一个类的实例, 方便在装饰器中记录、绑定、共享信息

  • @property装饰器类:可以将类方法method_atrr“转变”为 属性

    • 设置完成之后,会创建2个以方法名开头的装饰器 method_attr.settermethod_attr.deleter用于装饰 同名的方法
    • 分别对应其中包含setterdeletergetter@property自身)三个方法

    详情查看clsobj

装饰类、静态方法

装饰器必须放在@staticmethod@classmethod装饰器之前 (内层),因为这两个装饰器实际上并不创建callable对象,而是 创建特殊的描述器对象

添加参数

为原函数“添加”KEYWORD_ONLY参数,这种做法不常见,有时能避免 重复代码

  • KEYWORD_ONLY参数容易被添加进*args**kwargs
  • KEYWORD_ONLY会被作为特殊情况挑选出来,并且不会用于调用 原函数
  • 但是需要注意,被添加的函数名称不能与原函数冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from functools import wraps
import inspect
def optional_debug(func):
if "debug" in inspect.getargspect(func).args:
raise TypeError("Debug argument already defined")
# 防止原函数参数中参数与新增`debug`冲突

@wraps(func)
def wrapper(*args, debug=False, **kwargs):
# 给原函数“增加”了参数
if debug:
print("calling", func.__name__)
return func(*args, **kwargs)
return wrapper

装饰器扩充类功能

可以通过装饰器修改类某个方法、属性,修改其行为

  • 可以作为其他高级技术:mixin、metaclass的简洁替代方案
  • 更加直观
  • 不会引入新的继承体系
  • 不依赖super函数,速度更快

注意:和装饰函数不同,装饰类不会返回新的类,而是修改原类, 因此装饰器的顺序尤为重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def log_getattribute(cls):
orig_getattribute = cls.__getattribute__

def new_getattribute(self, name):
print("getting:", name)
return orig_getattribute(self, name)

cls.__getattribute = new_getattribute
return cls

@log_getattribute
class A:
def __init__(self, x):
self.x = x
def spam(self):
pass

Metaclass元类

用途示例

单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton(type):
def __init__(self, *args, **kwargs):
self.__intance = None
super().__init__(*args, **kwargs)

def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance

class Spam(metaclass=Singleton):
def __init__(self):
print("Creating Spam")

缓存实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import weakref
class Cached(type):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__cache = weakref.WeakValueDictionar()

def __call__(self, *args):
if args in self.__cache:
return self.__cache[args]
else:
obj = super().__call__(*args)
self.__cache[args] = obj
return obj

class Spam(metaclass=Cached):
def __init__(self, name):
print("creating spam({!r})".format(name))
self.name = name

Python Readme

函数书写声明

1
2
3
4
5
6
return = func(
essential(type),
optional=defaults/type,
*args,
**kwargs
)

格式说明

参数

  • essential参数:essential(type)

    • 没有=
    • 在参数列表开头
    • (type):表示参数可取值类型
  • optional参数:optional=defaults/type

    • defaults若为具体值,表示默认参数值
      • 默认值首字母大写
    • 默认参数值为None
      • 函数内部有默认行为
      • None(默认行为等价参数值)
    • type:之后表示可能取值类型
  • *args参数:[参数名]=defaults/type

    • 首参数值为具体值表示函数默认行为(不是默认值,args 参数没有默认值一说)
    • 其后为可取参数值类型
    • 说明
      • 参数名仅是标记作用,不能使用关键字传参
      • []:代表参数“可选”
  • **kwargs参数:[param_name=defaults/type]

    • 参数默认为可选参数,格式规则类似args参数
  • POSITION_ONLY参数:[param_name](defaults/type)

    • POSITION_ONLY参数同样没有默认值一说,只是表示默认 行为方式(对应参数值)
  • 参数名后有?表示参数名待确定
  • 参数首字母大写表示唯一参数

返回值

返回值类型由返回对象名称蕴含

对象类型

  • obj(type)type表示包含元素类型

Pandas Readme

常用参数说明

  • 函数书写声明同Python全局
  • 以下常用参数如不特殊注明,按此解释

DataFrame

  • axis=0/1/"index"/"columns"

    • 含义:作用方向(轴)
    • 默认:0/"index",一般表示row-wise(行变动)方向
  • inplace=False/True

    • 含义:是否直接在原对象更改
    • 默认:False,不更改,返回新DF对象(为True时无返回值)
    • 其他
      • 大部分df1.func()类型函数都有这个参数
  • level=0/1/level_name...

    • 含义:用索引层级
    • 默认:部分默认为0(顶层级)(也有默认为底层级), 所以有时会如下给出默认值
      • t(top):顶层级0(仅表意)
      • b(bottom):底层级-1(仅表意)
      • 默认值为None表示所有层级

Pandas非必须依赖包

文件相关

  • Excel
    • xlrd/xlwtxls格式读写,速度较快
    • openpyxlxlsx格式读写,速度较慢

Pandas版本

  • 0.22.x

    • flaot类型可作为Categorical Index成员,不能被用于 loc获取值
  • l.1.1

    • astype方法不支持pd.Timestamp类型,只能用 "datetime64"替代
  • ALL

    • Category Series作为groupby聚集键时,类别元素都会 出现在聚集结果中,即使其没有出现在seris值中
    • setfrozenset被认为是list-like的indexer,所以 在索引中的frozenset无法用一般方法获取

      1
      df.iloc[list(df.index).index(fronzenset)]

并行计算简介

并行计算模型

PRAM

Parallel Random Access Machine:随机存取并行机器模型,也称 共享存储的SIMD模型,从串行的RAM模型直接发展起来

假定

  • 容量无限大的共享存储器

  • 有限个或无限个功能相同的处理器,具有简单的算术运算和逻辑 判断功能

  • 任何时刻各处理器都可以通过共享存储单元相互交互数据

分类

根据处理器对共享存储单元同时读、同时写的限制,PRAM模型可以 分为下面几种

  • PRAM-EREW:Exclusive-Read and Exclusive-Write,不允许同时 读、写

  • PRAM-CREW:Concurrent-Read and Exclusive-Write,允许同时 读、不允许同时写

  • PRAM-CRCW:Concurrent-Read and Concurrent-Write,允许 同时读和同时写,允许同时写是不现实的,进一步约定

    • CPRAM-CRCW:Common PRAM-CRCN,只允许所有的处理器同时 写相同的数

    • PPRAM-CRCW:Priority PRAM-CRCN,只允许最优先的处理器 先写

    • APRAM-CRCW:Aribitrary PRAM-CRCN,允许任意处理器 自由写

    • SPRAM-CRCW:Sum PRAM-CRCN,往存储器中写的实际内容是 所有处理器写的数的和

优点

  • 适合于并行算法的表达、分析和比较

  • 使用简单,很多关于并行计算机的底层细节,比如处理器间通信 、存储系统管理和进程同步都被隐含在模型中

  • 易于设计算法和稍加修改便可以运行在不同的并行计算机系统上

缺点

  • 模型中使用了全局、单一共享存储器、局存容量较小

    • 不足以描述分布主存多处理机的性能瓶颈
    • 共享单一存储器的假定,不适合分布存储结构的MIMD机器
  • PRAM模型是同步的

    • 意味着所有的指令都按照锁步的方式操作
    • 耗时长、不能反映现实中很多系统的异步性;
  • 假设不现实

    • 模型假设每个处理器可在单位时间访问共享存储器的任一 单元,要求处理机间通信无延迟、无限带宽和无开销,忽略 资源竞争和有限带宽
    • 假设处理机有限或无限,对并行任务的增大无开销

BSP模型

异步MIMD-DM(Distributed Memory)模型

  • BSP模型支持消息传递系统,块内异步并行,块间显式同步

  • 模型基于一个master协调,所有worker同步(lock-step)执行, 数据从输入的队列中读取

模型描述

模型可以用 p/s/g/i 4个参数进行描述

  • p:处理器的数目(带有存储器)。

  • s:处理器的计算速度。

  • g:选路器吞吐率

    • 定义为:time_steps / packet
    • time_steps:每秒本地完成的局部计算数目
    • packet:通信网络每秒传送的数据量
  • i:全局同步时间开销,Barrier synchronization time

同步和通信的开销都规格化为处理器的指定条数,p台处理器同时 传送h个字节信息,则gh就是通信的开销

模型结构

BSP程序同时具有水平和垂直两个方面的结构

  • 垂直上:BSP程序由一系列串行的超步(superstep)组成

    super_step_line

  • 从水平上看:在一个超步中,所有的进程并行执行局部计算

  • 超步可分为三个阶段

    super_step

    • 本地计算阶段:每个处理器只对存储本地内存中的数据进行 本地计算
    • 全局通信阶段:对任何非本地数据进行操作
    • 栅栏同步阶段:等待所有通信行为的结束

特点

  • 模型将计算划分为一个一个的超步(superstep),有效避免死锁。

  • 处理器和路由器分开,强调了计算任务和通信任务的分开,且 路由器仅仅完成点到点的消息传递,不提供组合、复制和广播等 功能,掩盖具体的互连网络拓扑、简化了通信协议

  • 一般分布存储的MIMD模型的可编程性比较差,但BSP模型中,若 计算和通信可以合适的平衡(例如g=1),则它在可编程方面 呈现出主要的优点。

  • 采用障碍同步的方式以硬件实现的全局同步是在可控的粗粒度级 ,从而提供了执行紧耦合同步式并行算法的有效方式,而程序员 并无过分的负担

  • BSP模型起到为软件和硬件之间架起一座类似于冯·诺伊曼机的 桥梁的作业,因此BSP模型也常叫做桥模型

  • BSP模型上曾直接实现了一些重要的算法(如矩阵乘、并行前序 运算、FFT和排序等),均避免自动存储管理的额外开销

  • 为PRAM模型所设计的算法,都可以采用在每个BSP处理器上模拟 一些PRAM处理器的方法来实现。

不足

  • 模型中,在超级步开始发送的消息,即使网络延迟时间比超级步 长度短,该消息也只能在下一个超级步才能被使用

  • 全局障碍同步假定是用特殊的硬件支持的,但很多并行机中可能 没有相应的硬件

LogP模型

分布存储、点到点的多处理机模型

模型描述

通信网络由4个主要参数描述

  • L:Latency,源处理机与目的处理机进行消息通信所需要的 等待或延迟时间的上限,表示网络中消息的延迟

  • O:Overhead,处理机准备发送或接收每个消息的时间开销

    • 包括操作系统核心开销和网络软件开销
    • 在这段时间里处理不能执行其它操作
  • G:Gap,一台处理机连续两次发送或接收消息时的最小时间 间隔,其倒数即微处理机的通信带宽。

  • P:Processor,处理机/存储器模块个数

以处理器周期为时间单位,Log可以表示成处理器周期 整数倍

特点

  • 抓住了网络与处理机之间的性能瓶颈:带宽

    • g反映了通信带宽,单位时间内最多有L/g个消息能进行处理机间传送。
  • 处理机之间异步工作,并通过处理机间的消息传送来完成同步

  • 对多线程技术有一定反映。每个物理处理机可以模拟多个虚拟 处理机(VP)

    • 某个VP有访问请求时,计算不会终止
    • VP的个数受限于通信带宽和上下文交换的开销、网络容量
    • 至多有L/g个VP。
  • 消息延迟不确定,但延迟不大于L

    • 消息经历的等待时间是不可预测的
    • 但在没有阻塞的情况下,最大不超过L。
  • 可以预估算法的实际运行时间。

不足

  • 对网络中的通信模式描述的不够深入,有些现象未描述、考虑

    • 重发消息可能占满带宽
    • 中间路由器缓存饱和等未加描述
  • 主要适用于消息传递算法设计

    • 对于共享存储模式,则简单地认为远地读操作相当于两次 消息传递
    • 未考虑流水线预取技术、Cache引起的数据不一致性以及 Cache命中率对计算的影响
  • 未考虑多线程技术的上下文开销

  • 用点对点消息路由器进行通信,这增加了编程者考虑路由器上 相关通信操作的负担

背景

  • 根据技术发展的趋势,20世纪90年代末和未来的并行计算机发展 的主流之一是巨量并行机,即MPC(Massively Parallel Computers), 它由成千个功能强大的处理器/存储器节点,通过具有有限带宽 和相当大的延迟的互连网络构成。所以我们建立并行计算模型 应该充分考虑到这个情况,这样基于模型的并行算法才能在现有 和将来的并行计算机上有效的运行。

  • 根据已有的编程经验,现有的共享存储、消息传递和数据并行 等编程方式都很流行,但还没有一个公认的和占支配地位的编程方式, 因此应该寻求一种与上面的编程方式无关的计算模型。而根据 现有的理论模型,共享存储PRAM模型和互连网络的SIMD模型对 开发并行算法还不够合适,因为它们既没有包含分布存储的情况, 也没有考虑通信和同步等实际因素,从而也不能精确的反映运行 在真实的并行计算机上的算法的行为,所以,1993年D.Culer等人 在分析了分布式存储计算机特点的基础上,提出了点对点通信 的多计算机模型,它充分说明了互联网络的性能特性,而不涉 及到具体的网络结构,也不假定算法一定要用现实的消息传递 操作进行描述

并行算法基本设计策略

串改并

发掘和利用现有串行算法中的并行性,直接将串行算法改造为并行 算法

  • 最常用的设计思路但并不普适
  • 好的串行算法一般无法并行化(数值串行算法可以)

全新设计

从问题本身描述出发,不考虑相应的串行算法,设计全新并行算法

借用法

找出求解问题和某个已解决问题之间的联系,改造或利用已知算法 应用到求解问题上

并行算法常用设计技术

划分设计技术

使用划分法把问题求解分成两步:

  • 把给定问题划分成p个几乎等尺寸的子问题
  • 用p台处理器并行求解子问题

分治设计技术

  • 将复杂问题划分成较小规模特性相同的子问题
  • 且子问题类型和原问题类型相同
  • 通常用递归完成分治算法

平衡树设计技术

  • 以树的叶结点为输入,中间结点为处理结点
  • 由叶向根或由根向叶逐层进行并行处理

倍增设计技术

  • 递归调用时,所要处理数据之间的距离逐步加倍
  • 经过k步后即可完成距离为2^k的所有数据的计算

流水线技术

  • 将算法路程分成p个前后衔接的任务片段,一个任务片段完成后 ,其后继任务片段可以立即开始

  • 则可以引入流水线的思想来处理多条数据

并行计算机体系架构

Shared Memory

shared_mem_image

Distributed Memory

distributed_mem_image

Hybrid

hybrid_mem_image

并行编程模型

特征 数据并行 共享变量 消息传递
代表 HPF OpenMP MPI、PVM
可移植性 SMP、DSM、MPP SMP、DSM 所有流行并行计算机
并行力度 进程级细粒度 线程级细粒度 进程级粗粒度
并行操作方式 松散同步 异步 异步
数据存储 共享存储 共享存储 分布式存储
数据分配方式 半隐式 隐式 显示
难度 较简单 简单
可扩展性 一般 较差

数据并行模型

相同操作同时作用于不同数据

共享变量模型

用共享变量实现并行进程间通信

消息传递模型

驻留在不同节点上的进程通过网络传递消息相互通信,实现进程之间 信息交换、协调步伐、控制执行等

?