可迭代对象 / 迭代器 / 生成器函数 / 生成器表达式 / 生成器
可迭代对象(满足下列条件 1, 2 中任意一个):
- 实现了
__iter__
方法, 用于返回一个迭代器. - 实现了
__getitem__
方法. Python 会自动创建一个迭代器并从 0 开始调用__getitem__
开始迭代. - 可迭代对象被迭代时, 会隐式使用
iter()
调用自己, 得到__iter__
返回的迭代器(或者自动创建一个).
迭代器(满足下列所有条件):
- 实现了
__iter__
方法, 用于返回一个迭代器, 一般是自身. - 实现了
__next__
方法(Python2 中是next
方法).
生成器函数:
- 使用了
yield
关键字的函数. - 自身是函数.
- 调用后返回的是一个生成器.
生成器表达式:
- 表达式得到的是一个生成器
生成器:
- 生成器是迭代器, 因为它具有
__iter__
方法和__next__
方法. - 迭代器并不止生成器.
一点思考:
list 是可以迭代的, 但是是在调用
iter(list)
后会返回一个可迭代对象, 所以我们说它是可以迭代的.
如果一个对象实现了__iter__
特殊方法, 但是直接返回一个 list, 这是错误的. 因为__iter__
会在iter()
函数唤起的时候得到调用, 期望是直接得到一个 Iterator, 但是你返回一个 list, 那么显然是类型不符的. 所以此处在__iter__
中必须显式直接使用iter(list)
返回一个 Iterator.
yield 关键字
yield 关键字在 Python 中很重要, 虽然看起来很像, 其实可能可以实现 3 种不同的效果(根据 流畅的Python 一书的观点, 这其实并不好, Python 其实应该为这些效果选择不同的关键字而不是复用 yield 关键字, 复用会容易导致疑惑).
这 3 种效果包括:
- 生成器 (yield 左边绝不产出值. 根本上来说, 生成器用于迭代)
- 协程 (yield 左边可以产出值. 根本上来说, 协程用于流程控制)
- 委派生成器 (
yield from
, Python 3 支持)
生成器:
基本用法, yield x
, 把 x
的值产出到调用处, 注意此处 yield 关键字左边本身没有值, 这是 yield 作为生成器的时候与作为协程的时候的重大区别.
协程:
基本用法, y = yield x
, 注意此处 yield 关键字左边会得到一个值, 此值来源于调用处 .send(v)
函数提供的值 v
. 当 yield 本身会产生一个值提供给左侧参数的时候, 我们称之为协程.
使用 next(gen)
, 和 gen.send(v)
都可以让 gen 向前推进, 区别是前者不会传入值, 后者传入了值. 前者是生成器用法, 后者是协程用法(并且注意下两者的调用写法).
委派生成器:
语法 yield from
, 只要 def
中使用了 yield from, 就是一个委派生成器, 调用它得到的将会是一个子生成器. 并且委派生成器自身来说, 主要只是起到一个管道作用, 作为调用方和子生成器之间的管道.
由于就 yield from
本身来说, 是会不断产生子生成器, 而且它本身只是两方之间的一个管道, 所以也常用于"优雅"地遍历包含迭代器的迭代器, 避免了多重 for 循环.
else 关键字
有可能鲜为人知, Python 中除了常规的 if/else
用法外, else
还可用于 for
, while
, try
之后.
for:
for 语句正常执行完毕, 即未被 break 等语句终止, 执行 else.
while:
仅当 while 因为假值退出, 即跟 for 同样, 未被 break 终止时, 执行 else.
try:
仅当 try 没有产生异常时, 运行 else.
列表推导 / 生成器表达式
形如 [x for x in range(10)]
的写法叫做列表推导, 得到的是一个 list
.
列表推导可以多维, 可以加判断条件, 简单总结如下:
[x for x in range(10)] # 列表推导
# 等同于
l = []
for x in range(10):
l.append(x)
[(x, y) for x in range(10) for y in range(10)] # 二维列表推导
# 等同于
l = []
for x in range(10):
for y in range(10):
t = (x, y)
l.append(t)
[(x, y) for x in range(10) if x % 2 == 0 for y in range(10) if y % 2 == 0] # 带条件二维列表推导
# 等同于
l = []
for x in range(10):
if x % 2 == 0:
for y in range(10):
if y % 2 == 0:
t = (x, y)
l.append(t)
# 只举例到二维, 实际上维度可以继续增加, 不再赘述
# 而且很显然, 列表推导比同样的 for 循环写法简洁了不止一点点
列表推导很大程度上可以用来替代 map
和 filter
函数, 使代码显得更为简洁, 避免出现 lambda
函数:
# map
l = map(lambda x: x ** 2, [1, 2, 3]) # --> [1, 4, 9]
# 等效的列表推导
l = [x ** 2 for x in [1, 2, 3]] # --> [1, 4, 9]
# filter
l = filter(lambda x: x % 2 == 0, [1, 2, 3]) # --> [2]
# 等效的列表推导
l = [x for x in [1, 2, 3] if x % 2 == 0] # --> [2]
生成器表达式的基础还是列表推导, 语法跟列表推导几乎一致, 唯一的不同是把方括号 []
换成了圆括号 ()
:
[x for x in range(10)] # 列表推导. <type 'list'>
(x for x in range(10)) # 生成器表达式, 列表推导的各种写法此处依然支持. <type 'generator'>
而且注意到, 列表推导得到是 <type 'list'>
, 而生成器表达式得到的是 <type 'generator'>
.
这一点很重要. 因为众所周知, list
是实际存在的, 一个巨大的 list
意味着实际占有巨大的内存. 而 generator
是惰性生成的, 只有调用才会生成值, 所以并不会实际占用巨大内存. 所以使用生成器表达式可以节约内存占用.
字典推导 / 集合推导
字典推导:
d = {k: v for k, v in enumerate(range(5), 0)} # --> {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
集合推导:
s = {x for x in range(5)} # --> set([0, 1, 2, 3, 4])
list / tuple
list 不可以作为 dict 的 key, tuple 需要分情况讨论.
tuple 虽然本身不可变, 但是它可以包含可变对象, 比如 ([])
, 这种情况下的 tuple 是不可以作为字典的 key 的. 只有 tuple 中所有的对象都是不可变的时候, 才可以作为字典的 key.
其实 Python 判断是否可以作为字典的 key 的条件就是该对象是否可以哈希化(hashable), 即 hash(x)
是否能成功. 正确实现了特殊方法 __hash__
, 并且能唯一返回一个结果的对象才能作为字典的 key.
l = []
hash(l) # TypeError: unhashable type: 'list', 所以不能作为字典 key
t = ()
hash(t) # --> 3527539, 成功, 所以可以作为字典 key
t = (1, "str", (2))
hash(t) # --> 586561332
d = {t: "t"} # 正确
t = (1, "str", [2])
d = {t: "t"} # TypeError: unhashable type: 'list', 因为包含 list, 所以做 key 失败
盒子模型 / 便利贴模型
盒子模型意味着每个变量名就是一个盒子, a = b
后, a 和 b 是两个盒子, 更改 a 盒子的东西不会影响到 b 盒子.
便利贴模型意味着变量名只是一个便利贴, a = b
后, a 和 b 都是同一个盒子上的便利贴, 更改 a 里的内容, 在 b 中同样会查看到.
Python 和 Java 的变量是便利贴模型, C++ 是盒子模型:
Python 便利贴:
class Foo:
def __init__(self):
self.x = 1
f1 = Foo()
f2 = f1
f1.x = 2;
print "f1.x: %s" % f1.x # f1.x: 2
print "f2.x: %s" % f2.x # f2.x: 2
Java 便利贴:
class Dog {
int x = 1;
}
public class Test {
public static void main(String[] args) {
Dog dog1 = new Dog();
Dog dog2 = dog1;
dog1.x = 2;
System.out.println("dog1.x: " + dog1.x); // dog1.x: 2
System.out.println("dog2.x: " + dog2.x); // dog2.x: 2
}
}
C++ 盒子:
#include <iostream>
using namespace std;
class Foo {
public:
int x = 1;
};
int main() {
Foo f1 = Foo();
Foo f2 = f1;
f1.x = 2;
cout << "f1.x: " << f1.x << endl; // f1.x: 2
cout << "f2.x: " << f2.x << endl; // f2.x: 1
return 0;
}
这也意味着函数调用的时候, 各自表现形式不同, Python 和 Java 的函数内部修改传入的一个变量, 会影响到外部, 而 C++ 不会.
Python:
class Foo:
def __init__(self):
self.x = 1
def bar(f):
f.x = 2
f = Foo()
bar(f)
print f.x # 2
Java:
class Dog {
int x = 1;
}
public class Test {
public static void bar(Dog d) {
d.x = 2;
}
public static void main(String[] args) {
Dog dog = new Dog();
bar(dog);
System.out.println(dog.x); // 2
}
}
C++:
#include <iostream>
using namespace std;
class Foo {
public:
int x = 1;
};
void bar(Foo f) {
f.x = 2;
}
int main() {
Foo f = Foo();
bar(f);
cout << f.x << endl; // 1
return 0;
}
set
# 创建 set
s = {1, 2, 3} # set([1, 2, 3])
s = {1, 2, 2, 3, 3} # set([1, 2, 3]), 不重复集合
s = set([1, 2, 3]) # set() 接受一个可迭代对象
# 创建空 set
s = set() # <type 'set'>, 正确
s = {} # <type 'dict'>, 错误
%r %s !r !s
%r
, %s
用于老式 %
字符串格式法.
!r
, !s
用于新式 str.format
字符串格式法.
%r
, !r
使用 repr()
调用对象, 即调用对象的 __repr__
.
%s
, !s
使用 str()
调用对象, 即调用对象的 __str__
.
具体参见下列例子:
import datetime
d = datetime.date.today()
repr(d) # --> datetime.date(2019, 1, 14)
str(d) # --> 2019-01-14
# 老式字符串格式化法
"%r" % d # --> datetime.date(2019, 1, 14)
"%s" % d # --> 2019-01-14
# 新式字符串格式化法
"{!r}".format(d) # --> datetime.date(2019, 1, 14)
"{!s}".format(d) # --> 2019-01-14
描述符
实现了以下特殊方法任意之一的类可以被称作描述符:
- __get__()
- __set__()
- __delete__()
其中实现了 __set__()
方法的被称为覆盖型描述符, 没有实现 __set__()
方法的被称为非覆盖型描述符. 他们之间的区别在于托管类(存放描述符的类)中具有同名属性的时, 描述符是否能够覆盖这个同名的属性.
关于描述符, 一个值得注意的一点是, 其实所有的类中的方法都是描述符, 因为方法就是函数, 而所有的函数其实都天生实现了 __get__()
特殊函数.
描述符的一个特点是, 一般来说, 通过实例访问的时候, 会返回一个可调用的对象, 而不通过实例访问时(即 instance 为 None)的时候, 会返回描述符自身.
# 注意此处必须使用 python3, 使用 python2 会分别显示 bound method 和 unbound method, 参照下例
class Foo(object): # 托管类
def foo(self):
pass
foo = Foo()
# 通过实例访问, 得到一个可调用的对象, 此处是 bound method
print(foo.foo) # <bound method Foo.foo of <__main__.Foo object at 0x7f31f45747b8>>
# 直接通过类访问(没有实例存在), 得到描述符自身, 此处是一个函数
print(Foo.foo) # <function Foo.foo at 0x7f31f44e1bf8>
# 使用 python2 的显示结果
class Foo(object): # 托管类
def foo(self):
pass
foo = Foo()
print(foo.foo) # <bound method Foo.foo of <__main__.Foo instance at 0x7fc1f101d560>>
print(Foo.foo) # <unbound method Foo.foo>, 其实 unbound 也就是一个函数
既然在 python3 中, 既然通过类调用方法得到的只是一个普通函数, 那么此函数除了能处理类对象以外, 甚至还能处理任意满足相应"鸭子类型"要求的对象, 比如:
class Foo(object):
def foo(self):
return len(self)
foo = Foo()
# 此处传入的并不是 Foo 的实例, 仅仅是一个具有 __len__ 特殊函数的对象, 但是同样可以处理
print(Foo.foo([1, 1, 1])) # 3
当然, 在 python2 中, 会报错:
class Foo(object):
def foo(self):
return len(self)
foo = Foo()
print(Foo.foo([1, 1, 1])) # TypeError: unbound method foo() must be called with Foo instance as first argument (got list instance instead)
调用 foo.foo
的时候其实等于调用 Foo.foo.__get__(foo)
, 把实例 foo 传递进去固定下来, 类似于 functools.partial
的效果, 然后得到了一个 bound 函数.
__get__()
函数的定义一般是 __get__(self, instance, owner=None)
, self 一般是隐式自动传递进去的, 所以 Foo.foo.__get__(foo)
中传递进去的参数 foo 就成为了 instance. 绑定了这个 instance 之后, 普通的函数就成为了 bound method.
绑定方法对象(描述符)其实有一个 __self__
属性, 永远等于调用此方法的实例引用. 绑定方法对象还有一个 __func__
属性, 永远等于托管类上的原始函数的引用. 绑定方法对象还有一个 __call__
方法, 用于处理真正的调用过程, 调用的时候, __call__
会调用 __func__
, 把 __func__
中的第一个参数设为 __self__
的值(一般也就是把 self 设为 __self__), 这其实就完成了 self 的隐式绑定.
总结一下, 使用实例调用方法的时候, 实际上会调用 instance.__get__(X)
. 而 Y.__get__(instance)
会完成 self 的隐式绑定.
关于 __get__() 的一些例子
class Class(object):
pass
def foo(self):
pass
obj = Class()
# 绑定到实例上, 未绑定到类上
print foo.__get__(obj) # <bound method ?.foo of <__main__.Class object at 0x00000000053A3358>>
# 绑定到实例和相应的类上
print foo.__get__(obj, Class) # <bound method Class.foo of <__main__.Class object at 0x00000000053A3358>>
# 绑定到类上, 没有特定的实例
print foo.__get__(Class, Class) # <bound method Class.foo of <class '__main__.Class'>>
Q: __get__() 可以用在什么地方?
A: 在 monkey patch 的时候可能会用到. 参考 JavaScript 中的 bind() 函数.
举例:
class Class(object):
def bar(self): # 注意此处接收 self
print self.bar
def foo(): # 注意此处不能接收 self
print foo
obj = Class()
# 绑定方法
obj.bar() # <bound method Class.bar of <__main__.Class object at 0x0000000004E94BA8>>
obj.bar = foo
# 普通函数
obj.bar() # <function foo at 0x0000000004E969E8>
可以观察到, 我们使用 foo 函数 patch 掉 obj.bar 后, bar 不再是绑定函数, 而成了一个普通函数, 这会导致没有实例传入, 也就没有办法通过 self 调用实例的各种属性和函数. 这也就是定义 foo 函数的时候没有写 self 参数的原因(写了会报参数数量不匹配的错误).
如果想在 patch 后依然能够正确的利用 self 接收到传入的参数, 需要在 patch 之前利用 __get__() 函数完成一次绑定:
class Class(object):
def bar(self):
print self # <__main__.Class object at 0x0000000005020E10>
def foo(self): # 此时可以接收 self 参数
pass
obj = Class()
print obj # <__main__.Class object at 0x0000000005020E10>
foo = foo.__get__(obj, Class) # 绑定到 obj 上
obj.bar()
obj.bar = foo
obj.bar()
此时可以观察到两点:
- 定义 foo 函数的时候可以写上 self 参数了, 不写反而会报参数不匹配的错误.
- obj 的地址和 bar 里接收到的 self 是同一个地址, 证明我们的 self 正确的接收到了 obj 这个实例.
如此, 我们保证了 monkey patch 的时候也能正确的使用 self 接收实例.
super 方法
python2 使用 super 的一种万能写法:
super(self.__class__, self).__init__() # => bound method, no need to feed "self"
根据文档, Python 2 中 super 关键字的用法为:
super(type[, object-or-type])
一般来说, 有两种用法:
super(C, self) # => bound super object
super(C) # => unbound super object
注意此处是 bound super object 和 unbound super object, 并非 method.
根据文档来看, 第二个参数是可选的, 那么似乎 super(C)
这种用法是常见用法. 然而实际并不是, 事实上几乎没有不加第二个参数的用法. 不使用第二个参数的用法简直一团糟, 完全是各种历史遗留造成的糟粕.
反正需要记住, 使用两个参数的写法 super(C, self)
才是常规的正经做法.
参考资料:
NotImplemented Vs. NotImplementedError
这俩兄弟长得比较像, 但是用处和类型完全不一样:
- NotImplemented 是一个实例对象, 用于某些魔法方法内部作为返回值, 调用的时候得到这个返回值可以去调用此魔法方法的备选方法.
- NotImplementedError 带有 Error 后缀, 所以是一个异常类型, 通常用于父类的抽象内部, 通过
raise NotImplementedError
来表明此方法需要在子类中实现.
黑科技
inspect
import inspect
# 获取当前函数名
inspect.currentframe().f_code.co_name
# or
import sys
sys._getframe().f_code.co_name
# 获取上层函数名
inspect.currentframe().f_back.f_code.co_name
# 获取上层函数所在文件名
inspect.currentframe().f_back.f_code.co_filename
反射
利用字符串, 通过反射得到模块(已经加载过的模块):
# type 1
import commons
mod = __import__('commons')
# type 2
from list.text import commons
mod = __import__(' list.text.commons', fromlist=True) # 若无 fromlist=True 将只导入 list 目录
# type 3
import sys
mod = sys.modules['commons']
f-string
Python3.6以上开始支持的语法, 实现我多年来梦寐以求的功能. 与 Ruby 中的 #{}
语法非常相似, 非常好用.
# 没有 f-string 的时候, 只能通过 fromat 方法插入变量值
var = 1
"value of var is: {var}".format(var=var) # value of var is 1
# 利用 f-string, 变量值直接插入
var = 1
"value of var is {var}" # value of var is 1
# 而且利用还可以进行各种运算
"1 + 1 is: {1 + 1}" # 1 + 1 is 2
Monkey patch
# 猴子补丁, 此函数下可以做任何你想做的事情
def monkey_patch(self):
pass
original_func = obj.target_func # 保存原函数
obj.target_func = monkey_patch.__get__(obj) # 将 monkey_patch 绑定到 obj 实例
# 注意: 替换具体实例方法的时候才需要 __get__(), 否则 self 会没有具体指向
# 若不是实例的方法(即没有 self 参数的普通函数), 或者不是某具体实例的方法
# 而是模块中的实例方法(即不是 obj.target_func 而是 modulename.target_func)
# 则不需要使用 __get__() 进行绑定
# 此处代码使用的将是 monkey_patch() 里的内容
obj.target_func = original_func # 恢复原函数
# 此后恢复原始状态