Python 面向对象编程之类的特殊属性和方法

Python 类中,凡是以双下划线 __ 开头和结尾命名的成员(属性和方法),都被称为类的特殊成员(特殊属性和特殊方法),例如类的 __init__(self) 构造方法。此外,Python 类中还包含很多其它的特殊成员,包括 __del__(self)__new__(self) 等等,这里会一一进行详解。

类常用特殊成员整理

我们知道,Python 类中的特殊成员,其特殊性类似 C++ 类的 private 私有成员,即不能在类的外部直接调用,但允许借助类中的普通方法调用甚至修改它们(封装)。如果需要,还可以对类的特殊方法进行重写,从而实现一些特殊的功能。

这一小节,将会详解 Python 顶级父类 object 中常用特殊成员的使用:

Method: __new__

__new__() 是一种用来创建类实例的静态方法,它无需使用 @staticmethod 装饰器修饰,且该方法会优先 __init__() 初始化方法被调用。

[1] >>>> __new__() 返回当前类实例

一般情况下,重写 __new__() 的实现需要使用合适的参数调用其超类的 super().__new__(),并在返回之前修改实例。

实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class demoClass:
instances_created = 0

def __new__(cls,*args,**kwargs):
print("__new__():",cls,args,kwargs)
instance = super().__new__(cls)
instance.number = cls.instances_created
cls.instances_created += 1
return instance

def __init__(self,attribute):
print("__init__():",self,attribute)
self.attribute = attribute

test1 = demoClass("abc")
# __new__(): ('abc',) {}
# __init__(): <__main__.demoClass object at 0x7efd9982fcc0> abc
test2 = demoClass("xyz")
# __new__(): ('xyz',) {}
# __init__(): <__main__.demoClass object at 0x7efd9982fcf8> xyz
print(test1.number, test1.instances_created)
# 0 2
print(test2.number, test2.instances_created)
# 1 2

[2] >>>> __new__() 返回其它类实例

__new__() 通常会返回该类的一个实例,但有时也可能会返回其他类的实例,如果发生了这种情况,则会跳过对 __init__() 方法的调用。

而在某些情况下(比如需要修改不可变类实例(Python 的某些内置类型)的创建行为),利用这一点会事半功倍。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class nonZero(int):

def __new__(cls,value):
print("__new__()")
return super().__new__(cls,value) if value != 0 else None

def __init__(self,skipped_value):
# 此例中会跳过此方法
print("__init__()")
super().__init__()

print(type(nonZero(-12)))
# __new__()
# __init__()
print(type(nonZero(0)))
# __new__()

[3] >>>> 何时使用 __new__()

很简单,在 __init__() 不够用的时候。

例如,前面例子中对 Python 不可变的内置类型(如 intstrfloat 等)进行了子类化,这是因为一旦创建了这样不可变的对象实例,就无法在 __init__() 方法中对其进行修改。

有些读者可能会认为,__new__() 对执行重要的对象初始化很有用,如果用户忘记使用 super(),可能会漏掉这一初始化。虽然这听上去很合理,但有一个主要的缺点,即如果使用这样的方法,那么即便初始化过程已经是预期的行为,程序员明确跳过初始化步骤也会变得更加困难。不仅如此,它还破坏了 __init__() 中执行所有初始化工作 的潜规则。

并且,由于 __new__() 不限于返回同一个类的实例,所以很容易被滥用,不负责任地使用这种方法可能会对代码有害,所以要谨慎使用。

一般来说,对于特定问题,最好搜索其他可用的解决方案,最好不要影响对象的创建过程,使其违背程序员的预期。比如说,前面提到的覆写不可变类型初始化的例子,完全可以用工厂方法(一种设计模式)来替代。


Method: __repr__

我们经常会直接输出类的实例化对象,例如:

1
2
3
4
5
6
class Language:
pass
langs = Language()

print(langs)
# <__main__.Language object at 0x7fe399c6a710>

通常情况下,直接输出某个实例化对象,本意往往是想了解该对象的基本信息,例如该对象有哪些属性,它们的值各是多少等等。但默认情况下,我们得到的信息只会是 class name + object at + <memory address>,对我们了解该实例化对象帮助不大。

那么,有没有可能自定义输出实例化对象时的信息呢???答案是肯定,通过重写类的 __repr__() 方法即可。

重写 __repr__() 方法输出实例化对象时的信息 >>>>

事实上,当我们输出某个实例化对象时,其调用的就是该对象的 __repr__() 方法,输出的是该方法的返回值。

以本节开头的程序为例,执行 print(clangs) 等同于执行 print(clangs.__repr__()),程序的输出结果是一样的(输出的内存地址可能不同)。

__init__(self) 的性质一样,Python 中的每个类都包含 __repr__() 方法,这是因为 object 类包含 __reper__() 方法,而 Python 中所有的类都直接或间接继承自 object 类。因此,你可以通过在类中重写这个方法,从而实现当输出实例化对象时,输出我们想要的信息。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
class Language:
def __init__(self):
self.name = "Python"
self.add = "Python is a OOP programing."

def __repr__(self):
return "Language [name = " + self.name +", add = " + self.add + " ]"

langs = Language()
print(langs)
# Language [name = Python, add = Python is a OOP programing. ]

由此可见,__repr__() 方法是类的实例化对象用来做“自我介绍”的方法,默认情况下,它会返回当前对象的 class name + object at + <memory address>,而如果对该方法进行重写,可以为其制作自定义的自我描述信息。


Method: __del__

我们知道,Python 中通过调用 __init__() 构造方法来创建当前类的实例化对象;__del__() 方法,功能正好和 __init__() 相反,其用来销毁实例化对象。

事实上在编写程序时,如果之前创建的类实例化对象后续不再使用,最好在适当位置手动将其销毁,释放其占用的内存空间(垃圾回收,GC)。

大多数情况下,Python 开发者不需要手动进行垃圾回收,因为 Python 有自动的垃圾回收机制,能自动将不需要使用的实例对象进行销毁。

但注意,无论是手动销毁,还是 Python 自动帮我们销毁,都会调用 __del__() 方法。实例演示:

1
2
3
4
5
6
7
8
9
10
11
class Language:
def __init__(self):
print("调用 __init__() 方法构造对象")

def __del__(self):
print("调用 __del__() 销毁对象,释放其空间")

langs = Language()
# 调用 __init__() 方法构造对象
del langs
# 调用 __del__() 销毁对象,释放其空间

[1] >>>> 类的自动回收机制

千万不要误认为,只要为该实例对象调用 __del__() 方法,该对象所占用的内存空间就会被释放。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Language:
def __init__(self):
print("调用 __init__() 方法构造对象")

def __del__(self):
print("调用 __del__() 销毁对象,释放其空间")

langs = Language()
# 添加一个引用 langs 对象的实例对象
cl = langs

del langs
print("**** Program End ****")

运行结果如下:

1
2
3
调用 __init__() 方法构造对象
**** Program End ****
调用 __del__() 销毁对象,释放其空间

可以看到,当程序中有其它变量(比如这里的 cl)引用该实例对象时,即便手动调用 __del__() 方法,该方法也不会立即执行,而是等待程序执行完成之后才会释放。这就和 Python 的垃圾回收机制有关。

以上面程序中的 langs 为例,实际上构建 langs 实例对象的过程分为两步:

  1. 先使用 Language() 调用该类中的 __init__() 方法构造出一个该类的对象(将其称为 C,计数器为 0),并立即用 langs 这个变量作为所建实例对象的引用( C 的计数器值 + 1)。
  2. 在此基础上,又有一个 cl 变量引用 langs(其实相当于引用 Language(),此时 C 的计数器再 +1 ),这时如果调用 del langs 语句,只会导致 C 的计数器减 1(值变为 1),因为 C 的计数器值不为 0,因此 C 不会被销毁(不会执行 __del__() 方法)。

[2] >>>> 重写时需显式调用父类 __del__() 方法

需要额外说明的是,如果我们重写子类的 __del__() 方法(父类为非 object 的类),则必须显式调用父类的 __del__() 方法,这样才能保证在回收子类对象时,其占用的资源(可能包含继承自父类的部分资源)能被彻底释放。

为了说明这一点,这里举一个反例:

1
2
3
4
5
6
7
8
9
10
11
12
class Language:
def __del__(self):
print("调用父类 __del__() 方法")

class cl(Language):
def __del__(self):
# super().__del__() # 需要显示调用父类 __del__() 方法
print("调用子类 __del__() 方法")

c = cl()
del c
# 调用子类 __del__() 方法

Method: __dir__

在 Python 内置函数中,提到过 dir() 函数,通过此函数可以某个对象拥有的所有的属性名和方法名,该函数会返回一个包含有所有属性名和方法名的有序列表。

实例如下:

1
2
3
4
5
6
7
8
9
10
11
class Language:
def __init__ (self,):
self.name = "C语言中文网"
self.add = "http://c.biancheng.net"

def say():
pass

langs = Language()
print(dir(langs))
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'add', 'name', 'say']

可以看到,不仅仅输出本类中新添加的属性名和方法(最后 3 个),还会输出从父类(这里为 object 类)继承得到的属性名和方法名。

值得一提的是,dir() 函数的内部实现,其实是在调用参数对象 __dir__() 方法的基础上,对该方法返回的属性名和方法名做了排序。

所以,你完全可以自行调用该对象具有的 __dir__() 方法来查看某个对象拥有的所有的属性名和方法名:

1
2
3
4
5
6
7
8
9
10
11
class Language:
def __init__ (self,):
self.name = "C语言中文网"
self.add = "http://c.biancheng.net"

def say():
pass

langs = Language()
print(langs.__dir__())
['name', 'add', '__module__', '__init__', 'say', '__dict__', '__weakref__', '__doc__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

显然,使用 __dir__() 方法和 dir() 函数输出的数据是相同,仅仅顺序不同。


Method: __dict__

事实上,在 Python 类的内部,无论是类属性还是实例属性,都是以字典的形式进行存储的,其中属性名作为键,而值作为该键对应的值。

为了方便用户查看类中包含哪些属性,Python 类提供了 __dict__ 属性。

需要注意是,该属性可以用类名或者类的实例对象来调用:

  • 使用类名直接调用 __dict__,会输出该由类中所有类属性组成的字典;
  • 使用类的实例对象调用 __dict__,会输出由类中所有实例属性组成的字典。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Language:
a = 1
b = 2
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing"
def info(self):
print("Instance Method")

@classmethod
def say(cls):
print("Class Method")

# 通过类名调用__dict__
print(Language.__dict__)
# {'__module__': '__main__', 'a': 1, 'b': 2, '__init__': , 'info': , 'say': , '__dict__': , '__weakref__': , '__doc__': None}

# 通过类实例对象调用 __dict__
langs = Language()
print(langs.__dict__)
# {'name': 'Python', 'add': 'Python is a OOP Programing'}

不仅如此,对于具有继承关系的父类和子类来说,父类有自己的 __dict__,同样子类也有自己的 __dict__,它不会包含父类的 __dict__。例如:

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
class Language:
a = 1
b = 2
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing"

class CL(Language):
c = 1
d = 2
def __init__ (self):
self.na = "Python3.X"
self.ad = "Python3.X is a OOP Programing"

# 父类名调用__dict__
print(Language.__dict__)
# {'__module__': '__main__', 'a': 1, 'b': 2, '__init__': , '__dict__': , '__weakref__': , '__doc__': None}
# 子类名调用__dict__
print(CL.__dict__)
# {'__module__': '__main__', 'c': 1, 'd': 2, '__init__': , '__doc__': None}

# 父类实例对象调用 __dict__
langs = Language()
print(langs.__dict__)
# {'name': 'Python', 'add': 'Python is a OOP Programing'}
# 子类实例对象调用 __dict__
cl = CL()
print(cl.__dict__)
# {'na': 'Python3.X', 'ad': 'Python3.X is a OOP Programing'}

可见,通过子类直接调用的 __dict__ 中,并没有包含父类中的 ab 类属性;同样,通过子类对象调用的 __dict__,也没有包含父类对象拥有的 nameadd 实例属性。

总结一下:

  • 实例的 __dict__ 仅存储与该实例相关的实例属性;
  • 类的 __dict__ 存储所有实例共享的变量和函数(类属性,方法等),类的 __dict__ 并不包含其父类的属性。

[1] >>>> 修改类实例属性值

借助由类实例对象调用 __dict__ 属性获取的字典,可以使用字典的方式对其中实例属性的值进行修改,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Language:
a = 1
b = 2
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing"

#通过类实例对象调用 __dict__
langs = Language()
print(langs.__dict__)
# {'name': 'Python', 'add': 'Python is a OOP Programing'}
langs.__dict__['name'] = "Python3.X"
print(langs.name)
# Python3.X

[2] >>>> __dict__ 魔法操作

我们知道,__dict__ 是用来存储对象属性的一个字典,其键为属性名,值为属性的值。

既然 __dict__ 是个字典那么我们就可以用字典的属性了。我们通过使用 dir() 属性来看看 __dict__ 都有哪些属性:

1
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

我们看一段代码内含注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A():
def __init__(self):
self.name="liming"

def save_data(self,dicts):
self.__dict__.update(dicts)#添加字典元素
if isinstance(self.__dict__,dict):
print(True)
# 获取字典独有的属性
print(set(dir(self.__dict__))-set(dir(self)))
return self.__dict__

if __name__ == '__main__':
dicts={"a":1, "b":2, "c":3}
a=A()
print(a.save_data(dicts))

# Output:
# True
# {'pop', 'items', '__contains__', 'get', '__len__', '__delitem__', 'setdefault', 'values', '__iter__', 'clear', 'keys', 'update', 'popitem', '__getitem__', '__setitem__', 'fromkeys', 'copy'}
# {'name': 'liming', 'a': 1, 'b': 2, 'c': 3}

可以看到,__dict__ 也提供了一个 update 方法,用于使用一个新的字典所包含的键值对来更新己有的字典。

这对于给对象的属性赋值的时候,非常 pythonic:

1
2
3
4
5
6
7
8
9
class A():
def __init__(self,dicts):
self.name=dicts["name"]
self.age=dicts["age"]
self.sex=dicts["sex"]
self.hobby=dicts["hobby"]
if __name__ == '__main__':
dicts={"name":"lisa","age":23,"sex":"women","hobby":"hardstyle"}
a=A(dicts)

想象一下如果我们传入的字典有 100 个键….如何还是这样一个一个赋值不敢想不敢想?!!

你可以使用如下方法:

1
2
3
4
5
6
7
8
class A():
def __init__(self,dicts):
self.__dict__.update(dicts)
print(self.__dict__)

if __name__ == '__main__':
dicts={"name":"lisa","age":23,"sex":"women","hobby":"hardstyle"}
a=A(dicts)

common Method

这一小节我们来看几个类以及对象相关的常用函数:

setattr && getattr && hasattr

[1] >>>> hasattr() 函数

hasattr() 函数用来判断某个类实例对象是否包含指定名称的属性或方法。该函数的语法格式如下:

1
hasattr(obj, name)

说明:obj 指的是某个类的实例对象,name 表示指定的属性名或方法名。同时,该函数会将判断的结果(True or False)作为返回值反馈回来。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Language:
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing."

def say(self):
print("I am learning Python.")

langs = Language()
print(hasattr(langs,"name"))
# True
print(hasattr(langs,"add"))
# True
print(hasattr(langs,"say"))
# True

显然,无论是属性名还是方法名,都在 hasattr() 函数的匹配范围内。

需要注意的是,我们只能通过该函数判断实例对象是否包含该名称的属性或方法,但不能精确判断,该名称代表的是属性还是方法。

[2] >>>> getattr() 函数

getattr() 函数获取某个类实例对象中指定属性的值。和 hasattr() 函数不同,该函数只会从类对象包含的所有属性中进行查找。其语法格式如下:

1
getattr(obj, name[, default])

其中,obj 表示指定的类实例对象;name 表示指定的属性名;而 default 是可选参数,用于设定该函数的默认返回值,即当函数查找失败时;如果不指定 default 参数,则程序将直接报 AttributeError 错误,反之该函数将返回 default 指定的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Language:
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing."

def say(self):
print("I am learning Python.")


langs = Language()
print(getattr(langs,"name"))
print(getattr(langs,"add"))
print(getattr(langs,"say"))
print(getattr(langs,"display",'nodisplay'))

# Output:
# Python
# Python is a OOP Programing.
# <bound method Language.say of <__main__.CLanguage object at 0x000001FC2F2E3245>>
# nodisplay

可以看到,对于类中已有的属性,getattr() 会返回它们的值,而如果该名称为方法名,则返回该方法的状态信息;反之,如果该明白不为类对象所有,要么返回默认的参数,要么程序报 AttributeError 错误。

[3] >>>> setattr() 函数

setattr() 函数的功能相对比较复杂,它最基础的功能是修改类实例对象中的属性值。其次,它还可以实现为实例对象动态添加属性或者方法。其语法格式如下:

1
setattr(obj, name, value)

1)–> 修改类实例对象中的属性值

实例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Language:
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing."

def say(self):
print("I am learning Python.")


langs = Language()
print(langs.name)
print(langs.add)

setattr(langs,"name","Python3.X")
setattr(langs,"add","Python3.X is a OOP Programing.")
print(langs.name)
print(langs.add)

# Output:
# Python
# Python is a OOP Programing.
# Python3.X
# Python3.X is a OOP Programing.

2)–> 别名用法

setattr() 函数,还可以将类属性修改为一个类方法,同样也可以将类方法修改成一个类属性。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def say():
print("I am learning Python.")

class Language:
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing"

langs = Language()
print(langs.name)
print(langs.add)

setattr(langs, "name", say)
langs.name()

# Output:
# Python
# Python is a OOP Programing
# I am learning Python.

显然,通过修改 name 属性的值为 say(全局函数),原来的 name 属性就变成了一个 name() 方法。

3)–> 动态添加实例属性和方法

使用 setattr() 函数对实例对象中执行名称的属性或方法进行修改时,如果该名称查找失败,Python 解释器不会报错,而是会给该实例对象动态添加一个指定名称的属性或方法。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def say():
print("I am learning Python.")

class Language:
pass

langs = Language()
setattr(langs,"name","Python")
setattr(langs,"say", say)

print(langs.name)
langs.say()

# Output:
# Python
# I am learning Python.

可以看到,虽然 Language 为空类,但通过 setattr() 函数,我们为 langs 对象动态添加了一个 name 属性和一个 say() 方法。


issubclass && isinstance

Python 中提供了如下两个函数来进行类型检查:

  • issubclass(cls, class_or_tuple):检查 cls 是否为后一个类或元组包含的多个类中任意类的子类;
  • isinstance(obj, class_or_tuple):检查 obj 是否为后一个类或元组包含的多个类中任意类的对象。

通过使用上面两个函数,程序可以方便地先执行检查,然后才调用方法,这样可以保证程序不会出现意外情况。

类型检查实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义一个字符串
hello = "Hello";
print('["Hello" 是否是 str 类的实例?] -->', isinstance(hello, str))
print('["Hello" 是否是 object 类的实例?] -->', isinstance(hello, object))
print('[ str 是否是 object 类的子类?] -->', issubclass(str, object))
print('["Hello" 是否是 tuple 类的实例?] -->', isinstance(hello, tuple))
print('[ str 是否是 tuple 类的子类?] -->', issubclass(str, tuple), '\n')

# 定义一个列表
my_list = [2, 4]
print('{ [2, 4] 是否是 list 类的实例?} -->', isinstance(my_list, list))
print('{ [2, 4] 是否是 object 类及其子类的实例?} -->', isinstance(my_list, object))
print('{ list 是否是 object 类的子类?} -->', issubclass(list, object))
print('{ [2, 4] 是否是 tuple 类及其子类的实例?} -->', isinstance([2, 4], tuple))
print('{ list 是否是 tuple 类的子类?} -->', issubclass(list, tuple))

通过上面程序可以看出,issubclass()isinstance() 两个函数的用法差不多,区别只是 issubclass() 的第一个参数是类名,而 isinstance() 的第一个参数是变量,这也与两个函数的意义对应:issubclass 用于判断是否为子类,而 isinstance() 用于判断是否为该类或子类的实例。

[1] >>>> 元组参数

issubclass() 和 isinstance() 两个函数的第二个参数都可使用元组。

实例演示:

1
2
3
4
data = (20, 'fkit')
print('[ data 是否为列表或元组? ] ->', isinstance(data, (list, tuple))) # True
print('[ str 是否为 list 或 tuple 的 子类? ] ->', issubclass(str, (list, tuple)))
print('[ str 是否为 list 或 tuple 或 object 的子类? ] ->', issubclass(str, (list, tuple, object)))

[2] >>>> __bases__ 属性参看直接父类

Python 为所有类都提供了一个 __bases__ 属性,通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
pass
class B:
pass
class C(A, B):
pass

print('类 A 的所有父类:', A.__bases__)
print('类 B 的所有父类:', B.__bases__)
print('类 C 的所有父类:', C.__bases__)
# 类 A 的所有父类: (<class 'object'>,)
# 类 B 的所有父类: (<class 'object'>,)
# 类 C 的所有父类: (<class '__main__.A'>, <class '__main__.B'>)

可以看出,如果在定义类时没有显式指定它的父类,则这些类默认的父类是 object 类。

[3] >>>> __subclasses__() 方法参看直接父类

Python 还为所有类都提供了一个 __subclasses__() 方法,通过该方法可以查看该类的所有直接子类,该方法返回该类的所有子类组成的列表。

实例演示:

1
2
3
4
5
6
7
8
9
10
11
class A:
pass
class B:
pass
class C(A, B):
pass

print('类 A 的所有子类:', A.__subclasses__())
print('类 B 的所有子类:', B.__subclasses__())
# 类 A 的所有子类: [<class '__main__.C'>]
# 类 B 的所有子类: [<class '__main__.C'>]

Method: __call__

__call__ 是 Python 类中一个非常特殊的实例方法。功能类似于在类中重载 () 运算符,使得类实例对象可以像调用普通函数那样,以 对象名() 的形式使用。

举个例子:

1
2
3
4
5
6
7
8
9
class Language:
# 定义__call__方法
def __call__(self,name,add):
print("调用__call__()方法", name, add)


langs = Language()
langs("Python", "Python is a OOP Programing.")
# 调用__call__()方法 Python Python is a OOP Programing.

可以看到,通过在 Language 类中实现 __call__() 方法,使的 langs 实例对象变为了可调用对象。

Python 中,凡是可以将 () 直接应用到自身并执行,都称为可调用对象。可调用对象包括自定义的函数、Python 内置函数以及本节所讲的类实例对象。

对于可调用对象,实际上 名称() 可以理解为是 名称.__call__() 的简写。如下:

1
2
langs.__call__("Python", "Python is a OOP Programing.")
# 调用__call__()方法 Python Python is a OOP Programing.

再来看一个自定义函数的例子,例如:

1
2
3
4
5
6
7
def say():
print("Python is a OOP Programing.")

say()
say.__call__()
# Python is a OOP Programing.
# Python is a OOP Programing.

不仅如此,类中的实例方法也有以上 2 种调用方式,这里不再举例,有兴趣的读者可自行编写代码尝试。


__call__() 弥补 hasattr() 函数的短板 >>>>

前面我们提到,hasattr() 函数查找类的实例对象中是否包含指定名称的属性或者方法,但该函数有一个缺陷,即它无法判断该指定的名称,到底是类属性还是类方法。

要解决这个问题,我们可以借助可调用对象的概念。要知道,类实例对象包含的方法,其实也属于可调用对象,但类属性却不是。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CLanguage:
def __init__ (self):
self.name = "Python"
self.add = "Python is a OOP Programing."
def say(self):
print("I am learning Python.")

clangs = CLanguage()

if hasattr(clangs,"name"):
print(hasattr(clangs.name,"__call__"))
if hasattr(clangs,"say"):
print(hasattr(clangs.say,"__call__"))

# Output:
# False
# True

可以看到,由于 name 是类属性,它没有以 __call__ 为名的 __call__() 方法;而 say 是类方法,它是可调用对象,因此它有 __call__() 方法。


Python 中的运算符重载

前面我们学习了各种序列类型,每个类型都有其独特的操作方法。例如列表类型支持直接做加法操作实现添加元素的功能,字符串类型支持直接做加法实现字符串的拼接功能。

也就是说,同样的运算符对于不同序列类型的意义是不一样的,这是怎么做到的呢?

事实上,每种序列类型都是 Python 的一个类,例如列表是 list 类,字典是 dict 类等,这些序列类的内部使用了一个叫作 重载运算符 的技术来实现不同运算符所对应的操作。

所谓重载运算符,是指通过重写 Python 内置运算符对应的方法来实现的。这样当类对象在进行运算符操作时,系统就会调用类中相应重写方法来处理。这些方法都是以双下划线开头和结尾的,类似于 __X__ 的形式。

那么,Python 类支持对哪些内置方法进行重载呢?如下表列出来常用的可重载的运算符,以及各自的含义:

重载运算符 含义
__new__ 创建类,在 __init__ 之前创建对象
__init__ 类的构造函数,其功能是创建类对象时做初始化工作。
__del__ 析构函数,其功能是销毁对象时进行回收资源的操作
__add__ 加法运算符 +,当类对象 X 做例如 X+Y 或者 X+=Y 等操作,内部会调用此方法。但如果类中对 __iadd__ 方法进行了重载,则类对象 X 在做 X+=Y 类似操作时,会优先选择调用 __iadd__ 方法。
__radd__ 当类对象 X 做类似 Y+X 的运算时,会调用此方法。
__iadd__ 重载 += 运算符,也就是说,当类对象 X 做类似 X+=Y 的操作时,会调用此方法。
__or__ “或”运算符
__repr__,__str__ 格式转换方法,分别对应函数 repr(X)、str(X)
__call__ 函数调用,类似于 X(*args, **kwargs) 语句
__getattr__ 点号运算,用来获取类属性
__setattr__ 属性赋值语句,类似于 X.any=value
__delattr__ 删除属性,类似于 del X.any
__getattribute__ 获取属性,类似于 X.any
__getitem__ 索引运算,类似于 X[key],X[i:j]
__setitem__ 索引赋值语句,类似于 X[key], X[i:j]=sequence
__delitem__ 索引和分片删除
__get__, __set__, __delete__ 描述符属性,类似于 X.attr,X.attr=value,del X.attr
__len__ 计算长度,类似于 len(X)
__lt__,__gt__,__le__,__ge__,__eq__,__ne__ 比较,分别对应于 <、>、<=、>=、=、!= 运算符。
__iter__,__next__ 迭代环境下,生成迭代器与取下一条,类似于 I=iter(X) 和 next()
__contains__ 成员关系测试,类似于 item in X
__index__ 整数值,类似于 hex(X),bin(X),oct(X)
__enter__,__exit__ 在对类对象执行类似 with obj as var 的操作之前,会先调用 __enter__ 方法,其结果会传给 var;在最终结束该操作之前,会调用 __exit__ 方法(常用于做一些清理、扫尾的工作)

实例演示:

[1] >>>> 构造函数和析构函数:__init____del__ 重载

主要作用是进行对象的创建和回收,当实例创建时,就会调用 __init__ 构造方法。当实例对象被收回时,析构函数 __del__ 会自动执行。

1
2
3
4
5
6
7
8
9
class Human:
def __init__(self, name):
self.name = name
print("__init__", self.name)
def __del__(self):
print("__del__")

test = Human("zhang_san")
test = "del"

[2] >>>> 加减运算: __add____sub__ 重载

重载这两个方法就可以在普通的对象上添加+-运算符操作。下面的代码演示了如何使用 + && - 运算符:

1
2
3
4
5
6
7
8
9
10
11
class Computer:
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value + other
def __sub__(self, other):
return self.value - other

test = Computer(10)
print(test + 2)
print(test - 5)

如果将代码中的 __sub__ 方法去掉,再调用减号运算符就会出错。

[3] >>>> 对象的字符串表达形式: __repr____str__ 重载

都是用来表示对象的字符串表达形式:

  • print()、str() 方法会调用到 __str__ 方法
  • print()、str() 和 repr() 方法会调用 __repr__ 方法。

从下面的例子可以看出,当两个方法同时定义时,Python 会优先搜索并调用 __str__ 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Str:
def __str__(self):
return "__str__ called"
def __repr__(self):
return "__repr__ called"

str_test = Str()
print(str_test)
# __str__ called

print(repr(str_test))
# __repr__ called
print(str(str_test))
# __str__ called

[4] >>>> 索引取值和赋值: __getitem____setitem__ 重载

通过实现这两个方法,可以通过诸如 X[i] 的形式对对象进行取值和赋值,还可以对对象使用切片操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Indexer:
data = [1, 2, 3, 4, 5]

def __getitem__(self, index):
return Indexer.data[index]
def __setitem__(self, index, value):
Indexer.data[index] = value
print(Indexer.data)

test = Indexer()
print(test[1])
# 2
print(test[0:3])
# [1, 2, 3]

test[3] = 6
# [1, 2, 3, 6, 5]

[5] >>>> 设置和访问属性: __getattr____setattr__ 重载

我们可以通过重载 __getattr____setattr__ 来拦截对对象成员的访问。

__getattr__ 在访问对象中不存在的成员时会自动调用。__setattr__ 方法用于在初始化对象成员的时候调用,即在设置 __dict__item 时就会调用 __setattr__ 方法。具体例子如下:

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
class Test:

def __init__(self, ax, bx):
self.ax = ax
self.bx = bx

def foo(self):
print(self.__dict__)

def __getattr__(self, name):
if name in self.__dict__.keys():
return self.__dict__[name]
print("__getattr__ called")

def __setattr__(self, name, value):
print("__setattr__ called")
self.__dict__[name] = value

test = Test(1, 2)
test.foo()
# __setattr__ called
# __setattr__ called
# {'ax': 1, 'bx': 2}

print(test.c)
# __getattr__ called
# None

test.ax = 4
test.foo()
# __setattr__ called
# {'ax': 4, 'bx': 2}

从结果可以看出,访问不存在的实例变量 c 时会调用 __getattr__ 方法;当 __init__ 被调用的时候,赋值运算也会调用 __setattr__ 方法。

[6] >>>> 迭代器对象: __iter____next__ 重载

前面我们知道,Python 中的迭代,可以直接通过重载 __getitem__ 方法来实现,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Indexer:
data = [1, 2, 3, 4, 5]
def __getitem__(self, index):
return Indexer.data[index]

test = Indexer()
for item in test:
print(item)

# 1
# 2
# 3
# 4
# 5

通过上面的方法是可以实现迭代,但并不是最好的方式。

事实上,Python 中的迭代操作会优先尝试调用 __iter__ 方法,再尝试 __getitem__。迭代环境(for in)是通过 iter() 去尝试寻找 __iter__ 方法来实现,而这种方法返回一个迭代器对象。如果 __iter__ 方法已经提供,Python 会重复调用迭代器对象的 next() 方法,直到发生 StopIteration 异常;如果没有找到 __iter__,Python 才会尝试使用 __getitem__ 机制。下面看一下迭代器的例子:

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
43
44
45
class Next():
def __init__(self, data=1):
self.data = data

def __iter__(self):
print("__iter__ called")
return self

def __next__(self):
print("__next__ called")
if self.data >5:
raise StopIteration
else:
self.data += 1
return self.data

for inx in Next(3):
print(inx)
print("---------")
# __iter__ called
# __next__ called
# 4
# __next__ called
# 5
# __next__ called
# 6
# __next__ called
# ---------

n = Next(3)
i = iter(n)
while True:
try:
print(next(i))
except Exception as e:
break

# __iter__ called
# __next__ called
# 4
# __next__ called
# 5
# __next__ called
# 6
# __next__ called

可见实现了 __iter____next__ 方法后,可以通过 for in 的方式迭代遍历对象,也可以通过 iter()next() 方法迭代遍历对象。


[7] >>>> 自定义序列示例

基于前面介绍的方法重载,我们来实现一个自定义的序列类。

下面列出了和自定义序列类有关的几个特殊方法:

__len__(self) 返回序列类中存储元素的个数。
__contains__(self, value) 判断当前序列中是否包含 value 这个指定元素。
__getitem__(self, key) 通过指定的 key(键),返回对应的 value(值)。
__setitem__(self, key, value) 修改指定 key(键)对应的 value(值)。
__delitem__(self, key) 删除指定键值对。

重写原则 >>>>

1)在上表中的这些特殊方法进行重写时,在实现其基础功能的基础上,还可以根据实际情况,对各个方法的具体实现进行适当调整。

__setitem__() 方法为例,当在序列中未找到指定 key 的情况下,该方法可以报错,当然也可以将此键值对添加到当前序列中。

2)在实现自定义序列类时,并不是必须重写表中全部的特殊方法。

例如如果该自定义序列是一个不可变序列(即序列中的元素不能做修改),则无需重写 __setitem__()__delitem__() 方法;反之,如果该自定义序列是一个可变序列,可以重写以上五个特殊方法。

下面实现一个比较简单的序列类(字典类),其特点是只能存储 int 类型的元素:

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
43
44
45
46
47
class IntDict:

def __init__(self):
# 定义用于存储数据的字典
self.__data = {}

def __len__(self):
return len(list(self.__data.values()))

def __getitem__(self, key):
if key in self.__data:
return self.__data[key]
return None

def __setitem__(self, key, value):
if not isinstance(value, int):
raise TypeError("Not Int Type")
self.__data[key] = value

def __delitem__(self, key):
if key in self.__data: del self.__data[key]

dic = IntDict()
# 输出序列中元素的个数,调用 __len__() 方法
print(len(dic))

# 向序列中添加元素,调用 __setitem__() 方法
dic['a'] = 1
dic['b'] = 2
print(len(dic))

dic['a'] = 3
dic['c'] = 4
print(dic['a'])

# 删除指定元素,调用 __delitem__() 方法
del dic['a']
print(dic['a'])
print(len(dic))


# Output:
# 0
# 2
# 3
# None
# 2

Python 中的迭代器

我们已经知道,字符串(str)、列表(list)、元组(tuple)、字典(dict)、集合(set)等序列容器有一个共同的特性:它们都支持使用 for in 循环遍历存储的元素。

可迭代对象(Iterable)

事实上,像字符串(str)、列表(list)、元组(tuple)、字典(dict)、集合(set)这些可以直接作用于 for in 循环的容器对象,都可以统称为可迭代对象(Iterable object)。

关于 Python 中的可迭代对象更多说明请参见:Python 中的可迭代对象(Iterable)

我们可以通过 collections 模块中的 Iterable 类型来判断一个对象是否是可迭代对象,具体判断方法如下:

1
2
3
4
5
6
7
8
9
10
11
>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

那么,可迭代对象(Iterable)和迭代器(Iterator)是否是一回事儿?答案肯定是不是的,不然废话这么多干嘛?!!


何为迭代器(Iterator)

从字面来理解,迭代器指的是:支持迭代的容器。这里的容器可以是列表、元组等 Python 内置的基础容器,也可以是用户自定义的容器类对象,只要该容器支持迭代即可。

你可能会问,既然都是支持迭代的容器,那么迭代器(Iterator)和可迭代对象(Iterable)的差别在哪里???

[1] >>>> 迭代器特性

更确切的说,迭代器(Iterator) 具有如下特性:

  • 支持迭代的(for in 遍历),可以记住遍历位置的容器;
  • 迭代器对象从容器的第一个元素开始访问,直到所有的元素被访问完结束,并且迭代器访问只能往前无法后退;
  • 迭代器具有两个基本的方法:iter() && next()
  • 迭代器都是可迭代对象。

上述列出的这些特性,都可以在下文给出的迭代器中表现出来。

[2] >>>> 判断对象是否属于迭代器

那么,如何判断一个对象是否属于 Iterator呢?可以借助 connections 模块中的 Iterator:

1
2
3
4
5
6
7
8
9
10
from collections.abc import Iterator
print(isinstance((x for x in range(10)), Iterator))
print(isinstance([], Iterator))
print(isinstance({}, Iterator))
print(isinstance('abc', Iterator))

# True
# False
# False
# False

可以看到,列表、字典、以及字符串等确实不是迭代器。并且 listdictstr 虽然是 Iterable,却不是 Iterator(不存在 next() 函数)。

[3] >>>> next && iter 方法说明

1)–> iter()

iter() 函数,可以将 listdictstr Iterable 对象变为 Iterator,从而创建迭代器:

1
2
3
4
5
6
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
>>> isinstance(iter({}), Iterator)
True

2)–> next()

迭代器容器对象可以被 next() 函数调用,并不断返回下一个元素值。元素访问完后,继续调用 next() 函数会产生 StopIteration Error。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
list=[1,2,3,4]
it = iter(list) # 创建迭代器对象

while True:
try:
print (next(it))
except StopIteration:
print("Iterator Occur StopIteration Error")
break

# 遍历后无法重新遍历出元素(只能向前访问)
for item in it:
print(item)

# 这里会报错:StopIteration,表示容器中元素已经遍历完了
print(next(it))

# Output:
# 1
# 2
# 3
# 4
# Iterator Occur StopIteration Error

[4] >>>> 迭代器对象打印

注意,直接打印迭代器对象是无法查看迭代器容器中元素值的,你需要根据容器数据结构使用 Python 内置的 list() && tuple() && dict() 等来将其转化成可打印的形式:

1
2
3
4
5
6
7
8
list=[1,2,3,4]
it = iter(list) # 创建迭代器对象

print(it)
print(tuple(it))

# <list_iterator object at 0x0000017949EA36D8>
# (1, 2, 3, 4)

如何定义迭代器容器

这里给出两种迭代器的实现思路:

  1. 运算符重载方法:通过重写类的 __next__() && __iter__() 方法实现自定义迭代器类;
  2. 内置函数方法:通过内置的 iter() 迭代器函数实现迭代器。

[1] >>>> 自定义迭代器类

如果要自定义实现一个迭代器,则类中必须实现如下两个方法:

  • __next__(self):返回容器的下一个元素;
  • __iter__(self):该方法返回一个迭代器(iterator)对象,该对象实现了 __next__() 方法并通过 StopIteration 异常来标识迭代的完成。

事实上,在运算符重载中已经实现过一个简单的迭代器了,推荐返回重新认识一下。

例如,下面程序自定义了一个简易的列表容器迭代器,支持迭代访问(对照迭代器特性进行学习):

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
class listDemo:
def __init__(self):
self.__date=[] # 用于存储迭代器容器元素
self.__step = 0 # 记录迭代器遍历位置,用于 StopIteration 异常判断

def __next__(self):
if self.__step <= 0:
raise StopIteration
self.__step -= 1
# 返回下一个元素
return self.__date[self.__step]

def __iter__(self):
# 实例对象本身就是迭代器对象,因此直接返回 self 即可
return self

# 用于向容器中添加元素
def __setitem__(self,key,value):
self.__date.insert(key,value)
self.__step += 1

mylist = listDemo()
mylist[0] = 1
mylist[1] = 2

# 判断是否为迭代器:
from collections.abc import Iterator
print(isinstance(mylist, Iterator))
# True

for i in mylist:
print (i)

# 2
# __next__ called
# 1
# __next__ called
# __next__ called

StopIteration 说明 >>>>

迭代环境下,StopIteration 异常用于标识迭代的完成,防止出现无限循环的情况。

上面程序遍历(for in)时,会自动调用 __next__() 方法来返回迭代器容器中的下个元素直至发生 StopIteration,故需要在 __next__() 方法中设置在完成指定循环条件后触发 StopIteration 异常来结束迭代,这是必要的。


[2] >>>> 内置 iter() 迭代器函数

Python 内置的 iter() 函数可以将对象转化为一个迭代器(返回一个迭代器对象),该函数的语法格式如下:

1
iter(obj[, sentinel])

其中,obj 必须是一个可迭代的容器对象,而 sentinel 作为可选参数,如果使用此参数,要求 obj 必须是一个可调用对象,具体功能后面会讲。

关于可调用对象,指的是该类的实例对象可以像函数那样,直接以 对象名() 的形式被使用。通过在类中添加 __call__() 方法,就可以将该类的实例对象编程为可调用对象(参见博文第一章节相关内容)。

iter() 函数的两种使用形式 >>>>

1)–> iter(obj)

我们常用的是仅有第一个参数的 iter() 函数,即通过传入一个可迭代对象,iter() 函数会返回一个迭代器对象。

你可以通过调用该迭代器中的 __next__() 方法即可实现迭代(或者 for in)。例如:

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
mylist = [1, 2, 3]

# 将可迭代的列表转换为迭代器
myIter = iter(mylist)

print(type(mylist))
print(type(myIter))
# <class 'list'>
# <class 'list_iterator'>

# 依次获取迭代器的下一个元素
print(myIter.__next__())
print(myIter.__next__())
print(myIter.__next__())
# 1
# 2
# 3

# 通过 for in 进行遍历:
for item in iter(mylist):
print(item, end='->')
# 1->2->3->

# 迭代器访问结束后,继续访问会产生 StopIteration 异常
print(myIter.__next__())
# Traceback (most recent call last):
# File ".code.tio", line 7, in
# print(item, end='->')
# StopIteration

可以看到,当迭代完存储的所有元素之后,如果继续迭代,则 __next__() 方法会抛出 StopIteration 异常。

另外,你也可以使用 next() 内置函数来迭代,即 next(myIter),和 __next__() 方法是完全一样的。


2)–> iter(obj[, sentinel])

如果同时使用 iter() 函数中的两个参数,则要求第一个 obj 参数必须传入可调用对象(可以不支持迭代)。

这样,当使用 iter() 函数返回的迭代器对象调用 __next__() 方法时,它会通过执行 obj() 调用 __call__() 方法来返回元素。

如果此时,__call__() 方法的返回值和第 2 个参数值相同,则输出 StopInteration 异常;反之,则输出 __call__() 方法的返回值。

例如,修改 listDemo 类如下所示:

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
class listDemo:
def __init__(self):
self.__date=[]
self.__step = 0

def __setitem__(self,key,value):
self.__date.insert(key,value)
self.__step += 1

# 使该类实例对象成为可调用对象
def __call__(self):
self.__step -= 1
return self.__date[self.__step]

mylist = listDemo()
mylist[0]=1
mylist[1]=2
mylist[2]=3

# 将 mylist 变为迭代器
test = iter(mylist, 4)
print(test)
# <callable_iterator object at 0x000001F22F8FC860>

print(next(test)) # 实际调用 __call__() 方法
print(next(test))
print(next(test))
# 3
# 2
# 1

print(next(test))
# StopIteration Error

3)–> iter(obj[, sentinel]) 实用场景

iter(obj[, sentinel]) 形式常用来构建块读取器。

例如,从二进制数据库文件中读取固定宽度的块,直至到达文件的末尾:

1
2
3
4
5
from functools import partial

with open('mydata.db', 'rb') as f:
for block in iter(partial(f.read, 64), b''):
process_block(block)

[3] >>>> 自定义字典迭代器 Demo

运算符重载小节最后,我们实现了一个自定义字典类序列,该字典是一个可迭代对象,但该字典对象并不支持良好的迭代(死循环),这里我们来将其升级为一个良好的迭代器。

尽管上面的自定义字典类序列中重写了 __getitem__ 方法,但仍然是不支持良好迭代的,你可以尝试下~~~

这里,基于上述知识,我们来完善一下上一小节中的自定义字典序列:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class IntDict:

def __init__(self):
# 定义用于存储数据的字典
self.__data = {}
self.__step = 0
self.__key_record = []

def __len__(self):
return len(list(self.__data.values()))

def __getitem__(self, key):
if key in self.__data:
return self.__data[key]
return None

def __setitem__(self, key, value):
if not isinstance(value, int):
raise TypeError("Not Int Type")
self.__data[key] = value
if key not in self.__key_record:
self.__key_record.append(key)
self.__step += 1

def __delitem__(self, key):
if key in self.__data:
del self.__data[key]
del self.__key_record[self.__key_record.index(key)]
self.__step -= 1

def __iter__(self):
return self
def __next__(self):
if self.__step <= 0:
raise StopIteration
self.__step -= 1
return (self.__key_record[self.__step], self.__data[self.__key_record[self.__step]])

dic = IntDict()
# 输出序列中元素的个数,调用 __len__() 方法
print(len(dic))
# 0

# 判断是否为迭代器:
from collections.abc import Iterator
print(isinstance(dic, Iterator))
# True

# 向序列中添加元素,调用 __setitem__() 方法
dic['Google'] = 1
dic['Edge'] = 2
print(len(dic))
# 2

dic['Firfox'] = 3
dic['Google'] = 4
print(dic['Google'])
# 4

# 删除指定元素,调用 __delitem__() 方法
del dic['Google']
print(dic['Google'])
print(len(dic))
# None
# 2

# 字典遍历,调用 __next__() 方法
for key,value in dic:
print(key, value)

# Firfox 3
# Edge 2

enumerate 迭代器

enumerate 方法可以用来实现下标循环 >>>>

很多情况下,我们想要对 listtuple 实现类似 Java 那样的下标循环怎么办?

Python 中内置的 enumerate 函数可以把一个 list (tuple)变成 index-element(索引-元素)对,这样就可以在for循环中同时迭代索引和元素本身。

先来给出 enumerate 内置函数的语法格式:

1
enumerate(iterable, start=0)

其中,iterable 表示必须是可迭代的对象,start 表示下标开始的序号,该函数返回一个可迭代的 enumerate 对象(非枚举对象),index-element 以元组形式保存在迭代器中。

来看一个示例:

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
seasons = ['Spring', 'Summer', 'Fall', 'Winter']

seasons_enum_1 = enumerate(seasons)

# 判断是否为枚举对象
from enum import Enum
type(Enum)
print(isinstance(seasons_enum_1, Enum))
# False
# 判断是否为迭代器:
from collections.abc import Iterator
print(isinstance(seasons_enum_1, Iterator))
# True

for index, elem in seasons_enum_1:
print(index, elem)
print("-----------------")
seasons_enum_2 = enumerate(seasons, 3)
for index, elem in seasons_enum_2:
print(index, elem)

# Output:
# <enumerate object at 0x000002B6C69AA438>
# False
# True
# 0 Spring
# 1 Summer
# 2 Fall
# 3 Winter
# -----------------
# 3 Spring
# 4 Summer
# 5 Fall
# 6 Winter

你无法直接查看可迭代的 enumerate 枚举对象中的 index-element 信息:

1
2
print(seasons_enum)
<enumerate object at 0x0000021C934C4CF0>

但我们可以通过使用 Python 内置的 list() && tuple() && dict() 来将其转化成可打印的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
seasons = ['Spring', 'Summer', 'Fall', 'Winter']

seasons_list = list(enumerate(seasons))
print(seasons_list)
# [(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]

seasons_tup = tuple(enumerate(seasons))
print(seasons_tup)
# ((0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter'))

seasons_dict = dict(enumerate(seasons))
print(seasons_dict)
# {0: 'Spring', 1: 'Summer', 2: 'Fall', 3: 'Winter'}

这样,你就可以以 List && Tuple && Dict 的形式进行枚举对象数据的访问了。


深入解读迭代器

我们知道,迭代器的主要功能就是进行容器元素的迭代(遍历),有了上面的知识,我们就可以重新认识一下 for 循环了:

For 循环解读

事实上,Python 中的迭代操作会优先尝试调用 __iter__ 方法,再尝试 __getitem__

迭代环境(for ... in ... 或者 next(Iterator))是通过 iter() 去尝试优先寻找 __iter__ 方法来实现,而这种方法返回一个迭代器对象。

如果 __iter__ 方法已经提供,Python 会重复调用迭代器对象的 next() 方法,直到发生 StopIteration 异常;如果没有找到 __iter__,Python 才会尝试使用 __getitem__ 机制。

示例演示:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class listDemo:
def __init__(self):
self.__date=[] # 用于存储迭代器容器元素
self.__step = 0 # 记录迭代器遍历位置,用于 StopIteration 异常判断

def __next__(self):
print("__next__ called")
if self.__step <= 0:
raise StopIteration
self.__step -= 1
# 返回下一个元素
return self.__date[self.__step]

def __iter__(self):
print("__iter__ called")
# 实例对象本身就是迭代器对象,因此直接返回 self 即可
return self

# 用于向容器中添加元素
def __setitem__(self,key,value):
self.__date.insert(key,value)
self.__step += 1

mylist1 = listDemo()
mylist1[0] = 1
mylist1[1] = 2
mylist1[2] = 3
for item in mylist1:
print(item)

print("----------")

mylist2 = listDemo()
mylist2[0] = 4
mylist2[1] = 5
mylist2[2] = 6
iter_list = iter(mylist2)
while True:
try:
print(next(iter_list))
except Exception as e:
break

# Output:
# __iter__ called
# __next__ called
# 3
# __next__ called
# 2
# __next__ called
# 1
# __next__ called
# ----------
# __iter__ called
# __next__ called
# 6
# __next__ called
# 5
# __next__ called
# 4
# __next__ called

可以看到,使用 for ... in ... 循环方式进行遍历和在 while 循环内使用 next(Iterator) 是等价的。

因此,对于迭代器容器中的元素遍历,支持三种方式:

  1. for … in …;
  2. while 循环结构内使用 next(Iterator) 或者 Iterator.__next__(),捕获 StopIteration 异常后停止;
  3. 借助 Python 内置的 list() && tuple() && dict() && set() 等转化为相应基本数据类型,然后进行遍历。

关于 __getitem__ 的迭代(循环)样例这里就不重复给出了,你可以参见第一个版本的自定义字典序列 Demo,或者参见运算符重载部分关于迭代器的重载部分。


Iterator 数据流

你可能还对 listdictstr等序列容器不是 Iterator 感到很困惑。

事实上,Python 的 Iterator 对象表示的是一个数据流,Iterator 对象可以被 next() 函数调用并不断返回下一个数据,直到没有数据时抛出 StopIteration 错误。

这也就意味着,在 Iterator 数据流向前迭代过程中,可以看作是容器内数据流出的过程,表现为当前对象无法再通过迭代方式获取数据了(已经流完了)。

Iterator 数据流 可以看作是惰性的计算序列 >>>>

可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next() 函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator 甚至可以表示一个无限大的数据流,例如全体自然数。而使用 list 是永远不可能存储全体自然数的。


Python 中的生成器

上一小节我们解读了什么是迭代器。事实上,生成器本质上也是迭代器,不过它比较特殊。

那么,相较于一般的迭代器,生成器特殊在哪里???

生成器(Generator)特性

参看前面我们创建的任意一个迭代器容器,你会发现,在使用该容器迭代一组数据时,必须事先将所有数据存储到容器中,才能开始迭代。而生成器(generator)却不同,它可以实现在迭代的同时生成元素(边迭代边计算元素)。

也就是说,对于某些特定的可迭代容器(如果其内部的元素是可以用某种算法推算得到的),将其编码成一个生成器对象,该生成器不会一次性生成容器中的所有元素,而是什么时候需要,才什么时候生成。

可能有同学就要问了,这样的机制什么用处呢???

想象一个容器内部元素可以用某种算法推算得到的场景 >>>>

例如前面通过列表推导式(解析式)直接创建一个列表,由于内存限制,列表容量肯定是有限的。假设要创建一个包含 10 万个元素的列表,这不仅占用很大的存储空间,如果程序中当前仅仅访问前面部分的元素,那后面绝大多数元素占用的空间都白白浪费了,这时生成器这种边迭代边推算的机制就会非常有用。

退一步来讲,就算容器中元素数量较少时,节省的内存有限,但使用生成器仍可以帮助我们获得一个方便操作的迭代器,这也是有益的。

使用生成器需要注意以下几点 >>>>

  • 生成器适用于容器内部元素可以用某种算法推算得到的场景;
  • 更适用于推算算法比较复杂时,用类似推导式(例如列表推导式)的 for 循环无法实现的场景;
  • 生成器是边迭代边计算元素的,元素什么时候需要,什么时候生成,用于节省内存空间;
  • 生成器是一个特殊的迭代器,支持迭代器中的相关使用规则。

并且,除了具有如上特性,生成器的创建方式也比迭代器简单很多。

生成器创建

Python 中提供了两种生成器的创建方式:1)带 yield 关键字的生成器函数;2)元组推导式(… for … in …)。

yield 关键字

使用带 yield 关键字的生成器函数来创建生成器分为以下两步:

  1. 定义一个以 yield 关键字标识返回值 的函数;
  2. 调用刚刚创建的函数,即可创建一个生成器。

Get Start >>>>

比如,构建一个著名的斐波那契数列(Fibonacci)容器,除第一个和第二个元素外,任意一个数都可由前两个数相加得到。类似于:

1
1, 1, 2, 3, 5, 8, 13, 21, 34, ...

斐波拉契数列用列表推导式写不出来(推导算法较复杂),但是,用函数把它打印出来却很容易:

1
2
3
4
5
6
7
8
9
10
11
12
13
def fibonc(max):
idx = 0
a, b = 0, 1
while idx < max:
print(b)
a, b = (b, a + b)
idx += 1

fibonc(4)
# 1
# 1
# 2
# 3

我们知道,斐波那契数列(Fibonacci)容器构建是符合生成器使用条件的,事实上上面的函数和 generator 仅一步之遥,即将 print(b) 打印函数改为 yield b 作为返回就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def fibonc(max):
print(" call fibonc generator ")
idx = 0
a, b = 0, 1
while idx < max:
yield b
a, b = (b, a + b)
idx += 1

fi = fibonc(4)
print(fi)
# <generator object fibonc at 0x0000012D76D41258>

from collections.abc import Iterator
print(isinstance(fi, Iterator))
# True

这样我们就成功创建了一个 fi 生成器对象。显然,和普通函数不同,fibonc(max) 函数的返回值用的是 yield 关键字,而不是 return 关键字,此类函数又称为生成器函数(generator function)。

注意,即便调用生成器函数,Python 解释器也不会执行函数中的代码,它只会返回一个生成器(对象)。并且,和 return 作为返回值关键字相比,yield 除了可以返回相应的值,还有一个更重要的功能,即每当程序执行完该语句时,程序就会暂停执行。


[1] >>>> 生成器元素访问

生成器对象创建好之后,如何使用生成器中的元素数据呢?

即,要想使生成器函数得以执行,或者想使执行完 yield 语句立即暂停的程序得以继续执行,怎么办?

同理于迭代器,有以下三种方式:

  • while 循环结构内使用 next(Iterator) 或者 Iterator.__next__() 遍历生成器,捕获 StopIteration 异常后停止;
  • 通过 for 循环遍历生成器;
  • 借助 Python 内置的 list() && tuple() && dict() && set() 等转化为相应基本数据类型,然后进行遍历。

示例如下:

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
43
44
45
46
47
48
def fibonc(max):
print(" call fibonc generator ")
idx = 0
a, b = 0, 1
while idx < max:
yield b
a, b = (b, a + b)
idx += 1

fi = fibonc(4)
print(fi)
# <generator object fibonc at 0x000002564CEE2BF8>
from collections.abc import Iterator
print(isinstance(fi, Iterator))
# True

# 调用 next() or __next__ 内置函数
while True:
try:
print(next(fi))
# print(fi.__next__())
except Exception as e:
break
# call fibonc generator
# 1
# 1
# 2
# 3

# 通过 for 循环遍历生成器
fi1 = fibonc(4)
for item in fi1:
print(item)
# call fibonc generator
# 1
# 1
# 2
# 3

# 通过 list() 转化
fi2 = fibonc(4)
for item in list(fi2):
print(item)
# call fibonc generator
# 1
# 1
# 2
# 3

[2] >>>> 生成器执行方式

给出一个例子来看生成器内部程序的执行机制:

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
def fibonc(max):
print("Begin to run")
idx = 0
a, b = 0, 1
while idx < max:
yield b
a, b = (b, a + b)
idx += 1
print("Continue to run")

print("Run End")

fi = fibonc(4)

# 调用 next() or __next__ 内置函数
print(next(fi))
# Begin to run
# 1
print(fi.__next__())
# Continue to run
# 1


# 通过 for 循环遍历生成器
for item in fi:
print(item)
# Continue to run
# 2
# Continue to run
# 3
# Continue to run
# Run End

1)首先,在创建有 fi 生成器的前提下,通过其调用 next() 内置函数,会使 Python 解释器开始执行 fibonc() 生成器函数中的代码,因此会输出 Begin to run,程序会一直执行到 yield b,而此时的 b==1,因此 Python 解释器输出 1。由于受到 yield 的影响,程序会在此处暂停。

2)然后,我们使用 fi 生成器调用 __next__() 方法,该方法的作用和 next() 函数完全相同(事实上,next() 函数的底层执行的也是 __next__() 方法),它会使得程序继续执行,即输出 Continue to run,程序又会执行到 yield b,此时 b==1,因此输出 1,然后程序暂停。

3)最后,我们使用 for 循环遍历 fi 生成器,之所以能这么做,是因为 for 循环底层会不断地调用 next() 函数,使暂停的程序继续执行,因此会输出后续的结果。

注意,在 Python 2.x 版本中不能使用 __next__() 方法,可以使用 next() 内置函数,另外生成器还有 next() 方法(即以 num.next() 的方式调用)。


元组推导式

另外对于不复杂的算法推算场景,即容器中元素可以使用推导式(解析式)生成。

Python 中支持一个很简单的 generator 创建方法就是,使用元组推导式,其返回一个生成器对象:

1
2
3
4
5
6
7
8
9
10
gen = (x for x in range(4))
print(gen)
# <generator object <genexpr> at 0x000002987E724F68>

for item in gen:
print(item)
# 0
# 1
# 2
# 3

关于 gen 中元素遍历方式,请参见上一小节提供的三种方式。


素数示例

一个生成器函数中是可以包含多个 yield 关键字作为返回的,这和 return 关键字是类似的。我们将以实现一个以素数作为元素的生成器容器进行展示。

我们知道,素数就是质数,是一个大于 1 的自然数,且除了 1 和它本身外,不再有其他的因数。

素数都有哪些? >>>> 计算素数的一个方法是埃氏筛法,它的算法理解起来非常简单:

首先,列出从 2 开始的所有自然数,构造一个序列:

2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …

取序列的第一个数 2,它一定是素数,然后用 2 把序列的 2 的倍数筛掉:

3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …

取新序列的第一个数 3,它一定是素数,然后用 3 把序列的 3 的倍数筛掉:

5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …

取新序列的第一个数5,然后用5把序列的5的倍数筛掉:

7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …

这样不断筛下去,就可以得到所有的素数。

Python 编程实现过程 >>>>

先来看三个核心的函数功能实现:

1)–> 先构造一个从 3 开始的奇数序列(注意这是一个生成器,并且是一个无限序列),作为初始序列:

1
2
3
4
5
def __oddSeq_generator(self):
n = 1
while True:
n += 2
yield n

2)–> 然后定义一个倍数筛查函数:

1
2
def __not_divisible(self, n):
return lambda x: x % n > 0

3)–> 定义一个生成器,不断返回下一个素数:

1
2
3
4
5
6
7
def __primes(self):
yield 2
it = self.__oddSeq_generator() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(self.__not_divisible(n), it) # 构造新序列

类封装后,最终如下:

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
class PrimesCalc:

def __init__(self, number):
self.__primes_list = []
self.__generate_flag = False
self.number = number

def __oddSeq_generator(self):
n = 1
while True:
n += 2
yield n

def __not_divisible(self, n):
return lambda x: x % n > 0

def __primes(self):
yield 2
it = self.__oddSeq_generator() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(self.__not_divisible(n), it) # 构造新序列

def calc(self):
for item in self.__primes():
if (item <= self.number) & (not self.__generate_flag):
self.__primes_list.append(item)
else:
self.__generate_flag = True
break
return self.__primes_list

primes_obj70 = PrimesCalc(70)
res = primes_obj70.calc()
print(res)
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67]

primes_obj50 = PrimesCalc(50)
res1 = primes_obj50.calc()
print(res1)
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

生成器高级用法

本节将在上述基础上,继续讲解生成器的一些高级用法,会涉及到:send() && close() && throw() 三种生成器方法。

send 方法

我们知道,通过 for 循环以及 next() 或者 __next__() 等方法,可以实现从外界控制生成器的执行。除此之外,通过 send() 方法,还可以向生成器中传值。

需要注意的是,使用 send() 方法可选择带一个参数,也可以不带任何参数(用 None 表示)。

[1] >>>> send(None)

当使用不带参数的 send(None) 方法时,它和 next() 函数的功能完全相同。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
def intNum():
print("Start to run")
for i in range(5):
yield i
print("Continue to run")

num = intNum()
print(num.send(None))
# Start to run
# 0
print(num.send(None))
# Continue to run
# 1

注意,虽然 send(None) 的功能是 next() 完全相同,但更推荐使用 next(),不推荐使用 send(None)。


[2] >>>> send(value)

send(value) 具备 next() 函数的部分功能,即将暂停在 yield 语句出的程序继续执行;但与此同时,该函数还会将 value 值作为 yield 语句返回值赋值给接收者。

样例演示:

1
2
3
4
5
6
7
8
def foo():
buff_a = yield "hello"
buff_b = yield buff_a
yield buff_b

f = foo()
print(f.send("Python"))
# TypeError: can't send non-None value to a just-started generator

哎?报错了:”你无法传入一个非 None 的值给一个刚启动的生成器”,什么意思? >>>>

事实上,带参数的 send(value) 无法启动执行生成器函数。必须先传递一个 None 进去或者调用一次 next() 方法(启动生成器),才能进行传值操作。修改程序如下:

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
def foo():
buff_a = yield "hello"

print("buff_a -->", buff_a)
buff_b = yield "hello"

print("buff_b -->", buff_b)
buff_c = yield buff_b

print("buff_c -->", buff_c)
yield buff_c

f = foo()

print(next(f)) # 等价于 print(f.send(None))
# hello

print(f.send(None)) # 等价于 print(next(f))
# buff_a --> None
# hello

print(f.send("Python"))
# buff_b --> Python
# Python

print(f.send("Python is a OPP Program."))
# buff_c --> Python is a OPP Program.
# Python is a OPP Program.

分析一下程序的执行流程:

1)首先,构建生成器函数,并利用其创建生成器对象 f

2)使用 next() 函数启动生成器(其功能和 f.send(None) 函数完全相同),开始执行生成器函数,程序会执行到 yield "hello",而此时的值为 “hello”,因此 Python 解释器输出 hello。这里注意,此时还未对 buff_a 进行赋值。

3)使用生成器 f 调用无参的 send(None) 函数重新启动生成器,它会使得程序继续执行(buff_a 赋值 -> 打印 buff_a),程序执行到第二个 yield "hello" 语句停止(未对 buff_b 进行赋值),此时 Python 解释器输出 hello。同时,你会发现,在打印 buff_a 时,输出 buff_a --> None,思考一下为什么???

事实上,3)在 buff_a 进行赋值时,由于使用的是 send(None) 函数启动的生成器,其没有返回值(或者就认为它也会将 None 赋值给当前 yield 语句的接收者),这就很合理了。这是不进一步意味着 next() 函数也是同理的,你可以将 send(None) 语句换为 next() 验证一下。

4)开始使用生成器 f 调用有参的 send(value) 函数重新启动生成器,它会使得程序继续执行(buff_b 赋值 -> 打印 buff_b),程序执行到 yield buff_b" 语句停止(未对 buff_c 进行赋值),此时 Python 解释器输出 Python。这里,由于使用的是带 value 值的方式,故 buff_b 进行赋值时会拿到 "Python",故打印输出:buff_b --> Python

5)最后依然是使用生成器 f 调用有参的 send(value) 函数重新启动生成器,过程和 4)中同理,请结合理解一下。


close 方法

当程序在生成器函数中遇到 yield 语句暂停运行时,此时如果调用 close() 方法,会阻止生成器函数继续执行,该函数会在程序停止运行的位置抛出 GeneratorExit 异常。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
def foo():
try:
yield "Python"
except GeneratorExit:
print('捕获到 GeneratorExit,你可以在这里进行异常处理操作')

f = foo()
print(next(f))
# Python
f.close()
# 捕获到 GeneratorExit,你可以在这里进行异常处理操作

需要注意的是,生成器函数一旦使用 close() 函数停止运行,后续将无法再调用 next() 函数或者 __next__() 方法启动执行,否则会抛出 StopIteration 异常。例如:

1
2
3
4
5
6
7
8
9
10
11
def foo():
yield "Python"
print("close 后仍想要执行的代码")

f = foo()
print(next(f))
# Python

f.close()
next(f)
# StopIteration Error

这就意味着,close 后仍想要执行的代码无法执行了,怎么办?

可以通过通过捕获 GeneratorExit 异常,可以在异常处理中继续执行生成器函数中仍然想执行的代码。但是注意这部分代码中不能再包含 yield 语句,否则程序会抛出 RuntimeError 异常。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def foo():
try:
yield "Python"
except GeneratorExit:
print('捕获到 GeneratorExit,你可以在这里进行异常处理操作')
print('close 后仍想要执行的代码')

yield "Java"

f = foo()
print(next(f))
# Python

f.close()
# 捕获到 GeneratorExit,你可以在这里进行异常处理操作
# close 后仍想要执行的代码

# RuntimeError Traceback (most recent call last)
# ---> f.close()
# RuntimeError: generator ignored GeneratorExit

throw 方法

生成器 throw() 方法的功能是,在生成器函数执行暂停处,抛出一个指定的异常,之后程序会继续执行生成器函数中后续的代码,直到遇到下一个 yield 语句。

需要注意的是,如果到剩余代码执行完毕没有遇到下一个 yield 语句,则程序会抛出 StopIteration 异常。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def foo():
try:
yield 1
except ValueError:
print('捕获到 ValueError')

f = foo()
print(next(f))
# 1

f.throw(ValueError)
# 捕获到 ValueError
# Traceback (most recent call last):
# ...
# StopIteration

显然,一开始生成器函数在 yield 1 处暂停执行,当执行 throw() 方法时,它会先抛出 ValueError 异常,然后继续执行后续代码找到下一个 yield 语句,该程序中由于后续不再有 yield 语句,因此程序执行到最后,会抛出一个 StopIteration 异常。


Python 中的@函数装饰器

所谓函数装饰器(Decorator),是指 通过装饰器函数,在不修改原函数的前提下,来对函数的功能进行合理的扩充。

@函数装饰器引入

在前面的章节,我们已经学习了三种 Python 内置的函数装饰器,分别是:@staticmethod、@classmethod 和 @property。它们分别是基于 Python 内置的装饰器函数 staticmethod()、classmethod() 和 property() 来实现的。

如下样例:

1
2
3
4
5
class Test:

@classmethod
def foo(cls):
print("Class Method")

也就是说,在不修改原函数 foo(cls) 的前提下(默认为类的实例方法),我们使用了一个 @classmethod 的装饰器,使得 foo(cls) 函数成为了一个类方法(功能扩展)。其中,@classmethod 函数装饰器,是基于装饰器函数 classmethod() 实现的。

那么,到底什么是函数装饰器?装饰器函数又是如何定义的???

你需要了解函数装饰器的工作原理 >>>>


@函数装饰器工作原理

假设用 funA() 装饰器函数所对应的函数装饰器 @funA,去装饰 funB() 函数以实现扩展其功能。如下所示:

1
2
3
4
5
6
7
8
9
10
# funA 作为装饰器函数
def funA(fn):
#...
fn() # 执行传入的 fn 参数
#...
return '...'

@funA
def funB():
#...

实际上,上面程序完全等价于下面的程序:

1
2
3
4
5
6
7
8
9
10
def funA(fn):
#...
fn() # 执行传入的 fn 参数
#...
return '...'

def funB():
#...

funB = funA(funB) # 等价于 @funA

对比如上程序可以发现,,使用装饰器函数 funA() 去装饰另一个函数 funB(),其底层执行了如下 2 步操作:

  1. 将 funB 作为参数传给 funA() 函数(传递的是函数,即函数名,或函数引用);
  2. 将 funA() 函数执行完成的返回值反馈回 funB。

来看一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# funA 作为装饰器函数
def funA(fn):
print("2018-3-13 12:30:12")
fn() # 执行传入的 fn 参数
print("2018-3-13 12:30:15")

return "Decorator Return"

@funA
def funB():
print("Learning Python")

# 2018-3-13 12:30:12
# Learning Python
# 2018-3-13 12:30:15

观察函数装饰器返回 >>>>

在此基础上,如果在程序末尾添加如下语句(打印观察返回的 funB 是什么?):

1
2
print(funB)
# Decorator Return

可见,funB 从一个函数引用,变为了一个普通的变量引用。显然,你可以理解被 “@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西(取决于装饰器的返回值):

  • 如果装饰器函数的返回值为普通变量,那么被修饰的函数名就变成了变量名;
  • 如果装饰器返回的是一个函数的名称,那么被修饰的函数名依然表示一个函数。

带参数的@函数装饰器###

分析@函数装饰器工作原理可以发现,即当被修饰的 funB() 函数无参数时,可以直接将 funB 作为 funA() 的参数传入。

思考一下,如果被修饰的函数本身带有参数 funB(arg),那应该如何传值呢?即如何向装饰器函数传递一个带参数的函数???

解决方法很简单 >>>>

就是在装饰器函数(funA())中嵌套一个函数,该函数带有的参数个数和被装饰器修饰的函数相同。

[1] >>>> 被修饰函数带有参数

1
2
3
4
5
6
7
8
9
10
11
12
# funA 作为装饰器函数
def funA(func):
def wrapper(arg):
# 说明:函数对象中,有一个 `__name__ `属性,可以拿到函数的名字
print('%s %s' % (arg, func.__name__) )
return wrapper

@funA
def funB(arg):
print("funB call")

funB("test")

等价于如下:

1
2
3
4
5
6
7
8
9
10
11
12
# funA 作为装饰器函数
def funA(func):
def wrapper(arg):
# 说明:函数对象中,有一个 `__name__ `属性,可以拿到函数的名字
print('%s %s' % (arg, func.__name__) )
return wrapper

def funB(arg):
print("funB call")

funB = funA(funB)
funB("test")

显然,此时 funB() 函数被装饰器 funA() 修饰,funB 就被赋值为 wrapper。这意味着,虽然我们在程序显式调用的是 funB() 函数,但其实执行的是装饰器嵌套的 wrapper() 函数。


[2] >>>> 多个(≥ 2)函数被同一个装饰器函数修饰,这些函数带有的参数个数并不相等

解决方法很简单 >>>>

*args**kwargs 作为装饰器内部嵌套函数的参数,*args**kwargs 表示接受任意数量和类型的参数。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# funA 作为装饰器函数
def funA(func):
def wrapper(*arg, **kwarg):
func(*arg, **kwarg)
return wrapper

@funA
def funB(arg):
print("Learning", arg)

@funA
def funC(name, add):
print(name, add)

funB("Python")
funC("Learning", "Java")
# Learning Python
# Learning Java

你应该可以想到,当只有一个被修饰函数,且被修饰函数参数列表有任意多个时,也可以采用上述方法。


上面我们提到的,都是被修饰函数中的参数场景。那你有没有考虑过一个问题,如果装饰器函数本身带有参数的话怎么办???

[3] >>>> 函数装饰器中含参数

如果装饰器函数(decorator)本身就需要传入参数怎么办???

借鉴上面的方法,可以在装饰器函数(funA)的外面套一个外部函数,通过外部函数传入参数,但需要保证外部函数返回一个装饰器(decorator),这是必要的。

为了方便理解,我们将 funA 改名为 decorator,示例如下:

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
# externalFunc 作为外部函数
def externalFunc(text):

def decorator(func): # funA 作为装饰器函数

def wrapper(*arg, **kwarg): # wrapper 作为内嵌函数
print('%s %s:' % (text, func.__name__)) # 说明:函数对象中,有一个 `__name__ `属性,可以拿到函数的名字
func(*arg, **kwarg)
return wrapper

return decorator

@externalFunc('execute')
def funB(arg):
print("Learning", arg)

@externalFunc('execute')
def funC(name, add):
print(name, add)

funB("Python")
funC("Learning", "Java")
# execute funB:
# Learning Python
# execute funC:
# Learning Java

这种 3 层嵌套等价于如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# externalFunc 作为外部函数
def externalFunc(text):

def decorator(func): # funA 作为装饰器函数

def wrapper(*arg, **kwarg): # wrapper 作为内嵌函数
print('%s %s:' % (text, func.__name__)) # 说明:函数对象中,有一个 `__name__ `属性,可以拿到函数的名字
func(*arg, **kwarg)
return wrapper

return decorator

def funB(arg):
print("Learning", arg)

def funC(name, add):
print(name, add)

funB = externalFunc("execute")(funB)
funC = externalFunc("execute")(funC)

完整的@函数装饰器

先给出一个前面的,一般@函数装饰器的样例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# funA 作为装饰器函数
def funA(func):
def wrapper(*arg, **kwarg):
func(*arg, **kwarg)
return wrapper

@funA
def funB(arg):
print("Learning", arg)

@funA
def funC(name, add):
print(name, add)

funB("Python")
funC("Learning", "Java")
# Learning Python
# Learning Java

考虑一下,在以上基础上添加如下代码的输出效果:

1
2
print(funB.__name__)
print(funC.__name__)

可以看到,运行后函数名称输出的都是 wrapper,这不是我们预期的输出啊,既然访问的是 funB/funC,你应该输出相应的函数签名啊。

事实上,出现上述情况也不意外,因为装饰器函数返回的那个 wrapper() 函数名字就是 'wrapper'funB/funC 指向的实际上还是 wrapper() 函数的空间。所以需要把原始函数的 __name__ 等属性复制到 wrapper() 函数中,否则有些依赖函数签名的代码执行就会出错。

当然了,不需要编写 wrapper.__name__ = func.__name__ 这样的代码,Python functools 模块内置的 functools.wraps 就是干这个事的,所以,一个完整的 decorator 的写法如下:

1
2
3
4
5
6
7
8
import functools

def funA(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

或者针对带参数的 decorator:

1
2
3
4
5
6
7
8
9
10
import functools

def externalFunc(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

@函数装饰器嵌套

上面都是使用一个装饰器的情况,但实际上,Python 中的函数也支持多个装饰器,比如:

1
2
3
4
5
@funA
@funB
@funC
def fun():
#...

上面程序的执行顺序是里到外,所以它等效于下面这行代码:

1
fun = funA( funB( funC(fun) ) )

深入解读@函数装饰器

这一小节将通过实际工作中的几个例子,来加深对@函数装饰器的理解。

身份认证

首先是最常见的身份认证的应用。

这个很容易理解,举个最常见的例子,登录微信时,需要输入用户名密码,然后点击确认,这样服务器端便会查询你的用户名是否存在、是否和密码匹配等等。如果认证通过,就可以顺利登录;反之,则提示你登录失败。

再比如一些网站,你不登录也可以浏览内容,但如果你想要发布文章或留言,在点击发布时,服务器端便会查询你是否登录。如果没有登录,就不允许这项操作等等。

一个实现身份认证的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import functools
def authenticate(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
request = args[0]
# 如果用户处于登录状态
if check_user_logged_in(request):
# 执行函数 post_comment()
return func(*args, **kwargs)
else:
raise Exception('Authentication failed')
return wrapper

@authenticate
def post_comment(request):
pass

上面这段代码中,定义了装饰器 authenticate,函数 post_comment() 则表示发表用户对某篇文章的评论,每次调用这个函数前,都会先检查用户是否处于登录状态,如果是登录状态,则允许这项操作;如果没有登录,则不允许。


日志记录

日志记录同样是很常见的一个案例。

在实际工作中,如果你怀疑某些函数的耗时过长,导致整个系统的延迟增加,想在线上测试某些函数的执行时间,那么,装饰器就是一种很常用的手段。

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
import functools
def log_execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
end = time.perf_counter()
print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
return res
return wrapper

@log_execution_time
def calculate_similarity(items):
pass

calculate_similarity("test")
# calculate_similarity took 0.00039999999999999996 ms

这里,装饰器 log_execution_time 记录某个函数的运行时间,并返回其执行结果。如果你想计算任何函数的执行时间,在这个函数上方加上 @log_execution_time 即可。


输入合理性检查

在大型公司的机器学习框架中,调用机器集群进行模型训练前,往往会用装饰器对其输入(往往是很长的 json 文件)进行合理性检查。这样就可以大大避免输入不正确对机器造成的巨大开销。

示例演示:

1
2
3
4
5
6
7
8
9
import functools
def validation_check(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
... # 检查输入是否合法

@validation_check
def neural_network_training(*args, **kwargs):
pass

很多情况下都会出现输入不合理的现象。因为我们调用的训练模型往往很复杂,输入的文件有成千上万行,很多时候确实也很难发现。

试想一下,如果没有输入的合理性检查,很容易出现“模型训练了好几个小时后,系统却报错说输入的一个参数不对,成果付之一炬”的现象。这样的“惨案”,大大减缓了开发效率,也对机器资源造成了巨大浪费。


缓存装饰器

关于缓存装饰器的用法,其实十分常见,这里以 Python 内置的 LRU cache 为例来说明。

LRU cache,在 Python 中的表示形式是 @lru_cache。@lru_cache 会缓存进程中的函数参数和结果,当缓存满了以后,会删除最近最久未使用的数据。

正确使用缓存装饰器,往往能极大地提高程序运行效率。举个例子,大型公司服务器端的代码中往往存在很多关于设备的检查,比如使用的设备是安卓还是 iPhone,版本号是多少。这其中的一个原因,就是一些新的功能,往往只在某些特定的手机系统或版本上才有(比如 Android v200+)。

这样一来,我们通常使用缓存装饰器来包裹这些检查函数,避免其被反复调用,进而提高程序运行效率,比如写成下面这样:

1
2
3
@lru_cache
def check(param1, param2, ...) # 检查用户设备类型,版本号等等
...

Author

Waldeinsamkeit

Posted on

2018-01-13

Updated on

2022-04-05

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.