python中的类变量

MeredithMHJ 8年前
   <p>最近我参加了一次面试,面试官要求用python实现某个api,一部分代码如下</p>    <pre>  <code class="language-python">class Service(object):      data = []        def __init__(self, other_data):          self.other_data = other_data</code></pre>    <p>面试官说:“data =[]这一行是错误的。”</p>    <p>我:“这没问题啊,为一个成员变量设定了初始值。”</p>    <p>面试官:“那么这段代码什么时候被执行呢?”</p>    <p>我:“我也不太清楚。为了不导致混乱还是把它删了吧”</p>    <p>于是把代码改成了下面这样</p>    <pre>  <code class="language-python">class Service(object):      def __init__(self, other_data):          self.data = []          self.other_data = other_data</code></pre>    <p>面试回来后再想想,我们都错了。问题出在对python类变量的理解。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/56a79de2f2c620541d237ddebf847625.png"></p>    <h2><strong>类成员</strong></h2>    <p>面试官错在,上面的代码在语法上是对的。</p>    <p>我错在,这句并不是为一个成员变量设置初始值,而是定义一个类变量,其初始值为空list。</p>    <p>和我一样,很多人都知道类变量,但是并不完全理解。</p>    <h3><strong>区别</strong></h3>    <p>类变量是类的一个属性,而不是一个对象的属性。</p>    <p>举个例子来说明吧, <em>class_var</em> 是一个类变量, <em>i_var</em> 是一个实例变量</p>    <pre>  <code class="language-python">class MyClass(object):      class_var = 1      def __init__(self, i_var):          self.i_var = i_var</code></pre>    <p>所有MyClass的对象都能够访问到 <em>class_var</em> ,同时 <em>class_var</em> 也能被MyClass直接访问到</p>    <pre>  <code class="language-python">foo = MyClass(2)  bar = MyClass(3)    foo.class_var, foo.i_var  ## 1, 2  bar.class_var, bar.i_var  ## 1, 3  MyClass.class_var  ## 1</code></pre>    <p>这个类成员有点像Java或者C++里面的静态成员,但是又不一样。</p>    <h2><strong>类和对象的命名空间</strong></h2>    <p>这里需要简单了解一下python的命名空间。</p>    <p>python中,命名空间是名字到对象映射的结合,不同命名空间中的名字是没有关联的。这种映射的实现有点类似于python中的字典</p>    <p>根据上下文的不同,可以通过"."或者是直接访问到命名空间中的名字。举个例子</p>    <pre>  <code class="language-python">class MyClass(object):      # 在类的命名空间内,不需要用"."访问      class_var = 1        def __init__(self, i_var):          self.i_var = i_var    ## 不在类的命名空间内,需要用"."访问  MyClass.class_var  ## 1</code></pre>    <p>python中,类和对象都有自己的命名空间,可以通过下面的方式访问。</p>    <pre>  <code class="language-python">>>> MyClass.__dict__  dict_proxy({'__module__': 'namespace', 'class_var': 1, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, '__init__': <function __init__ at 0x106cb9230>})  >>> a = MyClass(3)  >>> a.__dict__  {'i_var': 3}</code></pre>    <p>当你名字访问一个对象的属性时,先从对象的命名空间寻找。如果找到了这个属性,就返回这个属性的值;如果没有找到的话,则从类的命名空间中寻找,找到了就返回这个属性的值,找不到则抛出异常。</p>    <p>举个例子</p>    <pre>  <code class="language-python">foo = MyClass(2)  ## 在对象的命名空间中寻找i_var  foo.i_var  ## 2    ## 在对象的命名空间中找不到class_var,则从类的命名空间中寻找  foo.class_var  ## 1</code></pre>    <p>逻辑类似下面的代码</p>    <pre>  <code class="language-python">def instlookup(inst, name):      if inst.__dict__.has_key(name):          return inst.__dict__[name]      else:          return inst.__class__.__dict__[name]</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/0eb5bfeaefcf01042c4f4e6a719a4bc7.png"></p>    <h3><strong>赋值</strong></h3>    <p>有了上面的基础,就能了解怎样给类变量赋值了。</p>    <p><strong>通过类来赋值</strong></p>    <p>举个例子</p>    <pre>  <code class="language-python">foo = MyClass(2)  foo.class_var  ## 1  MyClass.class_var = 2  foo.class_var  ## 2</code></pre>    <p>在类的命名空间内,设置</p>    <p>setattr(MyClass, 'class_var', 2)<br> 需要说明的是,MyClass. <strong>dict</strong> 返回的是一个dictproxy,这是不可变的,所以不能通过 MyClass.__dict__['class_var']=2</p>    <p>的方式修改。之后在对象中访问class_var,得到返回值是2</p>    <p><strong>通过对象来赋值</strong></p>    <p>如果通过对象来给类变量赋值,将只会覆盖那个对象中的值。举个例子</p>    <pre>  <code class="language-python">foo = MyClass(2)  foo.class_var  ## 1  foo.class_var = 2  foo.class_var  ## 2  foo.__dict__  {'i_var': 2, 'class_var': 2}    MyClass.class_var  ## 1  MyClass.__dict__  ## dict_proxy({'__module__': 'namespace', 'class_var': 1, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None, '__init__': <function __init__ at 0x10fa5d230>})</code></pre>    <p>上面的代码在对象的命名空间内,加入了class_var属性,这时候,类的命名空间中的class_var属性并没有被改变,MyClass的其他对象的命名空间中并没有class_var这个属性,所以在其他对象中访问这个属性时,依然会返回类命名空间中的class_var,也就是1。</p>    <h3><strong>可变属性</strong></h3>    <p>假如类命名空间中的变量是可变的话,这时候会发生什么呢?</p>    <p>答案是,如果通过类的实例改变了变量,类变量也会发生改变,还是举个例子看看吧。</p>    <pre>  <code class="language-python">class Service(object):      data = []      def __init__(self, other_data):          self.other_data = other_data</code></pre>    <p>在上面的代码中,在Service的命名空间中定义一个data,其初始值为空list,现在通过对象来改变它</p>    <pre>  <code class="language-python">s1 = Service(['a', 'b'])  s2 = Service(['c', 'd'])  s1.data.append(1)    s1.data  ## [1]  s2.data  ## [1]    s2.data.append(2)    s1.data  ## [1, 2]  s2.data  ## [1, 2]</code></pre>    <p>可以看到,如果属性是可变的,在对象中改变这个属性,将会影响到类的命名空间。</p>    <p>可以通过赋值防止对象改变类变量。</p>    <pre>  <code class="language-python">s1 = Service(['a', 'b'])  s2 = Service(['c', 'd'])    s1.data = [1]  s2.data = [2]    s1.data  ## [1]  s2.data  ## [2]</code></pre>    <p>在上面的例子中,我们给s1加了一个data,所以Service中的data不受影响。</p>    <p>但是上面的做法也有问题,因为Service的对象很容易就改变了data,应该从设计上来来避免这个问题。我个人的意见是,如果要用一个类变量来为对象的变量设定初始值,不要使用可变类型来定义这个类变量。我们可以这样</p>    <pre>  <code class="language-python">class Service(object):      data = None      def __init__(self, other_data):          self.other_data = other_data</code></pre>    <p>当然,这样就要多花一点心思来处理None了。</p>    <h3><strong>使用</strong></h3>    <p>类变量有时候会很有用</p>    <p><strong>存储常量</strong></p>    <p>类变量可以用来存储常量,比如下面的例子</p>    <pre>  <code class="language-python">class Circle(object):      pi = 3.14159      def __init__(self, radius):          self.radius = radius        def area(self):          return Circle.pi * self.radius * self.radius    Circle.pi  ## 3.14159  c = Circle(10)  c.pi  ## 3.14159  c.area()  ## 314.159</code></pre>    <p><strong>定义默认值</strong></p>    <p>比如下面的例子</p>    <pre>  <code class="language-python">class MyClass(object):      limit = 10        def __init__(self):          self.data = []        def item(self, i):          return self.data[i]        def add(self, e):          if len(self.data) >= self.limit:              raise Exception("Too many elements")          self.data.append(e)    MyClass.limit  ## 10</code></pre>    <p><strong>追踪类的所有对象</strong></p>    <p>比如下面的例子</p>    <pre>  <code class="language-python">class Person(object):      all_names = []        def __init__(self, name):          self.name = name          Person.all_names.append(name)    joe = Person('Joe')  bob = Person('Bob')  print Person.all_names  ## ['Joe', 'Bob']</code></pre>    <h2><strong>深入底层</strong></h2>    <p>之前提到,类的命名空间在声明的时候就创建了。也就是说,对一个类,只会执行一次初始化,而对象每创建一次,就要初始化一次。举个例子</p>    <pre>  <code class="language-python">def called_class():      print "Class assignment"      return 2    class Bar(object):      y = called_class()        def __init__(self, x):          self.x = x    ## "Class assignment"    def called_instance():      print "Instance assignment"      return 2    class Foo(object):      def __init__(self, x):          self.y = called_instance()          self.x = x    Bar(1)  Bar(2)  Foo(1)  ## "Instance assignment"  Foo(2)  ## "Instance assignment"</code></pre>    <p>可以看到,Bar中的y被初始化了一次,而Foo中的y在每次生成新的对象时都要被初始化一次。</p>    <p>为了进一步的探究,我们使用 Python disassembler</p>    <pre>  <code class="language-python">import dis    class Bar(object):      y = 2        def __init__(self, x):          self.x = x    class Foo(object):      def __init__(self, x):          self.y = 2          self.x = x    dis.dis(Bar)  ##  Disassembly of __init__:  ##  7           0 LOAD_FAST                1 (x)  ##              3 LOAD_FAST                0 (self)  ##              6 STORE_ATTR               0 (x)  ##              9 LOAD_CONST               0 (None)  ##             12 RETURN_VALUE    dis.dis(Foo)  ## Disassembly of __init__:  ## 11           0 LOAD_CONST               1 (2)  ##              3 LOAD_FAST                0 (self)  ##              6 STORE_ATTR               0 (y)    ## 12           9 LOAD_FAST                1 (x)  ##             12 LOAD_FAST                0 (self)  ##             15 STORE_ATTR               1 (x)  ##             18 LOAD_CONST               0 (None)  ##             21 RETURN_VALUE</code></pre>    <p>可以明显看到 Foo.__init__ 执行了两次赋值操作,而 Bar.__init__ 只有一次赋值操作。</p>    <p>那么在实际中这两种方式性能有没有差别呢?</p>    <p>这里需要说明的是,影响代码执行速度的因素是很多的。</p>    <p>不过在这里的简单例子应该还是能说明一些问题,使用python中 timeit 模块来进行测试。</p>    <p>为了方便,笔者使用ipython写一些测试代码。</p>    <pre>  <code class="language-python">In [1]: class Bar(object):     ...:     y = 2     ...:     def __init__(self, x):     ...:         self.x = x     ...: class Foo(object):     ...:     def __init__(self, x):     ...:         self.x = x     ...:         self.y = 2</code></pre>    <h3><strong>初始化测试</strong></h3>    <pre>  <code class="language-python">In [2]: %timeit Bar(2)  The slowest run took 8.17 times longer than the fastest. This could mean that an intermediate result is being cached.  1000000 loops, best of 3: 379 ns per loop  In [3]: %timeit Foo(2)  The slowest run took 8.10 times longer than the fastest. This could mean that an intermediate result is being cached.  1000000 loops, best of 3: 471 ns per loop</code></pre>    <p>可以看到 Bar 的初始化比 Foo 的初始化要快了不少。</p>    <p>为什么会这样呢,一个合理的解释是:Bar对象初始化的时候执行了一次赋值,而Foo对象初始化时执行了两次赋值</p>    <h3><strong>赋值测试</strong></h3>    <pre>  <code class="language-python">In [4]: %timeit Bar(2).y = 15  The slowest run took 27.73 times longer than the fastest. This could mean that an intermediate result is being cached.  1000000 loops, best of 3: 430 ns per loop  In [5]: %timeit Foo(2).y = 15  1000000 loops, best of 3: 511 ns per loop</code></pre>    <p>因为这里实际上执行了一次初始化操作,所以需要减掉之前的初始化值</p>    <pre>  <code class="language-python">Bar assignments: 430 - 379 = 51ns  Foo assignments: 511 - 471 = 40ns</code></pre>    <p>看起来Foo的赋值操作比Bar的赋值操作要快一些。一个合理的解释是,在Foo的对象命名空间中能够直接找到( Foo(2).__dict__[y] )这个属性,而在Bar的对象命名空间中找不到( Bar(2).__dict__[y] )这个属性,然后就去Bar的类命令空间中找,这多出来的查找导致了性能的消耗。</p>    <p>虽然在实际中这样的性能差别几乎可以忽略不计,但是对于理解类中的变量和对象中的变量之间的差异还是有帮助的。</p>    <h2><strong>总结</strong></h2>    <p>在学习python的时候,了解类属性和对象属性还是很有必要的。</p>    <p>不过在工作中,为了保证不入坑,还是避免使用的好。</p>    <h2><strong>私有变量</strong></h2>    <p>额外说一点,python中并没有私有变量,但是通过取名可以部分实现私有变量的效果。</p>    <p>python文档中说,不希望被外部访问到的属性取名时,前面应该加上 __ ,这不仅仅是个标志,而且是一种保护措施。比如下面的代码</p>    <pre>  <code class="language-python">class Bar(object):      def __init__(self):          self.__zap = 1    a = Bar()  a.__zap  ## Traceback (most recent call last):  ##   File "<stdin>", line 1, in <module>  ## AttributeError: 'Bar' object has no attribute '__zap'    ## 查看命名空间  a.__dict__  {'_Bar__zap': 1}  a._Bar__zap  ## 1</code></pre>    <p>可以看到,前面加了 __ 的变量,被自动加上了前缀 _Bar ,python就是通过这样的机制防止'私有'的变量被访问到。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/3aca78a84def</p>    <p> </p>