可迭代对象 / 迭代器 / 生成器函数 / 生成器表达式 / 生成器

可迭代对象(满足下列条件 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 循环写法简洁了不止一点点

列表推导很大程度上可以用来替代 mapfilter 函数, 使代码显得更为简洁, 避免出现 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()

此时可以观察到两点:

  1. 定义 foo 函数的时候可以写上 self 参数了, 不写反而会报参数不匹配的错误.
  2. 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) 才是常规的正经做法.

参考资料:

Things to Know About Python Super [2 of 3] by Michele Simionato

Python’s super() considered super! | Deep Thoughts by Raymond Hettinger


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  # 恢复原函数

# 此后恢复原始状态
Comments
Write a Comment