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}
所以,公开的类属性应该只能使用常量(数字,字符串,元组),可变对象应该赋值给实例属性。
- 如果是私有的类属性(以两个下划线开头),则可以避免意外修改,但仍然不能避免刻意修改。(注意:私有类属性不会被继承)
- 如果在实例中将另一个对象赋值给类属性,则类的类属性不会受到影响(相当于实例属性覆盖了类属性)。
在接下来的内容之前,要搞清楚什么是类以及什么是实例。以上面的例子为例:A
为类,a
为A
的实例。
什么是类属性,什么是实例属性?
- 类属性:在类内部直接定义的属性(如上面例子中类A的属性b)。
- 实例属性:
self
(即实例)的属性。 - 类方法:以
@classmethod
装饰的函数,第一个参数为cls
(即类本身)。 - 实例方法:定义于类内部的函数,第一个参数为
self
(即实例本身)。 - 静态方法:以
@staticmethod
装饰的函数,不与实例进行交互。
类属性有哪些用途呢?
- 可以存储常量。由于类属性是属于类的而不是属于实例的,所以比起在
__init__
方法中将常量赋值给实例属性,如果将常量赋值给类属性,那么在创建多个对象时可以节约一些时间,节省内存,提高效率。 - 可以定义默认值,在继承时会很有用。
上面例子中的
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
再试一试,你会发现效果是一样的。再思考一下这样做有哪些缺点。