Python类变量和实例变量

Last Updated: 2023-05-12 01:05:05 Friday

-- TOC --

在Python中,class本身也是一个对象,且可以绑定变量,被称为类变量。对应类变量的,就是实例变量,绑定在实例对象上。这两类变量有着微秒的区别和联系。

类变量(坑)

Python中一切都是对象,定义一个class,这个class本身也是个对象,用class在运行时创建的instance,也是对象,但它们是不同类型的对象。

我们习惯了给instance对象设置各种变量,而Python提供了一个机制,可以给class对象设置变量,这些变量被称为是类变量,class variable

class variable这个机制,从逻辑上看,它可以用于存储从这个class派生出来的所有instance共用的数据。这很像在C++的class中定义static成员变量。访问类变量建议通过类名,虽然通过instance也可以访问,但可能很容易出现通过instance尝试修改类变量的错误。比如:

>>> class xyz():
...   a = 0
...
>>> xyz.a
0
>>> xyz.a = 9
>>> xyz.a
9
>>> x = xyz()
>>> x.a
9
>>> x.a = 88
>>> x.a
88
>>> xyz.a
9

当执行x.a = 88这行代码的时候,实际上是给x对象创建了一个名为a变量,并没有修改类变量xyz.a的值。但是,下面这种用法却是正确的:

>>> class xyz():
...   b = {}
...
>>> xyz.b.setdefault('a',1)
1
>>> xyz.b
{'a': 1}
>>> y = xyz()
>>> y.b.setdefault('b',2)
2
>>> y.b
{'a': 1, 'b': 2}
>>> xyz.b
{'a': 1, 'b': 2}

上面的代码为何就正确了?

可以这样来理解,y.b.setdefault这行代码在访问b,调用b的一个接口,并不是用=进行创建并赋值,这时Python解释器要去寻找b在哪里,而不是创建一个新的b。因此,通过instance也可以实现修改类变量的效果。

上面示例的这个微妙的差异,使用时必须要非常小心,虽然Python标准Lib里面也有这样的用法。比如下面的写法,就又变成错误的了:

>>> class xyz():
...   b = {}
... 
>>> z = xyz()
>>> z.b = [1,2,3]  # create z.b
>>> z.b
[1, 2, 3]
>>> xyz.b
{}

Python中的class,在运行时也是存在于内存中的一个对象,可以为class动态地创建或删除class variable:

>>> class xyz(): pass
... 
>>> xyz.a = 1
>>> xyz.b = {}
>>> xyz.b.setdefault('a',9)
9
>>> xyz.a
1
>>> xyz.b
{'a': 9}
>>> del xyz.a
>>> del xyz.b

比较有趣味的细节是,定义在class中(非def内)的代码,实际上在导入时,会被直接执行:

>>> class codeinclass():
...     for i in range(10): print(i)
...
0
1
2
3
4
5
6
7
8
9
>>> cic = codeinclass()
>>> cic
<__main__.codeinclass object at 0x7fc73001e358>

类变量就是这样在运行时被执行了一次。

当instance和class都有相同名称的成员时,优先访问instance的。但我认为,这是代码设计需要避免的容易出错的细节。

vars接口和__dict__

vars是builtin接口,返回输入对象的__dict__属性,这里面存放了所有与对象绑定的实例变量。

>>> class xyz():
...   pass
...
>>> x = xyz()
>>> x.a = 1
>>> x.b = 2
>>> vars(x)
{'a': 1, 'b': 2}
>>> x.__dict__
{'a': 1, 'b': 2}

instance variables都放在instance的__dict__中。

vars接口的一个作用,就是用访问dict的方式,访问instance variable,比如特别好用的setdefault

>>> vars(x).setdefault('c',3)
3
>>> vars(x).setdefault('c',5)
3
>>> vars(x)
{'a': 1, 'b': 2, 'c': 3}

事实上,class variables也都是存放在class对象的__dict__中的:

>>> class xyz(): pass
...
>>> xyz.a = 11
>>> xyz.b = 22
>>> vars(xyz)
mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'xyz' objects>, '__weakref__': <attribute '__weakref__' of 'xyz' objects>, '__doc__': None, 'a': 11, 'b': 22})

例外的情况

如果类变量是descriptor(descriptor只能是class variable),并且定义了__set__接口,通过实例对此变量进行setting的时候,不会创建同名实例变量,而是访问descriptor的set接口。

class bob():

    def __get__(self, obj, objtype=None):
        print("bob.__get__")
        return obj._b

    def __set__(self, obj, val):
        print("bob.__set__")
        obj._b = val+1  # crate x._b


class xyz():

    b = bob()

    def __init__(self, bob):
        self.b = bob  # call xyz.b.__set__


x = xyz(777)
print(x.b)
x.b = 888   # call xyz.b.__set__
print(x.b)

实例对象的私有成员

一般用_暗示这是个私有的成员,但Python是允许直接访问的,毕竟大家都是成年人了...

Python的面向对象语法比C++要简化了很多,比如成员定义的时候,由于不需要申明,也没有private和public关键词。有人说,Python对象没有私有成员。这个说法,其实,对也不对!

__实现私有成员

双下划线开头的成员,不管是变量还是函数,都可以说是私有的,只能内部访问。

class test():
    def __init__(self):
        self.__foo = 'foo'
        self.__bar = 'bar'

    def show(self):
        self.__show()

    def __show(self):
        print(self.__foo)
        print(self.__bar)


t = test()
t.show()    # success
t.__show()  # fail

调用t.show()会成功,但是调用t.__show()会失败,此时Python解释器提示:AttributeError: 'test' object has no attribute '__show'。同样,直接访问t.__foo和t.__bar,一样失败,一样提示没有此属性。使用hasattr接口判断,返回False!

但,__加持的私有成员还是可以直接访问的...

t = test()
t._test__show()
print(t._test__foo)
print(t._test__bar)

用一个下划线加类名再加成员名称,obj._<className><__attributeName>,就可以访问了。这基本只是在调试的时候,为了调试方便而打开的后门,代码可千万别写成这样了!

魔术类变量__slots__

如果class定了类变量__slots__,带来的效果是:

  1. 实例对象将不再有默认的魔法__dict__属性,节省了内存;
  2. 实例对象将只能拥有__slots__中定义的属性,限制了动态地随意地增加实例属性的危险行为;
  3. 存在__slots__的class,它的instance不能使用vars接口,会失败;
  4. 访问实例属性,速度更快,节省了一次hash的过程。(原理是根据成员变量的偏移值来访问内存,这会不会是slots这个名字的由来?)

举个例子:

>>> class xyz():
...   __slots__ = ('a','b')
... 
>>> x = xyz()
>>> hasattr(x, '__dict__')
False
>>> x.a = 1
>>> x.b = 2
>>> x.c = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'xyz' object has no attribute 'c'
>>> vars(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute

xyz的实例x,只能拥有属性a和b,其它名称的属性都不行,而且x没有魔法__dict__属性。

看起来新代码没有不使用__slots__的理由呀...

本文链接:https://cs.pynote.net/sf/python/202209301/

-- EOF --

-- MORE --