Python3入门到精通——枚举与闭包

作者: Daniel Meng

GitHub: LibertyDream

博客:明月轩

本系列教程采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议

枚举

枚举的定义

编程的时候,为了表示有限的类型,我们可以使用唯一的数字标识并赋予其意义,但缺点是可阅读性很差。除非双方有共识,不然别人很难通过 if type = 1 这样的语句明白你想要表达什么。

为了便捷、安全地表达有限个类型,Python 提供了枚举类应对这一需求。强调一下,其他编程语言中枚举通常是作为一个数据类型提供的,Python 中枚举是个类。

想要使用枚举,需要从模块 enum 中导入 Enum 类,自定义一个类继承 Enum,不同的类型值通过定义类变量实现,类变量值的类型不限定。类型名称要大写

In [1]:
from enum import Enum
In [2]:
class VIP(Enum):
    '''QQ钻石会员'''
    RED = 1  # 变量名要大写
    YELLOW = 2
    GREEN = 3
    BLACK = 4
    PINK = 5
In [3]:
class City(Enum):
    
    BEIJING = 'Beijing' # 值类型不限
    SHANGHA = 'Shangha'
    SHENZHEN = 'Shenzhen'
    GUANGZHOU = 'Guangzhou'

使用枚举和访问类变量方式相同,class_name.type_name。注意,访问枚举类型的得到的就是类型名本身,而不是类变量的值,这也正是枚举类的意义所在。

In [4]:
print(VIP.RED)
VIP.RED
In [15]:
class Fake_VIP(object):
    RED = 1
    
print(Fake_VIP.RED) # 得到的是类变量的值
1

有人或许会说可以使用字典 key:value 表示类型,但字典和定义类变量最大的问题是不安全,它们都可以随意修改“类型值”

In [6]:
Fake_VIP.RED = 1 # 普通的赋值操作
VIP.RED = 9 # 枚举类型不允许修改类型值
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-bd41f76a69e5> in <module>
      1 Fake_VIP.RED = 1 # 普通的赋值操作
----> 2 VIP.RED = 9 # 枚举类型不允许修改类型值

~\AppData\Local\Continuum\anaconda3\envs\python37\lib\enum.py in __setattr__(cls, name, value)
    384         member_map = cls.__dict__.get('_member_map_', {})
    385         if name in member_map:
--> 386             raise AttributeError('Cannot reassign members.')
    387         super().__setattr__(name, value)
    388 

AttributeError: Cannot reassign members.

属性访问

通过 enum.type_name.name 获取枚举类型的名称,得到的是名称字符串。通过 enum.type_name.value 获取类型值

In [16]:
class Call(Enum):
    
    COP = 110
    FIRE = 119
    ICU = 120
In [17]:
Call.COP.name
Out[17]:
'COP'
In [22]:
Call.COP.value
Out[22]:
110
In [19]:
print(type(Call.COP.name))  # 类型名称字符串
print(type(Call.COP))  # 得到的是枚举类型
<class 'str'>
<enum 'Call'>

如果知道类型名称可以通过类型名称获取对应的枚举类型

In [23]:
Call['COP']
Out[23]:
<Call.COP: 110>

枚举类型可以遍历

In [30]:
for type_name in Call:
    print(type_name)
Call.COP
Call.FIRE
Call.ICU

类型比较

枚举类型支持身份比较(is)和等值比较(==),不支持大小比较

In [26]:
class Num(Enum):
    ONE = 1
    TWO = 2
    THREE = 3
    FOUR = 1
In [25]:
Num.ONE == Num.TWO
Out[25]:
False
In [27]:
Num.ONE == Num.FOUR
Out[27]:
True
In [28]:
1 is Num.ONE
Out[28]:
False
In [29]:
Num.TWO is Num.TWO
Out[29]:
True

注意到 Num.FOUR 的值和 Num.ONE 相同,此时前者会被当作后者的别名,遍历时也不会显现

In [31]:
print(Num.FOUR)  # 别名
Num.ONE
In [32]:
for type_name in Num:
    print(type_name)  # 没有 Num.FOUR
Num.ONE
Num.TWO
Num.THREE

如果想要将别名输出,可以访问枚举类下的成员变量__members__

In [34]:
for type_name in Num.__members__:
    print(type(type_name), type_name)
<class 'str'> ONE
<class 'str'> TWO
<class 'str'> THREE
<class 'str'> FOUR

数据库类型转换

枚举类型在数据库中存储的通常是类型值,这样不可避免的在编程时需要把从数据库读来的数据转换成枚举类型,这一步可以通过构造函数实现

In [36]:
type_name = Num(1)
print(type(type_name), type_name)
<enum 'Num'> Num.ONE
In [37]:
type_name == Num.ONE
Out[37]:
True

IntEnum

继承 Enum 得到的枚举类不限定类型值,甚至可以混用,如果希望值的类型全部统一成数字,可以使用 IntEnum。

In [40]:
from enum import IntEnum

class VIP(IntEnum):
    '''QQ钻石会员'''
    RED = 1 
    YELLOW = 2
    GREEN = 3
    BLACK = 'black' # 会报错
    PINK = 5
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-40-e6fc0227351f> in <module>
      1 from enum import IntEnum
      2 
----> 3 class VIP(IntEnum):
      4     '''QQ钻石会员'''
      5     RED = 1

~\AppData\Local\Continuum\anaconda3\envs\python37\lib\enum.py in __new__(metacls, cls, bases, classdict)
    216                     enum_member._value_ = value
    217             else:
--> 218                 enum_member = __new__(enum_class, *args)
    219                 if not hasattr(enum_member, '_value_'):
    220                     if member_type is object:

ValueError: invalid literal for int() with base 10: 'black'

此外,如果希望类型值彼此互斥,保证唯一性。可以借助 unique 触发器

In [41]:
from enum import Enum, unique

@unique
class Num(Enum):
    ONE = 1
    TWO = 2
    THREE = 3
    FOUR = 1  # 会报错
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-41-1cbb674b5a0a> in <module>
      2 
      3 @unique
----> 4 class Num(Enum):
      5     ONE = 1
      6     TWO = 2

~\AppData\Local\Continuum\anaconda3\envs\python37\lib\enum.py in unique(enumeration)
    867                 ["%s -> %s" % (alias, name) for (alias, name) in duplicates])
    868         raise ValueError('duplicate values found in %r: %s' %
--> 869                 (enumeration, alias_details))
    870     return enumeration
    871 

ValueError: duplicate values found in <enum 'Num'>: FOUR -> ONE

闭包

一切皆对象

在 C#、JAVA 这类种语言中,函数只是一段可执行的代码,一经编译就被固化下来。Python 中一切皆对象,函数也不例外。这意味着可以将函数当做参数进行传递、返回和调用。

In [14]:
def call(a):
    return 'call %s' % a

c = call  # 把函数赋值给变量
print(c('me'), type(c))
call me <class 'function'>

既然函数只是一个对象,那么在函数内部定义函数就可以理解为在一个函数内创建了一个对象,只不过这个对象是一个函数而已。

闭包概念

现象一

In [25]:
def get_y():
    slope = 2
    def one_dim(x):  # 函数内部定义一个函数,且接受一个参数
        return slope*x + 1
    return one_dim  # 函数作为参数返回

因为可见性不同,外部不能直接调用函数one_dim,但是可以通过调用get_y间接使用one_dim

In [24]:
y = get_y() 
y(2)
Out[24]:
5

这里实际上y = one_dim,y(2) 等价于 one_dim(2), 计算 2 × 2 + 1 = 5

现象二

如果get_y内部没有给定斜率,我们在外部进行定义,按照链式作用规则,我们依然可以得到相同结果

In [27]:
slope = 2 

def get_y():
    def one_dim(x):  
        return slope*x + 1 # 内部没有会向上一级寻找
    return one_dim

y = get_y()
y(2)
Out[27]:
5

现象三

如果内外同时定义了斜率,但是值不相同,会发生什么呢?

In [28]:
def get_y():
    slope = 2
    def one_dim(x):  
        return slope*x + 1 # 内部没有会向上一级寻找
    return one_dim

slope = 4 
y = get_y()
y(2)
Out[28]:
5

结果似乎有些奇怪,按理说 y 就是 one_dim,计算 y(2) 时因为内部没有斜率slope进而向外部访问。但这里没有访问与y同级的斜率slope=4,而是使用了one_dim定义时同级的斜率slope=2,这种函数定义和定义时存在的变量(环境变量)彼此绑定在一起的状态就叫做闭包,即闭包 = 函数 + 环境变量,环境变量不能是全局变量

get_y返回one_dim时,返回的不只是函数对象,实际上返回的是闭包,可以通过内部变量__closure__查看

In [29]:
y.__closure__
Out[29]:
(<cell at 0x000001CF35BDC318: int object at 0x00007FFEB2C9A1B0>,)
In [32]:
# 查看闭包内的环境变量
y.__closure__[0].cell_contents
Out[32]:
2

闭包的意义

闭包的意义在于将函数定义时的现场保存了下来,这样在函数调用时可以免受外部干扰,保证运行结果的正确。也正因为是和环境绑定,环境不同闭包也就不同,闭包不能由单一函数决定

In [34]:
def get_y():
    slope = 3
    def one_dim(x):  # 一个闭包
        return slope*x + 1

def get_k():
    slope = 4
    def one_dim(x): # 另一个闭包
        return slope*x + 1

闭包的关键在于函数调用外部的环境变量,如果函数内部定义了同名变量,Python 会将其视作局部变量进行访问、返回,不再是闭包。闭包与是否返回值无关

In [35]:
def f1():
    a = 10
    def f2():
        a = 20  # 局部变量,不再是闭包
        print(a) # 访问内部的局部变量
    print(a) # 访问 f1.a
    f2()
    print(a) # f2 无法操纵 f1.a,依旧是10

f1()
10
20
10
In [40]:
def f1():
    a = 10
    def f2():
        a = 20  # 没有调用外部环境变量,不是闭包
    return f2  # 闭包与返回值无关

f = f1()
print(type(f))
print(f.__closure__) # 不是闭包故为空
<class 'function'>
None
In [42]:
def f1():
    a = 10
    def f2():
        print(a) #  调用了外部环境变量,构成闭包
    return f2 

f = f1()
print(f.__closure__)
(<cell at 0x000001CF35465C18: int object at 0x00007FFEB2C9A2B0>,)

闭包的应用

闭包存有环境变量,所以当当前步骤的计算需要之前的计算结果,就可以使用闭包

In [46]:
init_pos = 0

def pos(loc):  # loc 为环境变量
    def go(step):
        nonlocal loc # 函数内存在对 loc 的赋值,Python 会解析为局部变量,所以需要声明
        new_loc = loc + step
        loc = new_loc # 调用外部环境变量,通过闭包积累已经走过的距离
        return loc
    return go

tour = pos(init_pos)
print(tour(1))
print(tour(2))
print(tour(3))
1
3
6

从另一个角度来看,闭包实现了在外部间接访问原本访问不到的局部变量,提供了一定的灵活性。但是因为存在外部调用的缘故,局部变量的内存空间迟迟得不到回收,因此容易导致内存泄漏