世人凭自己的智慧,既不认识神,神就乐意用人所当作愚拙的道理拯救那些信的人,这就是神的智慧了。犹太人是要神迹,希腊人是求智慧;我们却是传钉十字架的基督。(1 CORINTHIANS 1:21-22)

类(3)

在上一节中,对类有了基本的或者说是模糊的认识,为了能够对类有更深刻的认识,本节要深入到一些细节。

类属性

在交互模式下,创建一个简单的类。

>>> class A(object):    #Python 3: class A:
...         x = 7
... 

这个类中的代码,没有任何方法,只有一个x = 7,当然,如果愿意还可以写别的。

先不用管为什么,继续在交互模式中敲代码。

>>> A.x
7

A是刚刚建立的类的名字,x是那个类中的一个变量,它引用的对象是整数7。通过A.x的方式,就能得到整数7。像这样的,类中的x被称为类的属性,而7是这个属性的值。A.x是调用类属性的方式。

这里谈到了“属性”,不要忽视这个词语,它是用在很多领域的一个术语,比如哲学有一些哲学家认为,属性所描述的是种类性质(例如颜色),属性的性质就是所谓的值(比如红色)。

哲学真的是“科学之母”呀,把上面的理解套用过来,就是我们现在讨论的类属性。x是类A的性质,它的值是7。如果还抽象,我就不得不寄出大招了。

>>> class Girl(object):    #Python 3: class Girl:
            breast = 90

在真实世界中,breast就是Girl的属性,你到某度上去搜索一下有关的关键词,就发现breast是一个重要属性了。所以,如果要建立类Girl,它必须有breast属性。那么这个属性的值,就关系到某类Girl的颜值了。这里的类Girl都是breast为90的,你可以想象其颜值高低了,是否符合你的喜好。

大招一出,为之一振,顿时困意消退。下面就请回到前面那个类A,继续枯燥地学习。

如果要调用类的某个属性——简称:类属性——其方法就是用半角的英文句号.,如前面所演示的A.x。类属性仅与其所定义的类绑定,并且这种属性,本质上说就是类中的变量,它的值不依赖于任何实例,只是由类中所写的变量赋值语句确定,比如Girl类,不管你用这个类建立的实例是东施还是西施,类属性Girl.breast都是90。所以,这个类属性还有别的名字,如静态变量或者静态数据。

在本书中,已经多次提到“万物皆对象”的观念,类也不例外,它也是对象。只有对象,才具有属性和方法。而属性是可以修改或者增加、删除的。既然如此,对刚才的类A或者类Girl,你都可以对目前其有的属性进行修改,也可以增加新的属性。

>>> A.y = 9
>>> A.y
9

对类A增加了一个新的属性,并且赋给了值。然后删除一个已有属性。

>>> del A.x
>>> A.x
Traceback (most recent call last):
  File "<pyshell#14>", line 1, in <module>
    A.x
AttributeError: type object 'A' has no attribute 'x'

A.x属性删除之后,再调用,就出现异常了。但是A.y依然存在。

>>> A.y
9

你也可以修改当前已有的类属性。

>>> Girl.breast = 40
>>> Girl.breast
40

breast是我们在类Gril中自己定义的属性,其实在一个类建立的同时,Python也让这个类具有了一些默认的属性。可以用我们熟知的dir()来查看类的所有属性(也包括方法)。

>>> dir(Girl)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'breast']

仔细观察,上面有一个特殊的属性__dict__,用“特殊”来修饰这个属性,是因为它也是以双下划线开头和结尾,就类似你已经见过的一个特殊方法__init__()一样。在类里面,凡以双下划线开头和结尾命名的属性或者方法,我们都会说它是“特殊”的。

对于Python 2和Python 3,上述显示结果会略有不同,其差别主要在于特殊属性和方法上。所以读者在自己的版本中操作的时候,不要执拗于上述结果。但是,自定义的属性breast是肯定都存在的。

>>> Girl.__dict__
mappingproxy({'breast': 40, '__weakref__': <attribute '__weakref__' of 'Girl' objects>, '__dict__': <attribute '__dict__' of 'Girl' objects>, '__module__': '__main__', '__doc__': None})

对于不同版本的Python,上述显示结果也有所差异,但你都能看到属性的详细信息。类的特殊属性__dict__所显示的是这个类的属性名称以及该属性的完整数据。

下面列出类的几种特殊属性的含义,读者可以一一查看。

  • C.__name__:以字符串的形式,返回类的名字,注意这时候得到的仅仅是一个字符串,它不是一个类对象
  • C.__doc__:显示类的文档
  • C.__base__:类C的所有父类。如果是按照上面方式定义的类,应该显示object,因为以上所有类都继承了它。等到学习了“继承”,再来看这个属性,内容就丰富了
  • C.__dict__:以字典形式显示类的所有属性
  • C.__module__:类所在的模块

稍微解释C.__module__。承接前面建立的类Gril,做如下操作:

>>> Girl.__module__
'__main__'

说明这个类所述的模块是__main__,换个角度,类Girl的全称是__main__.Girl。还记得我们曾经用过标准库中的math吗?它是一个模块,它下面的内容都是用类似math.pi的样式读取。两者是同样道理,只不过模块名称变化了。

>>> from math import sin
>>> sin.__module__
'math'

这里的sin全称就是math.sin,所以你也可以import math,然后使用math.sin。以上两者是一样的道理。

最后对类属性进行总结:

  1. 类属性跟类绑定,可以自定义、删除、修改值,也可以随时增加类属性
  2. 类属性不因为实例变化而发生变化
  3. 每个类都有一些特殊属性,通常情况特殊属性是不需要修改的,虽然有的特殊属性可以修改,比如C.__doc__

对于类,除了属性,还有方法。但是类中的方法,因为牵扯到实例,所以,我们还是通过研究实例,理解类中的方法

创建实例

创建实例并不是困难的事情,只需要通过调用类就能实现。

>>> canglaoshi = Girl()
>>> canglaoshi
<__main__.Girl object at 0x0000000003726C18>

这就创建了一个实例canglaoshi

请读者注意,调用类的方法和调用函数的方法类似。如果仅仅写Girl()也是创建了一个实例,如下所示:

>>> Girl()
<__main__.Girl object at 0x00000000035262E8>

canglaoshi = Girl()本质上就是将变量canglaoshi与实例对象Girl()建立引用关系,这种关系同以前见过的赋值语句a = 2是同样的效果。

那么,一个实例的建立过程是怎样进行的呢?

再次启用上节中写的类。

Python2:

#!/usr/bin/env python
# coding=utf-8

class Person(object):
    """
    This is a sample of class.
    """

    def __init__(self, name):
        self.name = name

    def get_name(self):
        return self.name

    def color(self, color):
        d = {}
        d[self.name] = color
        return d

Python 3:

class Person:
    """
    This is a sample of class.
    """

    def __init__(self, name):
        self.name = name

    def get_name(self):
        return self.name

    def color(self, color):
        d = {}
        d[self.name] = color
        return d

实例还是用girl = Person('canglaoshi'),当然,你建立别的实例也是可以的。利用一个类,可以建立无限多个实例,所以有一种说法,类是实例的工厂。

创建实例,就是调用类。当类被调用之后:

  1. 创建实例对象;
  2. 检查是否有——专业的说法:是否实现——__init__()方法。如果没有,则返回实例对象;
  3. 如果有__init__(),则调用该方法,并且将实例对象作为第一个参数self传递进去

__init__()作为一个特殊方法,是比较特殊的,在它里面,一般是规定一些属性或者做一些初始化要让类具有的一些特征,但是,它没有return语句。观察别的方法都是可以有这个的。

class Foo:
    def __init__(self):
        print("I am in init()")
        return 1

bar = Foo()

假如写了上面的代码,在初始化函数中有return语句,又会如何?

I am in init()
Traceback (most recent call last):
  File "F:/MyGitHub/StarterLearningPython/2code/20801p3.py", line 7, in <module>
    bar = Foo()
TypeError: __init__() should return None, not 'int'

这就是运行结果,出现了异常,并且明确告知“init() should return None”,所以不能有return,如果非要有,也得是return None,索性就不要写了。

由此可知,对于__init__()初始化函数,除了第一个参数是并且必须有self、不能有return语句这两个特征之外,其它方面和普通函数就没有什么异样了。比如参数和里面的属性,你可以这样来做:

class Person:
    def __init__(self, name, lang="golang", website="www.google.com"):
        self.name = name
        self.lang = lang
        self.website = website
        self.email = "[email protected]"

实例创建好之后,就要研究关于实例的内容,首先看实例属性

实例属性

与类属性类似,实例所具有的属性叫做“实例属性”。

还是用那个简单的类,虽然有点枯燥。

>>> class A(object):    #Python 3: class A:
...         x = 7
... 
>>> foo = A()

类已经有一个属性A.x = 7,那么由这个类所建立的实例也具有这个属性。

>>> foo.x
7

除了foo.x这个属性之外,实例也具有其它的属性和方法,依然能使用dir()方法来查看。

>>> dir(foo)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x']

不同Python版本显示结果稍微有差异,但是那个名为x的属性是都有的,这点上还是跟类属性类似的。并且实例也有一些特殊属性,比如:

>>> foo.__dict__
mappingproxy({'__dict__': <attribute '__dict__' of 'A' objects>, '__doc__': None, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__module__': '__main__', 'x': 7})

实例属性和类属性的一个最大不同,在于实例属性可以随意更改,不用有什么担心(前面我们建议,尽可能不要修改类属性)。

>>> foo.x += 1
>>> foo.x
8

这是把实例属性修改了。但是,类属性并没有因为实例属性修改而变化,正如前文所讲,类属性是跟类绑定,不受实例影响。

>>> A.x
7

上述结果说明“类属性与实例属性无关”。

那么,foo.x += 1的本质是什么呢?

其本质是该实例foo又建立了一个新的属性,但是这个属性(新的foo.x)居然与原来的属性(旧的foo.x)重名,所以,原来的foo.x就被“遮盖了”,只能访问到新的foo.x,它的值是8.

>>> foo.x
8
>>> del foo.x
>>> foo.x
7

既然新的foo.x“遮盖”了旧的foo.x,如果删除它,旧的不就显现出来了?的确是。删除之后,foo.x就还是原来的值。此外,还可以通过建立一个不与它重名的实例属性:

>>> foo.y = foo.x + 1
>>> foo.y
8
>>> foo.x
7

foo.y就是新建的一个实例属性,它没有影响原来的实例属性foo.x。其实,在这里你也完全可以依照变量和对象的关系来理解上述实例属性和数值(对象)的关系。

但是,类属性能够影响实例属性,这是因为实例就是通过调用类来建立的。

>>> A.x += 1
>>> A.x
8
>>> foo.x
8

如果是同一个属性x,实例属性跟着类属性而改变。

以上所言,是指当类中变量引用的是不可变数据。

如果类中变量引用可变数据,情形会有所不同。因为可变数据能够进行原地修改。

>>> class B(object):
...     y = [1, 2, 3]

这次定义的类中,变量引用的是一个可变对象。

>>> B.y         #类属性
[1, 2, 3]
>>> bar = B()
>>> bar.y       #实例属性
[1, 2, 3]

>>> bar.y.append(4)
>>> bar.y
[1, 2, 3, 4]
>>> B.y
[1, 2, 3, 4]

>>> B.y.append("aa")
>>> B.y
[1, 2, 3, 4, 'aa']
>>> bar.y
[1, 2, 3, 4, 'aa']

从上面的比较操作中可以看出,当类中变量引用的是可变对象是,类属性和实例属性都能直接修改这个对象,从而影响另一方的值。

如果增加一个类属性,相应的也增加了一个实例属性,

>>> A.y = "hello"
>>> foo.y
'hello'

反过来,如果增加通过实例增加属性呢?看下面:

>>> foo.z = "python"
>>> foo.z
'python'
>>> A.z
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'A' has no attribute 'z'

类并没有收纳这个属性。

以上所显示的实例属性或者类属性,都源自于类中的变量所引用的值,或者说是静态数据,尽管能够通过类或者实例增加新的属性,其值也是静态的。

还有一类实例属性的生成方法,就是在实例创建的时候,通过__init__()初始化函数建立,这种建立则是动态。

self的作用

类里面的任何方法,第一个参数是self,但是在创建实例的时候,似乎没有这个参数什么事儿(不显示地写出来),那么self是干什么的呢?

self是一个很神奇的参数。

将前文的Person类简化一下,

#!/usr/bin/env python
# coding=utf-8

class Person(object):        #Python 3: class Person:
    def __init__(self, name):
        self.name = name
        print self                        #Python 3: print(self)
        print type(self)           #Python 3: print(type(self))

其它部分省略。

当创建实例时候,首先要执行构造函数,同时就打印新增的两条。结果是:

>>> girl = Person("canglaoshi")
<__main__.Person object at 0x0000000003146C50>
<class '__main__.Person'>

这说明self就是类Person的实例,再看看刚刚建立的那个实例girl

>>> girl
<__main__.Person object at 0x0000000003146C50>
>>> type(girl)
<class '__main__.Person'>

selfgirl所引用的实例对象一样。

当创建实例的时候,实例变量作为第一个参数,被Python解释器悄悄地传给了self,所以我们说在初始化函数中的self.name就是实例的属性。

注意,self.name中的name和初始化函数的参数name没有任何关系,它们两个一样,只不过是一种起巧合(经常巧合,其实是为了省事和以后识别方便,故意让它们巧合),或者说是写代码的人懒惰,不想另外取名字而已,无他。当然,如果写成self.xxxooo = name,也是可以的。

>>> girl.name
'canglaoshi'

这是我们得到的实例属性,但是,在类的外面不能这样用:

>>> self.name
Traceback (most recent call last):
  File "<pyshell#23>", line 1, in <module>
    self.name
NameError: name 'self' is not defined

数据流转

将类实例化,通过实例来执行各种方法,应用实例的属性,是最常见的操作。

所以,对此过程中的数据流转一定要弄明白。

把前文的类再稍微修改,如下:

#!/usr/bin/env python
# coding=utf-8

class Person(object):        #Python 3: class Person:
    def __init__(self, name):
        self.name = name

    def getName(self):
        return self.name

    def breast(self, n):
        self.breast = n

    def color(self, color):
        print "%s is %s" % (self.name, color)          #Python 3: print("{0} is {1}".format(self.name, color))

    def how(self):
        print "%s breast is %s" % (self.name, self.breast)        #Python 3: print("{0} breast is {1}".format(self.name, self.breast))

girl = Person('canglaoshi')
girl.breast(90)

girl.color("white")
girl.how()

运行后结果:

$ python 20701.py 
canglaoshi is white
canglaoshi breast is 90

一图胜千言,有图有真相。通过图示,我们看一看数据的流转过程。

创建实例girl = Person('canglaoshi'),注意观察图上的箭头方向。girl这个引用实例对象的变量传给了self,即self也引用了实例对象。简化理解为:self是实例(不求准确,只求表面现象),实例变量主外,self主内。

"canglaoshi"是一个具体的数据,通过初始化函数中的name参数,传给self.name——准确地说谁传了对象引用给实例的属性name。前面已经讲过,self是一个实例,可以为它设置属性,self.name就是一个属性,经过初始化函数,这个属性的值由参数name传入,现在就是"canglaoshi"

在类Person的其它方法中,都是以self为第一个或者唯一一个参数。注意,在Python中,这个参数要显明写上,在类内部是不能省略的。这就表示所有方法都承接self实例对象,它的属性也被带到每个方法之中。例如在方法里面使用self.name即是调用前面已经确定的实例属性数据。当然,在方法中,还可以继续为实例self增加属性,比如self.breast。这样,通过self实例,就实现了数据在类内部的流转。

如果要把数据从类里面传到外面,可以通过return语句实现。如上例子中所示的getName方法。

因为引用实例对象的变量girlself,所以,在类里面也可以用girl代替self。例如,做如下修改:

#!/usr/bin/env python
# coding=utf-8

class Person(object):    #Python 3: class Person:
    def __init__(self, name):
        self.name = name

    def getName(self):
        #return self.name
        return girl.name    #修改成这个样子,但是在编程实践中不要这么做。

girl = Person('canglaoshi')
name = girl.getName()
print name

运行之后,打印:

canglaoshi

这个例子说明,在实例化之后,girlself都引用了实例对象。但是,提醒读者,千万不要用上面的修改了的那个方式。因为那样写使类没有独立性,这是大忌。


总目录   |   上节:类(2)   |   下节:类(4)

如果你认为有必要打赏我,请通过支付宝:[email protected],不胜感激。

results matching ""

    No results matching ""