符合 Python 风格的对象

2018/07/08 16:05 下午 posted in  Python

在 Python 中,自定义的类也可以表现得像内置类型一样自然,这都得益于鸭子类型:我们只需按照预定行为实现对象所需的方法即可。这篇文章主要介绍自定义类的一些特殊方法,来让类的行为跟真正的 Python 对象一样。

类的特殊方法

类的特殊方法是为了被解释器调用,目的是可以将一些内置的方法用在对象上。比如特殊方法 __len__ 是为了 len() 函数的调用,我们在使用的时候就可以直接使用 len(a) 这种写法,而不是 a.__len__() ,如果 a 是自定义对象,Python 就会自己去调用我们自定义的 __len__ 方法。

Python 中与运算符无关的特殊方法:

与运算符有关的特殊方法:

下面介绍几个常用的特殊方法。

对象表示形式

Python 提供了两种获取对象的字符串表示形式: repr()str()

Python 有一个内置 repr 函数,将一个对象用字符串形式表示出来,通过调用 __repr__ 这个特殊方法来实现的。同样, str() 是通过调用 __str__ 实现的。

在类中,我们需要实现 __repr____str__ 方法来实现将对象用字符串表示。需要注意的是, __repr__ 所返回的字符串应该准确无歧义。

__repr____str__ 的区别在于,后者是在 str() 函数被使用,或在用 print 函数打印对象的时候才被调用,并且它返回的字符串对终端用户更友好;而一个对象没有 __str__ 函数, Python 又需要调用它的时候,解释器会用 __repr__ 函数作为代替。

格式化显示

Python 内置的 format 方法和 str.format 方法将各个类型的格式化方法委托给相应的 __format__ 方法。

如果类中没有定义该方法,在对对象使用 format 方法时,从 object 继承的方法会返回 str() 。然而在这种情况下,有一种弊端就是如果我们传入格式说明符,object.__format__ 方法会报错。

我们希望得到的结果是对象的每个属性都以我们传入的形式表示出来。举个例子:

In [1]: class Test:
   ...:     def __init__(self, x, y):
   ...:         self.integer = x
   ...:         self.decimals = y
   ...:     def __iter__(self):
   ...:         return (i for i in (self.integer, self.decimals))
   ...:     def __str__(self):
   ...:         return str(tuple(self))
   ...:     def __format__(self, format_spec=''):
   ...:         contexts = (format(s, format_spec) for s in self)
   ...:         return "{}.{}".format(*contexts)
   ...:     

In [2]: format(Test(123, 9), "d")
Out[2]: '123.9'

类的散列化

为了实现类的散列化,我们需要实现 __hash__ 方法。根据散列化的定义,我们需要保证对象唯一不变,且需要返回对象属性的散列值,所以另外需要实现 __eq__ 方法。

为了保证唯一不变,我们需要将对象属性设置成只读。举个例子:

class Test:
    def __init__(self, x, y):
        self.__integer = x
        self.__decimals = y

    @property
    def integer(self):
        return self.__integer

    @property
    def decimals(self):
        return self.__decimals

    def __iter__(self):
        return (i for i in (self.integer, self.decimals))

    def __str__(self):
        return str(tuple(self))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __hash__(self):
        return hash(self.integer) ^ hash(self.decimals)

property 装饰器即可将读值方法标记成特性,只有将对象的属性都设置成不可变,才能实现 __hash__ 方法。这个方法需要返回一个整数,且需要保证相等对象的散列值相同,所以最好的实现方式是使用异或运算来混合各属性的散列值。我们来看看上面这个类的散列值:

In:
    t1 = Test(123, 9)
    t2 = Test(234, 8)
    t3 = Test(234, 8)

    print("t1 hash is: {}".format(hash(t1)))
    print("t2 hash is: {}".format(hash(t2)))
    print("t3 hash is: {}".format(hash(t3)))
    
Out:
    t1 hash is: 114
    t2 hash is: 226
    t3 hash is: 226

classmethod 和 staticmethod

Python 中有两个特殊的装饰器:classmethod、 staticmethod

classmethod :即类方法,是 Python 专用的。该装饰器定义了该方法是操作类,而不是操作实例,因此类方法的第一个参数时类本身,而不是实例。按照约定,类方法的第一个参数名为 cls 。classmethod 最常见的用途是定义备选构造方法。

staticmethod:即静态方法。该装饰器也会改变方法的调用方式,但第一个参数不是特殊的值。静态方法就是普通的函数,只是碰巧在类的定义体中,所以无法引用类或对象的属性。

举个例子看两者的差别:

class Test:
    cont = 1

    @classmethod
    def c_method(cls):
        return "This is a classmethod, which cont is {}".format(cls.cont)

    @staticmethod
    def s_method():
        return "This is a staticmethod."

输出结果:

This is a classmethod, which cont is 1
This is a staticmethod.

类的属性

在 Python 的底层实现中,类的所有对象的属性都存在 __dict__ 属性中,且所有对象的属性共用 key

Python 不能像其他语言一样用 private 修饰符来创建私有属性,但是有一个简单的机制来避免子类意外覆盖私有属性,即以 __ 开头来标记属性私有化,比如上面例子中 __integer__decimals 属性。这些私有属性存入 __dict__ 中时,属性名变为 _ + 类名 + 属性名,比如 __integer 会变为 _Test__integer。这种语言特性称为名称改写。

需要注意的是,Python 中不会使用单下划线对属性名做特殊处理,不过很多 Python 程序员会严格遵守 “不在类外部访问这种属性” 的约定。

在前面的博客中,我们讲到了 Python 中字典的底层实现,字典的运算速度很快,但相当吃内存。而当类的属性多到一定数量时,我们需要用到 __slots__ 属性,来节省内存。

使用 __slots__ 类属性节省空间

在 Python 中,唯一节省内存的数据结构是元组,所以 __slots__ 属性的实现方法是让解释器在元组中存储实例属性,而不是字典。其定义十分简单:

class Test:
    __slots__ = ("__integer", "__decimals")

使用元组的形式也就意味着这些属性不可变。有几点需要注意的是:

  • 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承该属性;
  • 对象只能拥有 __slots__ 中列出的属性,除非将 __dict__ 加入其中;
  • 用户自定义的类中默认有 __weakref__ 属性,若想把对象作为弱引用的目标,需要把 __weakref__ 也添加到 __slots__ 中。

如果使用得当, __slots__ 属性能显著节省内存,而该属性的存在是为了优化,不能用此作为限制用户赋值的属性。