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

可迭代对象(满足下列条件 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 的隐式绑定. 此总结将会对下一节的内容(super 关键字)有所帮助.


super 关键字

根据文档, Python 2 中 super 关键字的用法为:

super(type[, object-or-type])

一般来说, 有两种用法:

super(C, self) # => bound method

super(C) # => unbound method

而且根据文档来看, 第二个参数是可选的, 那么似乎 super(C) 这种用法才是常见用法. 然而实际并不是, super(C, self) 才是常规的正经做法, 不使用第二个参数的用法简直一团糟.

收集了一些资料, 需要回头看, 另外可能需要看完 流畅的Python 第六部分元编程之后才能解答, 因为不使用第二个参数的用法似乎涉及到了 descriptor 的知识, 需要先行学习了才能明白.

参考资料:

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

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


Comments
Write a Comment