教程:Python和Sage中的对象和类

本教程是对Python和Sage中的面向对象编程的介绍。它需要有关命令式/过程性编程(最常见的编程风格)的基本知识--即条件指令、循环、函数(请参阅Sage教程的“编程”部分)--但不假定有关于对象和类的进一步知识。它被设计为正式介绍和练习的交替序列。 练习的解决方案 都在最后给出了。

前言:变量、名称和对象

作为一种面向对象的语言,对于习惯于命令式语言(如C或Maple)的人来说,Python的“变量”行为可能会令人惊讶。原因是他们是 not variables but names

下面的解释是 borrowed from David Goodger

其他语言也有“变量”

在许多其他语言中,为变量赋值会将值放入框中。

int a = 1;
_images/a1box.png

框“a”现在包含整数1。

将另一个值赋给同一变量将替换框中的内容:

a = 2;
_images/a2box.png

现在框“a”包含一个整数2。

将一个变量赋给另一个变量会复制该值并将其放入新框中:

int b = a;
_images/b2box.png _images/a2box.png

“b”是第二个盒子,有一个整数2的拷贝。盒子“a”有一个单独的拷贝。

Python 有“名字”

在Python中,“名称”或“标识符”就像是贴在对象上的地块标签(或名称标签)。

a = 1
_images/a1tag.png

这里,一个整数1对象有一个标记为“a”的标签。

如果我们重新赋值为“a”,我们只需将标记移动到另一个对象:

a = 2
_images/a2tag.png _images/1.png

现在,名称“a”被附加到一个整数2对象。

原来的INTEGER 1对象不再有标记“a”。它可能会继续存在,但我们不能通过“a”这个名字到达它。(当对象不再有引用或标记时,它将从内存中删除。)

如果我们将一个名字分配给另一个名字,我们只是将另一个名字标签附加到现有对象:

b = a
_images/ab2tag.png

名称“b”只是与“a”绑定到同一对象的第二个标签。

尽管我们通常甚至在Python中也提到“变量”(因为这是常见的术语),但我们实际上指的是“名称”或“标识符”。在Python中,“变量”是值的名称标签,而不是标有标签的框。

警告

因此,当有 two tags “a”和“b” same object ,修改标记为“b”的对象也会修改标记为“a”的对象::

sage: a = [1,2,3]
sage: b = a
sage: b[1] = 0
sage: a
[1, 0, 3]

请注意,重新分配标记“b”(而不是修改带有该标记的对象)不会影响标记为“a”的对象::

sage: b = 7
sage: b
7
sage: a
[1, 0, 3]

面向对象编程范型

这个 object-oriented programming 范式依赖于以下两条基本规则:

  1. 任何需要由计算机操纵的真实(或数学)世界的任何东西都由一个 object

  2. 每个对象都是一个 instance 一些人 class

在这一点上,这两条规则有点没有意义,所以让我们或多或少地给出一些准确的术语定义:


object

一个 portion of memory 其中包含对真实世界的事物进行建模所需的信息。

class

定义 data structure 用于将作为类实例的对象与其 behavior


让我们从一些例子开始:我们考虑向量空间 QQ 其基由排列和其中的特定元素索引:

sage: F = CombinatorialFreeModule(QQ, Permutations())
sage: el = 3*F([1,3,2])+ F([1,2,3])
sage: el
B[[1, 2, 3]] + 3*B[[1, 3, 2]]

(对于每个排列,例如 [1, 3, 2] 中的相应元素 F 表示为 B[[1, 3, 2]] --在 CombinatorialFreeModule ,如果元素由 x ,则默认情况下其打印表示为 B[x] 。)

在Python中,一切都是对象,所以类型和类之间没有任何区别。可以获取对象的类 el 发信人::

sage: type(el)
<class 'sage.combinat.free_module.CombinatorialFreeModule_with_category.element_class'>

因此,这不是很有信息量。我们稍后再来讨论这个问题。与对象相关联的数据存储在所谓的 attributes 。它们是通过语法访问的 obj.attribute_name 。对于组合自由模的元素,主属性称为 _monomial_coefficients 。它是一个将系数与指数相关联的词典:

sage: el._monomial_coefficients
{[1, 2, 3]: 1, [1, 3, 2]: 3}

修改属性会修改对象:

sage: el._monomial_coefficients[Permutation([3,2,1])] = 1/2
sage: el
B[[1, 2, 3]] + 3*B[[1, 3, 2]] + 1/2*B[[3, 2, 1]]

警告

作为用户,您是 not 应该自己做这样的修改(请参阅 private attributes (见下文)。

作为向量空间的一个元素, el 具有特定的行为::

sage: 2*el
2*B[[1, 2, 3]] + 6*B[[1, 3, 2]] + B[[3, 2, 1]]
sage: sorted(el.support())
[[1, 2, 3], [1, 3, 2], [3, 2, 1]]
sage: el.coefficient([1, 2, 3])
1

The behavior is defined through methods (support, coefficient). Note that this is true even for equality, printing or mathematical operations. For example, the call a == b actually is translated to the method call a.__eq__(b). The names of those special methods which are usually called through operators are fixed by the Python language and are of the form __name__. Examples include __eq__ and __le__ for operators == and <=, __repr__ (see 关于课程的SAGE细节) for printing, __add__ and __mult__ for operators + and *. See http://docs.python.org/library/ for a complete list.

sage: el.__eq__(F([1,3,2]))
False
sage: el.__repr__()
'B[[1, 2, 3]] + 3*B[[1, 3, 2]] + 1/2*B[[3, 2, 1]]'
sage: el.__mul__(2)
2*B[[1, 2, 3]] + 6*B[[1, 3, 2]] + B[[3, 2, 1]]

备注

我们现在创建一个自定义 Element 类来解释属性在Python中的工作方式的详细信息(您可以忽略 parent 在下面的代码中,这在这里不相关)::

sage: from sage.structure.element import Element
sage: class MyElt(Element):
....:     def __init__(self, parent, val):
....:         super(MyElt, self).__init__(parent)
....:         self.value = val
sage: el = MyElt(val=42, parent=ZZ)
sage: el
Generic element of a structure

该类存储在一个名为 __class__ ,并且普通属性存储在名为 __dict__ **

sage: el.__dict__
{'value': 42}
sage: el.__class__
<class '__main__.MyElt'>

某些特定操作会修改 el **

sage: el.rename("bla")
sage: el
bla
sage: el.__dict__
{'_SageObject__custom_name': 'bla', 'value': 42}

很多Sage对象不是Python对象,而是编译后的Cython对象。Python将它们视为内置对象,您无权访问它们的一些内部数据结构。例如,基类 Element 存储的父级 el 作为Cython属性 _parent 但它不会出现在 __dict__ 而且我们不能从Python访问它。

Cython类的一些示例(从技术上讲, extension types )包括整数和置换群元素。这些产品都没有 __dict__ 根本没有::

sage: e = Integer(9)
sage: type(e)
<class 'sage.rings.integer.Integer'>
sage: e.__dict__
Traceback (most recent call last):
...
AttributeError: 'sage.rings.integer.Integer' object has no attribute '__dict__'...

sage: id4 = SymmetricGroup(4).one()
sage: type(id4)
<class 'sage.groups.perm_gps.permgroup_element.SymmetricGroupElement'>
sage: id4.__dict__
Traceback (most recent call last):
...
AttributeError: 'sage.groups.perm_gps.permgroup_element.SymmetricGroupElement' object has no attribute '__dict__'...

备注

每个对象对应于内存的一部分,称为其 identity 在Python中。您可以使用以下命令获取身份 id **

sage: el = Integer(9)
sage: id(el)  # random
139813642977744
sage: el1 = el; id(el1) == id(el)
True
sage: el1 is el
True

在Python中(因此在Sage中也是如此),具有相同身份的两个对象将是相等的,但反之亦然。因此,恒等式函数不同于数学恒等式:

sage: el2 = Integer(9)
sage: el2 == el1
True
sage: el2 is el1
False
sage: id(el2) == id(el)
False

摘要

要定义某个对象,首先必须编写一个 class 。该类将定义对象的方法和属性。

method

与对象相关联的特定类型的函数,用于获取有关该对象的信息或对其进行操作。

attribute

存储有关对象的信息的变量。

一个例子:餐馆里的一杯饮料

让我们写一个关于餐厅玻璃杯的小班::

sage: class Glass():
....:     def __init__(self, size):
....:         assert size > 0
....:         self._size = float(size)  # an attribute
....:         self._content = float(0.0)  # another attribute
....:     def __repr__(self):
....:         if self._content == 0.0:
....:             return "An empty glass of size %s"%(self._size)
....:         else:
....:             return "A glass of size %s cl containing %s cl of water"%(
....:                     self._size, self._content)
....:     def fill(self):
....:         self._content = self._size
....:     def empty(self):
....:         self._content = float(0.0)

让我们创建一个小玻璃::

sage: myGlass = Glass(10); myGlass
An empty glass of size 10.0
sage: myGlass.fill(); myGlass
A glass of size 10.0 cl containing 10.0 cl of water
sage: myGlass.empty(); myGlass
An empty glass of size 10.0

以下是一些评论:

  1. 类的定义 Glass 定义了两个属性, _size_content 。它定义了四种方法, __init____repr__fill ,以及 empty 。(此类的任何实例还将具有从该类继承的其他属性和方法 object 。看见 Inheritance (如下所示。)

  2. 方法 __init__ 用于初始化对象:它由所谓的 constructor 调用时执行的类的 Glass(10)

  3. 方法 __repr__ 返回用于打印对象的字符串,例如在本例中计算 myGlass

备注

Private Attributes

  • 大多数情况下,为了确保数据结构的一致性,用户不应该直接更改对象的某些属性。这些属性称为 private 。由于在Python中没有确保隐私的机制,因此约定如下:私有属性的名称以下划线开头。

  • 因此,只能通过方法进行属性访问。读取或写入私有属性的方法称为访问器。

  • 仅供内部使用的方法也带有下划线前缀。

习题

  1. 添加方法 is_empty 如果杯子是空的,则返回TRUE。

  2. 定义一种方法 drink 使用一个参数 amount 这使得人们可以部分喝掉杯子里的水。如果一个人要求喝比杯子里的水更多的水或负量的水,就会出现错误。

  3. 允许在玻璃杯中装满葡萄酒、啤酒或其他饮料。方法 fill 应接受参数 beverage 。饮料存储在属性中 _beverage 。更新方法 __repr__ 相应地。

  4. 添加属性 _clean 和方法 is_cleanwash 。在创造的时候,杯子是干净的,一装满就变脏了,必须洗干净才能变得干净。

  5. 测试所有的东西。

  6. 确保所有东西都经过了测试。

  7. 再测试一次所有东西。

继承

问题:目标: different 类可以共享一个 common behavior

例如,如果一个人想要处理不同的盘子(叉子、勺子等),那么就会有常见的行为(弄脏和被清洗)。因此,与不同种类的菜肴相关的不同等级应该是相同的 cleanis_cleanwash 方法:研究方法。但复制和粘贴代码非常不利于维护:错误会被复制,要想更改任何东西,必须记住所有副本的位置。因此,需要一种允许程序员分解常见行为的机制。它被称为 inheritancesub-classing :编写一个基类,该基类分解公共行为,然后重用该类中的方法。

我们首先编写一个小类‘’AbstractDish‘’,它实现“清理-脏-洗”行为::

sage: class AbstractDish():
....:     def __init__(self):
....:         self._clean = True
....:     def is_clean(self):
....:         return self._clean
....:     def state(self):
....:         return "clean" if self.is_clean() else "dirty"
....:     def __repr__(self):
....:         return "An unspecified %s dish"%self.state()
....:     def _make_dirty(self):
....:         self._clean = False
....:     def wash(self):
....:         self._clean = True

现在,您可以在类中重用此行为 Spoon **

sage: class Spoon(AbstractDish):  # Spoon inherits from AbstractDish
....:     def __repr__(self):
....:         return "A %s spoon"%self.state()
....:     def eat_with(self):
....:         self._make_dirty()

让我们测试一下::

sage: s = Spoon(); s
A clean spoon
sage: s.is_clean()
True
sage: s.eat_with(); s
A dirty spoon
sage: s.is_clean()
False
sage: s.wash(); s
A clean spoon

摘要

  1. 任何类都可以重复使用另一个类的行为。一种说法是,这个子类 inherits 从超类或它的 derives 从它那里。

  2. 子类的任何实例也是其超类的实例::

    sage: type(s)
    <class '__main__.Spoon'>
    sage: isinstance(s, Spoon)
    True
    sage: isinstance(s, AbstractDish)
    True
    
  3. 如果一个子类重新定义了一个方法,那么它将替换前一个方法。一种说法是,这个子类 overloads 该方法。不过,可以显式调用隐藏的超类方法。

    sage: s.__repr__()
    'A clean spoon'
    sage: Spoon.__repr__(s)
    'A clean spoon'
    sage: AbstractDish.__repr__(s)
    'An unspecified clean dish'
    

备注

Advanced superclass method call

有时,人们想要调用重载方法,而不知道它是在哪个类中定义的。为此,请使用 super 操作员::

sage: super(Spoon, s).__repr__()
'An unspecified clean dish'

此构造的一个非常常见的用法是调用 __init__ 超类的方法::

sage: class Spoon(AbstractDish):
....:     def __init__(self):
....:         print("Building a spoon")
....:         super(Spoon, self).__init__()
....:     def __repr__(self):
....:         return "A %s spoon"%self.state()
....:     def eat_with(self):
....:         self._make_dirty()
sage: s = Spoon()
Building a spoon
sage: s
A clean spoon

习题

  1. 修改类 Glasses 这样它就继承了 Dish

  2. 写一个类 Plate 它的实例可以包含与类一起的任何用餐 Fork 。尽可能避免代码重复(提示:您可以编写分解的类 ContainerDish )。

  3. 测试所有的东西。

关于课程的SAGE细节

与Python相比,Sage有特殊的对象处理方式:

  • Sage中的任何数学对象类都应该继承自 SageObject 而不是来自 object 。大多数情况下,它们实际上继承自一个子类,比如 ParentElement

  • 打印应通过以下方式完成 _repr_ 而不是 __repr__ 以允许重命名。

  • 更一般地,特定于SAGE的特殊方法通常被命名为 _meth_ 而不是 __meth__ 。例如,许多类实现了 _hash_ 它由使用和缓存 __hash__ 。同样,组的元素通常实现 _mul_ ,这样就不需要注意强迫行为,因为它们是在 __mul__

有关详细信息,请参阅《Sage开发人员指南》。

练习的解决方案

  1. 以下是第一个练习的解决方案:

    sage: class Glass():
    ....:     def __init__(self, size):
    ....:         assert size > 0
    ....:         self._size = float(size)
    ....:         self.wash()
    ....:     def __repr__(self):
    ....:         if self._content == 0.0:
    ....:             return "An empty glass of size %s"%(self._size)
    ....:         else:
    ....:             return "A glass of size %s cl containing %s cl of %s"%(
    ....:                     self._size, self._content, self._beverage)
    ....:     def content(self):
    ....:         return self._content
    ....:     def beverage(self):
    ....:         return self._beverage
    ....:     def fill(self, beverage = "water"):
    ....:         if not self.is_clean():
    ....:             raise ValueError("Don't want to fill a dirty glass")
    ....:         self._clean = False
    ....:         self._content = self._size
    ....:         self._beverage = beverage
    ....:     def empty(self):
    ....:         self._content = float(0.0)
    ....:     def is_empty(self):
    ....:         return self._content == 0.0
    ....:     def drink(self, amount):
    ....:         if amount <= 0.0:
    ....:             raise ValueError("amount must be positive")
    ....:         elif amount > self._content:
    ....:             raise ValueError("not enough beverage in the glass")
    ....:         else:
    ....:             self._content -= float(amount)
    ....:     def is_clean(self):
    ....:         return self._clean
    ....:     def wash(self):
    ....:         self._content = float(0.0)
    ....:         self._beverage = None
    ....:         self._clean = True
    
  2. 让我们检查一下一切是否按预期运行::

    sage: G = Glass(10.0)
    sage: G
    An empty glass of size 10.0
    sage: G.is_empty()
    True
    sage: G.drink(2)
    Traceback (most recent call last):
    ...
    ValueError: not enough beverage in the glass
    sage: G.fill("beer")
    sage: G
    A glass of size 10.0 cl containing 10.0 cl of beer
    sage: G.is_empty()
    False
    sage: G.is_clean()
    False
    sage: G.drink(5.0)
    sage: G
    A glass of size 10.0 cl containing 5.0 cl of beer
    sage: G.is_empty()
    False
    sage: G.is_clean()
    False
    sage: G.drink(5)
    sage: G
    An empty glass of size 10.0
    sage: G.is_clean()
    False
    sage: G.fill("orange juice")
    Traceback (most recent call last):
    ...
    ValueError: Don't want to fill a dirty glass
    sage: G.wash()
    sage: G
    An empty glass of size 10.0
    sage: G.fill("orange juice")
    sage: G
    A glass of size 10.0 cl containing 10.0 cl of orange juice
    
  3. 以下是第二个练习的解决方案:

    sage: class AbstractDish():
    ....:     def __init__(self):
    ....:         self._clean = True
    ....:     def is_clean(self):
    ....:         return self._clean
    ....:     def state(self):
    ....:         return "clean" if self.is_clean() else "dirty"
    ....:     def __repr__(self):
    ....:         return "An unspecified %s dish"%self.state()
    ....:     def _make_dirty(self):
    ....:         self._clean = False
    ....:     def wash(self):
    ....:         self._clean = True
    
    
    sage: class ContainerDish(AbstractDish):
    ....:     def __init__(self, size):
    ....:         assert size > 0
    ....:         self._size = float(size)
    ....:         self._content = float(0)
    ....:         super(ContainerDish, self).__init__()
    ....:     def content(self):
    ....:         return self._content
    ....:     def empty(self):
    ....:         self._content = float(0.0)
    ....:     def is_empty(self):
    ....:         return self._content == 0.0
    ....:     def wash(self):
    ....:         self._content = float(0.0)
    ....:         super(ContainerDish, self).wash()
    
    
    sage: class Glass(ContainerDish):
    ....:     def __repr__(self):
    ....:         if self._content == 0.0:
    ....:             return "An empty glass of size %s"%(self._size)
    ....:         else:
    ....:             return "A glass of size %s cl containing %s cl of %s"%(
    ....:                     self._size, self._content, self._beverage)
    ....:     def beverage(self):
    ....:         return self._beverage
    ....:     def fill(self, beverage = "water"):
    ....:         if not self.is_clean():
    ....:             raise ValueError("Don't want to fill a dirty glass")
    ....:         self._make_dirty()
    ....:         self._content = self._size
    ....:         self._beverage = beverage
    ....:     def drink(self, amount):
    ....:         if amount <= 0.0:
    ....:             raise ValueError("amount must be positive")
    ....:         elif amount > self._content:
    ....:             raise ValueError("not enough beverage in the glass")
    ....:         else:
    ....:             self._content -= float(amount)
    ....:     def wash(self):
    ....:         self._beverage = None
    ....:         super(Glass, self).wash()
    
  4. 让我们检查一下一切是否按预期运行::

    sage: G = Glass(10.0)
    sage: G
    An empty glass of size 10.0
    sage: G.is_empty()
    True
    sage: G.drink(2)
    Traceback (most recent call last):
    ...
    ValueError: not enough beverage in the glass
    sage: G.fill("beer")
    sage: G
    A glass of size 10.0 cl containing 10.0 cl of beer
    sage: G.is_empty()
    False
    sage: G.is_clean()
    False
    sage: G.drink(5.0)
    sage: G
    A glass of size 10.0 cl containing 5.0 cl of beer
    sage: G.is_empty()
    False
    sage: G.is_clean()
    False
    sage: G.drink(5)
    sage: G
    An empty glass of size 10.0
    sage: G.is_clean()
    False
    sage: G.fill("orange juice")
    Traceback (most recent call last):
    ...
    ValueError: Don't want to fill a dirty glass
    sage: G.wash()
    sage: G
    An empty glass of size 10.0
    sage: G.fill("orange juice")
    sage: G
    A glass of size 10.0 cl containing 10.0 cl of orange juice
    

这就是所有人了!