类实例

到现在为止,我们学习了定义新对象类,提供其名字,属性,和方法(也就是函数属性)的语法。当我们离开类定义的作用域外,一个类对象将被创建——定义创建的类对象(class object)是单个包装我们类定义的对象。我们很少会想要在类对象创建后将其作为参数输入或操作它。反而,我们将想要使用它来创建该类的一个个实例(instance)。具体来讲,list 是一个类对象(请记住,“类”和“类型”是一个意思)——它和 class 定义执行后创建的对象是同一种类的。就如你在模组2中看到的一样,我们可以使用这个类对象来创建列表类单独的实例,而每个实例有着属于自己的对象序列。

# 使用类对象 `list` 来创建列表实例
>>> list()  # 创建一个空的 `list` 实例
[]

>>> list((1, 2, 3))  # 创建一个包括1,2,和3的 `list` 实例
[1, 2, 3]

# `a` 和 `b` 是两个不同的列表类/类型实例,虽然它们储存着相同的
# 整数序列
>>> a = list((1, 2, 3))
>>> b = list((1, 2, 3))
>>> a is b
False

>>> isinstance(a, list)
True

>>> isinstance(b, list)
True

# 对 `a` 调用append方法只会影响该列表实例。
>>> a.append(-1)
>>> a
[1, 2, 3, -1]

>>> b
[1, 2, 3]

以上每一个实例都共享着属性 appendcountreverse,等等,如被 list 类对象包装的列表类定义描述得一般。虽然如此,每个列表的内容是一个属于该列表实例的属性;也就是说,列表的成员是一个实例属性(instance attribute)而不是一个类属性(class attribute)。到现在为止,我们还没有办法创建实例级别的属性。让我们改变这一事实。

假设我们想要创建自己的 Person 类。每个人都应该有着自己的名字,所以名字应该是实例属性。我们将学习如何定义一个特殊的初始化方法来允许我们定义并设置实例属性。在 Person 的情况下,这将允许我们为每一个人提供自己的名字:

>>> class Person:
...    def __init__(self, name):
...        self.name = name

>>> emmy = Person("Emmy")
>>> niels = Person("Niels")

>>> emmy.name
'Emmy'

>>> niels.name
'Niels'

>>> isinstance(emmy, Person)
True

我们接下来会马上学习 __init__ 方法和奇怪的 self 参数。首先,我们将学习如何使用类对象来创建实例对象。

对象身份和创建实例

我们将在这里学习创建对象实例的基本方法。设以下简单的类定义:

class Dummy:
    x = 1

我们可以对 Dummy 使用“调用”语法 Dummy() 来创建该类的单独实例:

# 创建一个为 Dummy 类实例的对象
>>> d1 = Dummy()

>>> isinstance(d1, Dummy)
True

请回忆,is 操作符检查两个变量是否引用了电脑内存中一样的对象。同时,请回忆,内置的 isinstance 函数会检查一个对象是否是某一类/类型的实例。这些函数将帮助我们理解类对象,类实例,和对象引用之间的关系。

# `Dummy` 是一个包含我们类定义的类对象
>>> Dummy
__main__.Dummy

# `d1` 是我们 Dummy 类的一个实例。
# 这个实例存在于某内存地址中(0x2ae8f68f2e8)
>>> d1
<__main__.Dummy at 0x2ae8f68f2e8>

# d1 不是 Dummy;它是 Dummy 的一个实例
>>> d1 is Dummy
False

>>> isinstance(d1, Dummy)
True

请注意,Dummyd1 的关系就像 list[1, 4, "a"] 的关系一样。

让我们创建另外一额 Dummy 的实例。理解这个新实例和之前创建的实例是不同的很重要。

# `d2` 是我们 Dummy 类的一个新实例。
# 它存在于某内存地址中(0x2ae8f666f60)
>>> d2 = Dummy()
>>> d2
<__main__.Dummy at 0x2ae8f666f60>

>>> d2 is d1  # `d2` 和 `d1` 是 `Dummy` 的不同实例
False

>>> isinstance(d2, Dummy)
True

Python的变量引用对象的规则在此依然成立:向某个变量赋值一个对象,不管是类对象还是其实例,都不会创建该对象的复制品。这个变量仅仅会引用该对象并作为它的一个代名词。

# `A` 引用 `Dummy`
>>> A = Dummy

>>> A is Dummy
True

# 使用 `A` 来创建 `Dummy` 的实例
>>> dummy_instance = A()
>>> dummy_instance
<__main__.Dummy at 0x2ae8f65fcf8>

>>> isinstance(dummy_instance, A)  # 等值于:`isinstance(dummy_instance, Dummy)`
True

# `var` 引用 Dummy 的实例 `dummy_instance`
>>> var = dummy_instance

>>> var is dummy_instance
True

# 向 `var.x` 设置一个新值等值于向 `dummy_instance.x`
# 设置一个新值
>>> var.x = 22
>>> dummy_instance.x
22

阅读理解:类初始化

使用以上定义的 Dummy 类,创建一个包含10个这一类的不同实例的列表。编写代码来显式检查每一个成员互不相同,以及每个成员都是 Dummy 类的实例。

然后,创建一个包含一个 Dummy 实例被分别储存10次的元组。编写代码来显式检查每一个成员都引用相同的对象,以及每个成员都是 Dummy 类的实例。

阅读理解:术语

设:

>>> class Cat:
...     pass

>>> x = Cat
>>> y = Cat()
>>> x1 = x
>>> x2 = x()
>>> y1 = y

xCat 共享着什么关系?

yCat 共享着什么关系?

xy 共享着什么关系?

x2y 共享着什么关系?

yy1 共享着什么关系?

接下来,指出以下每个对象是类对象还是类实例(如果是实例,是哪个类的实例)?

  • "hello world"

  • True

  • int

  • {"a" : 22}

  • tuple

在学习了如何创建类实例,理解类对象和实例的关系,并理解了不同实例之间的区别后,我们可以学习如何为我们的类创建实例级别的属性了。

定义实例级别的属性:__init__ 方法

就像这一节之前用 Person 类和本模组简介节定义的 Rectangle 类演示的一样,Python提供了一个特殊方法 __init__ 来允许我们定义类的实例级别的属性。这是一个极其重要的,我们会经常使用的方法。请注意这个方法的名字是:“下划线-下划线-init-下划线-下划线”,而你可以将其读作“dunder-init”(“dunder”代表着“double-underscore”)。

设以下稍微修改过的 Person 定义,其同时包含了类属性 x

class Person:
    x = 1  # 这设置了一个类级别的属性,通用于所有 `Person` 实例

    def __init__(self, name):
        """ 本方法在每次创建一个新 `Person` 实例时将被执行。
            `self` 指向被创建的对象实例。"""
        self.name = name   # 为 Person 实例 `self` 设置属性 `name`

        # __init__ 不能反悔任何除了 `None` 以外的对象。它的唯一目的是修改
        # `self`,也就是被创建的 `Person` 实例。

调用 Person() 其实在“幕后”调用 __init__(),且任何输入到 Person() 中的参数会被送到 __init__ 中。乍一眼看 __init__ 的定义,我们好像需要向这个方法输入两个参数:selfname。第一个参数 self 其实代表着正在被创建的 Person 对象实例。Python会自动向 __init__self 输入相应对象,所以我们只需要担心向其输入 name 的值。

让我们创建 Person 类的一个实例,并使用字符串 "Kamasi" 来作为名字:

# 创建实例 `self`,将其和 `"Kamasi"` 输入到 `Person.__init__` 中,
# 然后返回被创建的实例对象
>>> p = Person("Kamasi")

>>> p.name  # 访问实例属性 `name`
'Kamasi'
>>> p.x     # 访问类属性 `x`
1

当我们创建 Person 的一个实例时,以下是“幕后”发生的事情(这很重要):

  • 调用 Person("Kamasi") 首先会假装 __init__ 方法不存在一样创建 Person 的一个实例。返回的对象在这时并没有 name 属性。它只拥有类级别的属性 x

  • 然后,Person 的那个实例会作为参数 self 被输入到 __init__ 中,而我们显式提供的 "Kamasi" 将会作为参数 name 被输入。

  • 有了这些参数后,Person.__init__(self, "Kamasi") 将会执行它的主体代码。具体来讲,self.name = name 将使用值 "Kamasi"self 设置属性。

  • 在执行完 __init__ 方法后,Person("Kamasi") 会返回被创建的实例对象。

我们现在有了在实例级别上定义和设置属性的能力!理解这个过程对掌握Python的面向对象编程至关重要。让我们创建多个 Person 实例并将它们存储在一个列表中:

# 创建多个 `Person` 的实例
>>> list_of_people = [Person(n) for n in ("Fermi", "Noether", "Euler")]

>>> for person in list_of_people:
...    print(person.name)
...    print(person.x)
Fermi
1
Noether
1
Euler
1

更新 Person 的类级别的属性 x 会影响所有 Person 的实例:

# 为类属性 `x` 设置一个新值
>>> Person.x = 22

# 这会影响 `Person` 的所有实例
>>> for person in list_of_people:
...    print(person.name)
...    print(person.x)
Fermi
22
Noether
22
Euler
22

阅读理解:实例属性

定义类 Tmp,其有着三个实例属性:xy,和 zxy 应为根据在实例创建时输入的值设定的数字,而 z 应为这两个值的乘积。

比如说:

>>> tmp = Tmp(2.1, 3.0)
>>> tmp.x
2.1

>>> tmp.y
3.0

>>> tmp.z
6.3

现在,你应该对特殊的 __init__ 方法能如何用来为你的类定义和设置实例级别的属性有一定的了解了。同时,调用类实例化会如何创建实例对象并将其作为 self 参数自动输入到 __init__ 的过程也应该很明显易懂。在接下来的一节,我们将会遇到三种不同的方法:实例方法,类方法,和静态(static)方法。我们还会遭遇类似于 __init__ 的所谓“特殊方法”(special method);我们可以使用它们来更加广泛地描述你的类的行为和它如何和Python的操作符交互。

阅读理解答案:

解:类初始化

使用以上定义的 Dummy 类,创建一个包含10个这一类的不同实例的列表。

# 会在每次迭代中调用 `Dummy()` 一次
>>> list_of_dummies = [Dummy() for i in range(10)]

# 请注意以下不相同的内存地址
>>> list_of_dummies
[<__main__.Dummy at 0x1d50de89940>,
 <__main__.Dummy at 0x1d50de896d8>,
 <__main__.Dummy at 0x1d50de897b8>,
 <__main__.Dummy at 0x1d50de89a20>,
 <__main__.Dummy at 0x1d50de89ac8>,
 <__main__.Dummy at 0x1d50de89a58>,
 <__main__.Dummy at 0x1d50de899e8>,
 <__main__.Dummy at 0x1d50de89a90>,
 <__main__.Dummy at 0x1d50de89b00>,
 <__main__.Dummy at 0x1d50de89b38>]

编写代码来显式检查每一个成员互不相同,以及每个成员都是 Dummy 类的实例。

>>> from itertools import combinations
# `combinations(list_of_dummies, 2)` 会循环 `list_of_dummies` 中所有
# 成员对
>>> all(a is not b for a,b in combinations(list_of_dummies, 2))
True

>>> all(isinstance(a, Dummy) for a in list_of_dummies)
True

创建一个包含一个 Dummy 实例被分别储存10次的元组。请注意在这里,我们仅仅初始化了 Dummy 一次,而元组理解语句仅仅向元组中输入了10次相同的实例。

>>> dummy = Dummy()  # 一个 `Dummy` 实例
>>> tuple_of_dummy = tuple(dummy for i in range(10))

# 请注意所有的内存地址都是相同的
>>> tuple_of_dummy
(<__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>,
 <__main__.Dummy at 0x1d50de887b8>)

编写代码来显式检查每一个成员都引用相同的对象,以及每个成员都是 Dummy 类的实例。

>>> all(dummy is i for i in tuple_of_dummy)
True

>>> all(isinstance(a, Dummy) for a in tuple_of_dummy)
True

解:术语

设:

>>> class Cat:
...     pass

>>> x = Cat
>>> y = Cat()
>>> x1 = x
>>> x2 = x()
>>> y1 = y

xCat 共享着什么关系?:xCat 引用相同的类对象。

yCat 共享着什么关系?:yCat 类的一个实例。

xy 共享着什么关系?:x 引用 Cat,而 yCat 的一个实例。因此,yx 的一个实例。

x2y 共享着什么关系?:它们是 Cat 的不同实例。

yy1 共享着什么关系?:它们引用相同的 Cat 实例。

接下来,指出以下每个对象是类对象还是类实例(如果是实例,是哪个类的实例)?

  • "hello world"str 类型(也就是类)的一个实例

  • Truebool 类型的一个实例

  • int:一个描述整数的类对象

  • {"a" : 22}dict 类型的一个实例

  • tuple:一个描述元组的类对象

解:实例属性

定义类 Tmp,其有着三个实例属性:xy,和 zxy 应为根据在实例创建时输入的值设定的数字,而 z 应为这两个值的乘积。

class Tmp:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.z = x * y
>>> test = Tmp(8, 5)
>>> test.x
8
>>> test.y
5
>>> test.z
40