Python 中的类属性,实例属性,类方法,实例方法和静态方法

Python 中的类属性,实例属性,类方法,实例方法和静态方法

不要将一个可变对象赋值给一个公开的类属性:

In [1]: class A:
   ...:     b = {1, 2, 3}
   ...:

In [2]: A.b
Out[2]: {1, 2, 3}

In [3]: a = A()

In [4]: a.b
Out[4]: {1, 2, 3}

In [5]: a.b.add(4)

In [6]: a.b
Out[6]: {1, 2, 3, 4}

In [7]: A.b  # 实例a的属性b发生变化之后却影响了类A的类属性b
Out[7]: {1, 2, 3, 4}

所以,公开的类属性应该只能使用常量(数字,字符串,元组),可变对象应该赋值给实例属性。

  1. 如果是私有的类属性(以两个下划线开头),则可以避免意外修改,但仍然不能避免刻意修改。(注意:私有类属性不会被继承)
  2. 如果在实例中将另一个对象赋值给类属性,则类的类属性不会受到影响(相当于实例属性覆盖了类属性)。

在接下来的内容之前,要搞清楚什么是类以及什么是实例。以上面的例子为例:A为类,aA的实例。

什么是类属性,什么是实例属性?

  • 类属性:在类内部直接定义的属性(如上面例子中类A的属性b)。
  • 实例属性:self(即实例)的属性。
  • 类方法:以@classmethod装饰的函数,第一个参数为cls(即类本身)。
  • 实例方法:定义于类内部的函数,第一个参数为self(即实例本身)。
  • 静态方法:以@staticmethod装饰的函数,不与实例进行交互。

类属性有哪些用途呢?

  1. 可以存储常量。由于类属性是属于类的而不是属于实例的,所以比起在__init__方法中将常量赋值给实例属性,如果将常量赋值给类属性,那么在创建多个对象时可以节约一些时间,节省内存,提高效率。
  2. 可以定义默认值,在继承时会很有用。

上面例子中的bug特性如果有心利用的话会有一些有趣的用法(比如计数…),但我强烈不建议这样搞,因为很有可能造成混乱。

实例方法和类方法有哪些区别?

操作实例的方法就是实例方法,与此相对应的,操作类的方法就是类方法。

实例方法的第一个参数self即为实例本身,而类方法的第一个参数cls为类本身。

我们可以通过Python的自省来更直观地了解:

In [1]: class A:
   ...:
   ...:     # 类属性
   ...:     x = 555
   ...:
   ...:     def __init__(self):
   ...:         # 实例属性
   ...:         self.y = 777
   ...:
   ...:     # 类方法
   ...:     @classmethod
   ...:     def class_m(cls):
   ...:         return cls.x
   ...:
   ...:     # 静态方法
   ...:     @staticmethod
   ...:     def static_m():
   ...:         return 666
   ...:
   ...:     # 实例方法
   ...:     def foo(self):
   ...:         return self.y
   ...:

In [2]: a = A()

In [3]: dir(A)
Out[3]:
['__class__',
 '__delattr__',
 ...,
 '__weakref__',
 'class_m',
 'foo',
 'static_m',
 'x']

In [4]: dir(a)
Out[4]:
['__class__',
 '__delattr__',
 ...,
 '__weakref__',
 'class_m',
 'foo',
 'static_m',
 'x',
 'y']

In [5]: a.__class__ is A  # 实例的__class__属性可以返回实例的类
Out[5]: True

我们可以发现,类A和实例a都拥有类属性、静态方法、类方法和实例方法,而实例a拥有特有的实例属性y。虽然类A也拥有实例方法foo(),但是无法调用(尝试调用A.foo()时会提示你缺少self参数)

我们刚才已经详细介绍了类属性,静态方法可以理解为一个不与实例进行交互的函数,接下来着重介绍类方法。

Tip:子类的实例方法会覆盖父类同名的类方法,且互不影响。

《流畅的Python》一书中第9章第4节写道:

(classmethod)定义操作类,而不是操作实例的方法。classmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。classmethod 最常见的用途是定义备选构造方法…

如何理解“定义备选构造方法”呢?

因为类方法的第一个参数一定为类本身,所以我们可以在类方法内生成一个类的实例并返回。

举个例子,《流畅的Python》一书中第19章第1节提出了这么一个问题:

feed['Schedule']['events'][40]['name'] 这种句法很冗长。在 JavaScript 中,可以使用feed.Schedule.events[40].name 获取那个值。在 Python 中,可以实现一个近似字典的类(网上有大量实现) ,达到同样的效果。我自己实现了 FrozenJSON 类,比大多数实现都简单,因为只支持读取,即只能访问数据。不过,这个类能递归,自动处理嵌套的映射和列表。

最终完成的FrozenJSON类的代码如下(为方便理解,我添加了一些注释):

import keyword
# collections.abc模块提供了抽象的基类 可以用来测试一个类是否提供了特定的接口
from collections import abc

class FrozenJSON:

    def __init__(self, mapping):
        self.__data = {}
        for key, value in mapping.items():
            # 如果key是python关键字 则在key结尾加上一个下划线
            if keyword.iskeyword(key):
                key += "_"
            self.__data[key] = value

    def __getattr__(self, name):  # __getattr__特殊方法会在实例找不到name属性时调用
        # 如果self.__data有name属性 则返回
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        try:
            return FrozenJSON.build(self.__data[name])
        except KeyError:
            raise AttributeError(r"'FrozenJSON' object has no attribute '%s'" % name)

    @classmethod
    def build(cls, obj):
        # 如果obj是只读且可变映射对象(字典) 则返回一个新的FrozenJSON对象
        if isinstance(obj, abc.Mapping):
            return cls(obj)
        # 如果obj是只读且可变序列对象(列表)
        if isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        # 如果既不是字典也不是列表 则直接返回
        return obj

试着把代码敲一遍,然后实践一下,就能理解“定义备选构造方法”的意义了。

然而,以上代码并不是原书中完成的最终代码,在最终的代码中,使用了实例的构造方法__new__使得代码更加简洁。

__init__方法是实例的初始化方法而不是构造方法,因为__init__方法必须返回None,而__new__方法可以返回实例,甚至可以返回其他类的实例。

限于篇幅,以后再讲。


一定要使用classmethod吗?

将上面FrozenJSON类的代码中的build方法改成这样:

    @staticmethod
    def build(obj):
        # 如果obj是只读且可变映射对象(字典) 则返回一个新的FrozenJSON对象
        if isinstance(obj, abc.Mapping):
            return FrozenJSON(obj)
        # 如果obj是只读且可变序列对象(列表)
        if isinstance(obj, abc.MutableSequence):
            return [FrozenJSON.build(item) for item in obj]
        # 如果既不是字典也不是列表 则直接返回
        return obj

再试一试,你会发现效果是一样的。再思考一下这样做有哪些缺点。