Factory Function for Tuples with Named Fields

Python 除了大家熟知的,可以直接使用的 list、dictionary、tuple 等容器,还有一些放在 collections 包中的容器,这些容器的泛用性不及普通容器,但是在特殊的场景下,有着超过普通容器的性能与便利性。

本篇就在官方文档的基础上,结合笔者的学习工作经验,于管中窥得 nametuple 之一斑。

使用场景

笔者在学习工作中使用 nametuple 的场景如下:

  1. 在定义类似坐标、rgba 值等简单的数据容器时,可以使用 nametuple
  2. 可以用来快速生成 csv、sql 对应的类

下面解释一下为什么这些场景可以使用 nametuple 吧~

简单数据容器

这里为了方便理解,首先使用一个众所熟知的定义数据容器的手段 – 类,来进行一个类比。

class Circle
{
      private:
      double radius;

      public:
      构造函数...
}

如上是国内 C++入门教材的必经之路,通过 定义了一个简单的圆容器。这里我们选择的案例并不是 circle,而是更加复杂的 rgba。

# 在python中定义一个color类,包含rgba四个关键变量
class ColorForCompare:
    __slots__ = ("r","g","b","alpha")

    def __init__(self,r,g,b,alpha):
        self.r = r  # red
        self.g = g  # green
        self.b = b  # blue
        self.alpha = alpha

这样,就可以使用 color_for_compare.r 来查看颜色对应的 red 值。

但是这显然是不够的,因为此时它可以接收类似 color_for_compare.x = 1 这样意义不明的语句。

class ColorForCompare:
+    __slots__ = ("r","g","b","alpha")

    def __init__(self,r,g,b,alpha):
        self.r = r
        self.g = g
        self.b = b
        self.alpha = alpha

+    def __repr__(self):
+        return f'r = {self.r}, g = {self.g}, b = {self.b}, alpha = {self.alpha}'

ColorForCompare(0,0,0,0)
output : r = 0, g = 0, b = 0, alpha = 0
color_for_compare = ColorForCompare(0,0,0,0)
color_for_compare.x = 1
output:

---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-58-e007350343ef> in <module>
----> 1 color_for_compare.x = 1


AttributeError: 'ColorForCompare' object has no attribute 'x'

这样,使用内置的 __slots__ 将变量固定住,就避免了 x 这样意义不明的值的干扰。同时,为了美观,又加上了一个 __repr__() 来输出实例化的结果。

至此,大体上完成了一个 color 容器。那么,在对应的 nametuple 中要如何实现这些功能呢?

from collections import namedtuple
Color = namedtuple("Color", "r g b alpha")

Color(0,0,0,0)
output:

Color(r=0, g=0, b=0, alpha=0)

对了,就是这样简单,这里我们可以再测试一下是否有限制变量名称的功能。

color = Color(0,0,0,0)
color.x = 1
output:

    ---------------------------------------------------------------------------

    AttributeError                            Traceback (most recent call last)

    <ipython-input-64-02f8a35ba0d6> in <module>
          1 color = Color(0,0,0,0)
    ----> 2 color.x = 1


    AttributeError: 'Color' object has no attribute 'x'

通过使用 nametuple,两行代码完成了 7 行代码的工作。

这里需要 补充 的是,你无法通过 color.r = 1 这样的语句来直接修改 nametuple 的值,具体可以看 python3.collections,毕竟,这是个 tuple😄

nametuple 本身无法应付一些复杂的场景,比如,我们可能需要根据特殊的 rgb 值来判断是否是一些特殊的颜色(如:50,0,255->蓝色),这就需要一些额外的函数来实现。
还有一种更加符合面向对象思维的方法,将这个函数变成 Color 类的成员函数,组件一个完善的 Color 类。显然单纯的 nametuple 无法做到这一点。

不过这并不是问题,我们完全可以通过强化 nametuple 来实现这个需求。

class ColorPlus(Color):
    def convert_color_to_string(self):
        if self.r == 50 and self.g == 0 and self.b == 255:
            return '蓝色'
        else:
            return '无'
color_p = ColorPlus(50,0,255,0)
color_p.convert_color_to_string()
output:  '蓝色'

新的类可以继承 nametuple 所定义的容器~

这就是我所说的 场景二:用来快速生成 csv、sql 对应的类

与 dictionary 的对比

python 提供了一个非常方便的容器,也就是字典。这二者的区别在于,dictionary 是 unhashable 的,而 nametuple 相反。

以 Counter 为例。我们想要统计一群颜色中,有多少是蓝色?

color1 = Color(50,0,255,0)
Counter([color, color,color1])
output:

    Counter({Color(r=0, g=0, b=0, alpha=0): 2,
             Color(r=50, g=0, b=255, alpha=0): 1})
c = {"r": 50, "g": 205, "b": 50, "alpha": 1.0}
Counter([c])
output:

    ---------------------------------------------------------------------------

    TypeError                                 Traceback (most recent call last)

    <ipython-input-80-85ae4a80ee69> in <module>
          1 c = {"r": 50, "g": 205, "b": 50, "alpha": 1.0}
    ----> 2 Counter([c.items()])


    ~\miniconda3\envs\jupy\lib\collections\__init__.py in __init__(*args, **kwds)
        566             raise TypeError('expected at most 1 arguments, got %d' % len(args))
        567         super(Counter, self).__init__()
    --> 568         self.update(*args, **kwds)
        569
        570     def __missing__(self, key):


    ~\miniconda3\envs\jupy\lib\collections\__init__.py in update(*args, **kwds)
        653                     super(Counter, self).update(iterable) # fast path when counter is empty
        654             else:
    --> 655                 _count_elements(self, iterable)
        656         if kwds:
        657             self.update(kwds)


    TypeError: unhashable type: 'dict_items'

dict 是无法使用 Counter 的。

本质上,nametuple 仍然是 tuple,是与 dictionary 并行的 python 容器。所以这两者的选用,根据实际情况,具体问题具体分析。

还有一个值得一提的区别。在日常使用 dict 时,我们可能会纠结应该时 color[‘blue’] 还是 color[‘b’],在多人开发时,这会导致一些麻烦甚至是风险 。而在使用 nametuple 时,ide 会像对待 class 一样给出相应的代码提示。

nametuple 与 dictionary 和 tuple 的转换方法

dictionary => nametuple

c = {"r": 50, "g": 205, "b": 50, "alpha": 1.0}
Color(**c)   # unpack
output:

    Color(r=50, g=205, b=50, alpha=1.0)

dictionary 创建一个 nametuple

Color = namedtuple("Color", c)
Color(**c)
output:

    Color(r=50, g=205, b=50, alpha=1.0)

nametuple => dictionary

Color(**c)._asdict()
output:
    OrderedDict([('r', 50), ('g', 205), ('b', 50), ('alpha', 1.0)])
tuple(Color(**c))
output:
    (50, 205, 50, 1.0)

排序

与字典的排序方法相类似,可以使用简单的 lambda 来实现 nametuple 的排序功能

colors = [
    Color(r=50, g=205, b=50, alpha=0.1),
    Color(r=50, g=205, b=50, alpha=0.5),
    Color(r=50, g=0, b=0, alpha=0.3)
]
sorted(colors,key=lambda x:x.alpha,reverse=True)
output:
    [Color(r=50, g=205, b=50, alpha=0.5),
     Color(r=50, g=0, b=0, alpha=0.3),
     Color(r=50, g=205, b=50, alpha=0.1)]

总结

以上,就是现阶段使用 nametuple 的一些心得体会,与常见的容器对比总结如下:

与 class

  1. 自带了 solt 属性
  2. 属性的值不可直接改变,而需要借助内置函数
  3. 原生 Class 无法使用 Counter
  4. 更简单的定义方法

与 dictionary

  1. hashable,可以使用 Counter 之类的统计方法
  2. 可以在 ide 中可以得到代码提示