引言

我们知道在诸多高级语言中都包含foreach方法,但这里讨论的更类似于JavaScript中的:

var array = ['a', 'b', 'c'];
array.forEach(function(element) {console.log(element);
});

列表对象直接调用foreach方法,并在其中对每个元素做一些事情。我们的实现效果如下:

bins = list(['1', '2', '3', '4'].foreach.int().do(lambda _: bin(_)[2:]
).do(lambda _: "{:0>4}".format(_)
))

其实我们通过Python的map函数也可以实现,这里只是提供另一种写起来优(奇)雅(葩)的思路,主要目的是学习它的实现逻辑而不是实现结果。如果非要抬杠,广泛意义上讲Python语法中本就包含foreach逻辑:

for key, value in d.items(): ...

本文是 Python修改内置类型的属性和方法 的后续,下面继续讲解上文提到的SO大神的回答 Answer - Extension method for python built-in types 中提供的foreach方法。我做了一些小改进,其中包括调整代码对Python3.x的兼容性以及实现foreach中执行自定义的lambda函数

同样Python基础知识掌握的不牢固的话,看这篇文章也会稍显吃力~

程序实现的全文

话不多说,先上代码。由于这份代码里有些故意炫技的成分,所以很不好读懂,待我慢慢说来。由于逻辑紧密很难切分,先给出全文,再慢慢解释。对于foreach的定义如下:

import operator
import functoolsdef die(message, cls=Exception):raise cls(message)def unguido(self, key):return functools.partial(getattr(__builtins__, key, None)or getattr(operator, key, None)or die(key, KeyError),self)class mapper():def __init__(self, iterator, key):self.iterator = iteratorself.key = keyself.fn = lambda o: getattr(o, key, None)def __getattribute__(self, key):if key in ('iterator', 'fn', 'key'):return object.__getattribute__(self, key)return mapper(self, key)def __call__(self, *args, **kwargs):if self.key == 'do':self.fn = args[0] if args else lambda o: oelse:self.fn = lambda o: (getattr(o, self.key, None)or unguido(o, self.key))(*args, **kwargs)return selfdef __iter__(self):for value in self.iterator:yield self.fn(value)class foreach(object):def __init__(self, iterator):self.iterator = iteratordef __getattribute__(self, key):if key in ('iterator',):return object.__getattribute__(self, key)return mapper(self.iterator, key)def __iter__(self):for value in self.iterator:yield value

使用前文提到的装饰器将foreach注册到list的方法字典中:

sign(list, 'foreach')(property(foreach))

然后调用本文引言中的方法,将十进制字符串列表转为4位二进制字符串列表:

bins = list(['1', '2', '3', '4'].foreach.int().do(lambda _: bin(_)[2:]
).do(lambda _: "{:0>4}".format(_)
))
# ['0001', '0010', '0011', '0100']

内置类型的方法注册

下面我们逆向分析一下代码的调用逻辑。首先注册方法的过程和前文提到的略有不同,之前我们是把一个函数作为value写进list的__dict__字典中的,但这里传入的是一个自定义的类,还加了一层property函数作为wrapper。我们先不用管传入类的用意,毕竟这里作为list属性字典的值,传入什么都可以,我们需要理解一下的是property函数。

Property的新用法 张风闲的文章 Python中的property属性 已经将传统用法讲解的十分细致了,作为Python高级语法,它无论是作为装饰器还是用作函数,大致思想都是为类的属性的读写访问添加一个setter层,这是一种常用的设计模式,可以缓冲用户的访问并添加边界检查、权限控制等逻辑,但同时对于用户又是透明的。本文用法更类似于函数,虽然property其实是个内置类:

# builtins.py class property(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None): pass# fget: 方法名,调用 对象.属性 时自动触发执行方法
# fset: 方法名,调用 对象.属性 = XXX 时自动触发执行方法
# fdel: 方法名,调用 del 对象.属性 时自动触发执行方法
#  doc: 字符串,调用 对象.属性.doc,此参数是该属性的描述信息

本文的property用法很特殊,它没有出现在类里,其传入的参数只有一个,是个类名。经过实验我发现,实际上property包装的意义在于调用该属性时可以自动实例化对应的类,也就是调用其__init__方法,这样看来foreach在这里应该是作为fget参数传入的。

foreach类的内部机制

接下来观察foreach类内部的机制。list.foreach自动调用其构造方法,构造方法的参数为iterator,实际上就是将list本身传入了,这里可以参考前文修改内置类中的用法:

@sign(list, 'mean')
def mean(l): return sum(l) / len(L)

同理,这里的l实际上就是list本身,可以认为它对应的就是list.*时传入的那个self参数。所以foreach的构造方法建立了一个列表内容的引用:self.iterator。观察__getattribute__方法。

__getattribute__方法 定义了外部在访问类的属性时做出的反应,更严谨地说是对属性访问运算符.的重载,因为其参数key未必是类中定义好的属性。为了防止无限递归,我们需要首先将类内定义过的属性排除在外,转发key值去调用重载前的父类object的方法。然后我们看到对于其他的key,它返回的是mapper类的一个实例。

由于作者写这篇代码的时候用的还是Python2.x,其中还有经典类和新式类的区分,可以参考新式类和经典类的属性区别和两者的方法搜索策略差异。在Python3.x中默认继承object,所以我们也可以去掉类定义中的继承,在这里使用super.*调用父类方法。接下来观察foreach的__iter__方法。

__iter__方法 传统是用来定义迭代器,但这里使用yield方法定义了一个生成器,也不是不可以。它对刚刚传入的列表进行顺序遍历,然后依次抛出每个元素。不熟悉生成器和迭代器原理的可以随便找一篇介绍,比如 Python3 迭代器与生成器 | 菜鸟教程。这个方法使得foreach对象可迭代,我们可以通过类似list([1, 2, 3].foreach)的方法输出结果,或者使用for in语句和列表推导式输出结果。

mapper类的内部机制

从上文的分析中我们得出list(['1','2','3','4'].foreach.int调用返回的实际上是mapper(['1', '2', '3', '4'], 'int'),下面继续观察mapper类的构造函数。

在__init__函数中,mapper对传入的列表和key值建立了引用,同时创建了self.fn这个属性。它其实是mapper定义的对列表中每个元素执行的一个函数。这里默认这个函数是返回元素的key属性,即getattr(o, key, None)的含义是返回o.key,如果属性不存在,就返回默认值None。

__getattribute__方法 首先同样排除掉类中定义的属性以避免死循环,然后对于未定义的key,返回一个新的mapper方法,但这时传入的iterator不再是列表,而是当前的mapper对象本身的引用self。这个实现有点函数式编程的curry化的意思,也和JQuery中的诸多方法相似,最常见的例子还是C++中重载左移运算符实现级联输出的方法:

ostream & operator<<( ostream & os, const Complex & c) {os << c.real << "+" << c.imag << "i"; // 以"a+bi"的形式输出return os;
}

对象的方法操作完成后返回对自身的引用,就可以级联地调用方法了。由于本文中方法调用直接要传递返回值,不能像这段代码这样直接返回引用,而是返回一个以自身引用为参数构造的新的类的实例。这就是最上面例子中,foreach可以调用完int()又调用两次do()的原因。

__iter__方法 mapper同样实现了一个生成器,以和foreach类保持统一。区别是,它循环遍历iterator中存储的可迭代对象,使用self.fn储存的函数分别作用于每个元素。这里仔细思考一下,其实是一个绝妙的递归,生成器嵌套的递归。例如[1,2,3].foreach.int().str()这段调用,其等价于

mapper(mapper(foreach([1, 2, 3]), 'int'), 'str')

那么当我们使用list()去收割最外层mapper对象的生成器时,对于每个元素它会调用自己的fn函数,但它首先要递归地去遍历内层mapper的生成器,内层mapper再去遍历foreach的生成器。foreach将列表原样返回给内层mapper,后者执行int函数,结果传递给外层mapper,然后外层mapper执行str函数得到结果。

__call__方法 使得mapper类的实例能够像函数一样使用。[1,2,3].foreach.int返回的是一个mapper对象,后面加上括号就可以直接执行它的__call__方法。我们使用*args和**kwargs接收不定长参数,并根据key值的不同构造不同的fn函数。如果key值为'do',我们将do括号内的第一个参数直接作为fn函数,当然为了健壮性,如果do没有参数,就将可迭代对象的元素原样返回,相当于do nothing。当key值为其他,比如‘int’、‘str’等,我们就在可迭代对象的内置方法以及Python全局的内置方法中搜索是否有匹配。

lambda o: (getattr(o, self.key, None)or unguido(o, self.key))(*args, **kwargs)

这段代码的意思是,寻找元素o的key方法或属性,如果不存在则得到None,又根据逻辑判断的短路原则,当且仅当前项为False时继续执行or后面的内容,即unguido方法。我没猜到作者为什么给它起这么个名字,只知道Guido是Python之父,加个否定前缀不知道想否定谁。在unguido函数中同样使用了or的短路:

def unguido(self, key):return functools.partial(getattr(__builtins__, key, None)or getattr(operator, key, None)or die(key, KeyError),self)

partial也是Python高级语法中很有趣的函数,其第一个参数是一个函数f,后面的参数如果是位置参数P,则依次将f的各个参数固定为p1...pn,如果后面的参数是关键字参数,则将关键字对应的参数固定下来。unguido这里将函数的第一个参数固定为self,也就是上文提到的lamda定义中的元素o。如果去掉partial的装饰,前面就应改成

unguido(self.key))(o, *args, **kwargs)

这里的or判断逻辑的意思是,先从__builtins__中寻找方法key,如果没有,再从operator库中寻找,如果仍然没有,就抛出KeyError的错误。实际上,想要效果更好,还应该考虑所有其他可用的函数,即从from XXX import *得到的函数。可以在调用die前面添加这句话

or dict(filter(lambda _: callable(_[1]), locals().items())).get(key, None)

程序效果演示

该方法不仅可以修改内置类,事实上修改一切类都是可能的,比如Numpy的ndarray

import numpy as np
import matplotlib.pyplot as plt
sign(np.ndarray, 'foreach')(property(foreach))x = np.linspace(-50, 50, 1000)
y = np.array(list(x.foreach.do(lambda _: _and np.sin(_) / _or 1
)))
plt.plot(x, y)
plt.show()

这里我们使用清晰明了的定义,画出震荡函数

的曲线图

虽然Numpy自带了更简洁和高效的写法,本文只是提供另一种思路的演示。我相信这个思路在某些场合能够体现出它的优越性~

python 列表定义 初始化为0_Python为列表添加一个foreach方法相关推荐

  1. python中列表中增加逗号,Python 实现在文件中的每一行添加一个逗号

    步骤1:读取每行(每行的类型是str) 步骤2:对每行列表化 步骤3:弹出每行的/n两个字符 步骤4:追加,/n三个字符 代码实现如下: #import os From_file=open('D:\\ ...

  2. 我的Python学习笔记(四):动态添加属性和方法

    一.动态语言相关概念 1.1 动态语言 在运行时代码可以根据某些条件改变自身结构 可以在运行时引进新的函数.对象.甚至代码,可以删除已有的函数等其他结构上的变化 常见的动态语言:Object-C.C# ...

  3. c语言将数组初始化为1_C语言数组的初始化表示方法

    展开全部 在C语言中,数组的初始化有以下62616964757a686964616fe58685e5aeb931333366303066几种方式: 1.定义的时候同时初始化: int array[10 ...

  4. 如何利用python画三棱锥_blender插件DEMO,添加一个三菱锥

    1.[文件] 插件 ~ 3KB 下载(17) bl_info = { "name" : "Tetrahedron Object", "author&q ...

  5. insert 语句的选择列表包含的项多于插入列表中的项_如何定义和使用Python列表(Lists)

    Python中最简单的数据集合是一个列表(list).列表是方括号内用逗号分隔的任何数据项列表.通常,就像使用变量一样,使用=符号为Python列表分配名称. 如果列表中包含数字,则不要在其周围使用引 ...

  6. python函数定义中参数列表里的参数是_详解Python函数中参数带星号是什么意思

    函数的参数使用除了常规的位置参数和关键字参数外,还支持可变个数的函数参数,这种支持可变个数的参数方法称为参数收集,对应的参数称为收集参数. 一.参数收集的定义 Python的函数支持可变不定数量的参数 ...

  7. Python基础day03【字符串(定义、输入输出、常用方法)、列表(定义、基本使用、增删改查、嵌套)、元组】

    视频.源码.课件.软件.笔记:超全面Python基础入门教程[十天课程]博客笔记汇总表[黑马程序员] Python基础day3 作业解析[5道 字符串题.3道 列表题.2道 元组题]      学习目 ...

  8. python基础之字符串定义常见操作、列表定义进阶操作

    字符串 创建字符串 创建字符串有三种方式:' ' ," " , ''' ''' 三引号允许换行,并且可以保留换行 引用--访问字符串中的字符 引用是访问字符串中的一个字符 不能超出 ...

  9. python函数定义中参数列表里的参数是_python-函数(def)参数 及参数解构 变量 知识整理...

    函数 python 函数 由若干语句组成的语句块.函数名称.参数列表构成,他是组织代码的最小单元 完成一定的功能 函数的作用: 结构化编程对代码的最基本的封装,一般按照功能组织一段代码 封装的目的是为 ...

最新文章

  1. 美团分布式服务治理框架OCTO之二:Mesh化
  2. 计算机中 什么是同步执行和异步执行?
  3. go to ifm as frequently as possible
  4. 问题 D: 巧求和(思维)
  5. 签入VSS中遇到UTF-8问题
  6. android 自己定义水平和圆形progressbar 仅仅定义一些style就能够
  7. 阿里云ECS,搭建MySQL5.7数据库环境
  8. android深度探索 HAL及驱动开发 第八章
  9. 查看SQL执行计划的方法及优劣
  10. java冒险模组_求推荐几个冒险类的MOD
  11. OpenShift 4 - 应急响应Demo应用(AMQ+Knative+Quay+BPM+BDM+SSO)
  12. Android安全:Hook技术
  13. Python Django之路由系统
  14. html中div圆角效果,div+css实现圆角即网页上常用的圆角效果
  15. php操作剪贴板内容代码,查看剪贴板内容的方法
  16. 网络爬虫-爱给音效素材网js逆向思路
  17. 口袋里只有一百块钱,也要活出十个亿的气势
  18. iphone 中的大小和像素问题
  19. #最详细# 常见服务器错误
  20. 【CMS建站】写给大家看的网站制作教程03—零基础学网站制作的简单入门指南...

热门文章

  1. C#实现小写金额转大写金额
  2. PCTFREEITLCONSISTANT READ
  3. java内存管理机制-转载保存有价值的东西
  4. Linux之dd命令使用
  5. 系统签名缺少libconscrypt_openjdk_jni.so解决
  6. Vysor 2.1.x Pro使用
  7. Win10关闭系统自动更新
  8. iOS 后台运行实现总结
  9. freemarker 解析对象的某元素_Freemarker常用技巧(三)
  10. cesium 加载Googl式的切片