Python算法的分享(三)

定义函数

之前的过程抽象例子调用了Python数学模块中的sqrt函数来计算平方根。通常来说,可以通过定义函数来隐藏任何计算的细节。函数的定义需要一个函数名、一系列参数以及一个函数体。函数也可以显式地返回一个值。例如,下面定义的简单函数会返回传入值的平方。

>>> def square(n):
...    return n**2
...
>>> square(3)
9
>>> square(square(3))
81
>>>

这个函数定义包含函数名square以及一个括号包含的形式参数列表。在这个函数中,n是唯一的形式参数,这意味着square只需要一份数据就能完成任务。计算n**2并返回结果的细节被隐藏起来。如果要调用square函数,可以为其提供一个实际参数值(在本例中是3),并要求Python环境计算。注意,square函数的返回值可以作为参数传递给另一个函数调用。

通过运用著名的牛顿迭代法,可以自己实现平方根函数。用于近似求解平方根的牛顿迭代法使用迭代计算的方法来求解正确的结果。

n e w g u e s s = 1 2 × ( o l d g u e s s + n o l d g u e s s ) newguess=\frac{1}{2}\times(oldguess+\frac{n}{oldguess}) newguess=21×(oldguess+oldguessn)

以上公式接受一个值n,并且通过在每一次迭代中将newguess赋值给oldguess来反复猜测平方根。初次猜测的平方根是n/2。代码清单1-1展示了该函数的定义,它接受值n并且返回20轮迭代之后的n的平方根。牛顿迭代法的细节都被隐藏在函数定义之中,用户不需要知道任何实现细节就可以调用该函数来求解平方根。代码清单1-1同时也展示了#的用法。任何跟在#之后一行内的字符都是注释。Python解释器不会执行这些注释。

通过牛顿迭代法求解平方根
1.  def squareroot(n):
2.      root = n/2 #initial guess will be 1/2 of n
3.      for k in range(20):
4.          root = (1/2)*(root + (n / root))
5.
6.      return root
>>> squareroot(9)
3.0
>>> squareroot(4563)
67.549981495186216
>>>

Python面向对象编程:定义类

前文说过,Python是一门面向对象的编程语言。到目前为止,我们已经使用了一些內建的类来展示数据和控制结构的例子。面向对象编程语言最强大的一项特性是允许程序员(问题求解者)创建全新的类来对求解问题所需的数据进行建模。

我们之前使用了抽象数据类型来对数据对象的状态及行为进行逻辑描述。通过构建能实现抽象数据类型的类,可以利用抽象过程,同时为真正在程序中运用抽象提供必要的细节。每当需要实现抽象数据类型时,就可以创建新类。

1 Fraction类

要展示如何实现用户定义的类,一个常用的例子是构建实现抽象数据类型Fraction的类。我们已经看到,Python提供了很多数值类。但是在有些时候,需要创建“看上去很像”分数的数据对象。

3 5 \frac{3}{5} 53这样的分数由两部分组成。上面的值称作分子,可以是任意整数。下面的值称作分母,可以是任意大于0的整数(负的分数带有负的分子)。尽管可以用浮点数来近似表示分数,但我们在此希望能精确表示分数的值。

Fraction对象的表现应与其他数值类型一样。我们可以针对分数进行加、减、乘、除等运算,也能够使用标准的斜线形式来显示分数,比如3/5。此外,所有的分数方法都应该返回结果的最简形式。这样一来,不论进行何种运算,最后的结果都是最简分数。

在Python中定义新类的做法是,提供一个类名以及一整套与函数定义语法类似的方法定义。以下是一个方法定义框架。

class Fraction:

   # 方法定义

所有类都应该首先提供构造方法。构造方法定义了数据对象的创建方式。要创建一个Fraction对象,需要提供分子和分母两部分数据。在Python中,构造方法总是命名为__init__(即在init的前后分别有两个下划线),如代码清单1-2所示。

Fraction类及其构造方法
1.  class Fraction:
2.
3.      def __init__(self, top, bottom):
4.
5.          self.num = top
6.          self.den = bottom

注意,形式参数列表包含3项。self是一个总是指向对象本身的特殊参数,它必须是第一个形式参数。然而,在调用方法时,从来不需要提供相应的实际参数。如前所述,分数需要分子与分母两部分状态数据。构造方法中的self.num定义了Fraction对象有一个叫作num的内部数据对象作为其状态的一部分。同理,self.den定义了分母。这两个实际参数的值在初始时赋给了状态,使得新创建的Fraction对象能够知道其初始值。

要创建Fraction类的实例,必须调用构造方法。使用类名并且传入状态的实际值就能完成调用(注意,不要直接调用__init__)。

myfraction = Fraction(3,5)

以上代码创建了一个对象,名为myfraction,值为3/5。图1-5展示了这个对象。

在这里插入图片描述

Fraction类的一个实例

接下来实现这一抽象数据类型所需要的行为。考虑一下,如果试图打印Fraction对象,会发生什么呢?

>>> myf = Fraction(3,5)
>>> print(myf)
<__main__.Fraction instance at 0x409b1acc>

Fraction对象myf并不知道如何响应打印请求。print函数要求对象将自己转换成一个可以被写到输出端的字符串。myf唯一能做的就是显示存储在变量中的实际引用(地址本身)。这不是我们想要的结果。

有两种办法可以解决这个问题。一种是定义一个show方法,使得Fraction对象能够将自己作为字符串来打印。代码清单1-3展示了该方法的实现细节。如果像之前那样创建一个Fraction对象,可以要求它显示自己(或者说,用合适的格式将自己打印出来)。不幸的是,这种方法并不通用。为了能正确打印,我们需要告诉Fraction类如何将自己转换成字符串。要完成任务,这是print函数所必需的。

show方法
1.  def show(self):
2.      print(self.num, "/", self.den)
>>> myf = Fraction(3,5)

>>> myf.show()
3/5
>>> print(myf)
<__main__.Fraction instance at 0x40bce9ac>
>>>

Python的所有类都提供了一套标准方法,但是可能没有正常工作。其中之一就是将对象转换成字符串的方法__str__。这个方法的默认实现是像我们之前所见的那样返回实例的地址字符串。我们需要做的是为这个方法提供一个“更好”的实现,即重写默认实现,或者说重新定义该方法的行为。

为了达到这一目标,仅需定义一个名为__str__的方法,并且提供新的实现,如代码清单1-4所示。除了特殊参数self之外,该方法定义不需要其他信息。新的方法通过将两部分内部状态数据转换成字符串并在它们之间插入字符/来将分数对象转换成字符串。一旦要求Fraction对象转换成字符串,就会返回结果。注意该方法的各种用法。

__str__方法
1.  def __str__(self):
2.      return str(self.num) + "/" + str(self.den)
>>> myf = Fraction(3,5)
>>> print(myf)
3/5
>>> print("I ate", myf, "of the pizza")
I ate 3/5 of the pizza
>>> myf.__str__()
'3/5'
>>> str(myf)
'3/5'
>>>

可以重写Fraction类中的很多其他方法,其中最重要的一些是基本的数学运算。我们想创建两个Fraction对象,然后将它们相加。目前,如果试图将两个分数相加,会得到下面的结果。

>>> f1 = Fraction(1,4)
>>> f2 = Fraction(1,2)
>>> f1+f2

Traceback (most recent call last):
  File "<pyshell#173>", line 1, in -toplevel-
    f1+f2
TypeError: unsupported operand type(s) for +:
          'instance' and 'instance'
>>>

如果仔细研究这个错误,会发现加号+无法处理Fraction的操作数。

可以通过重写Fraction类的__add__方法来修正这个错误。该方法需要两个参数。第一个仍然是self,第二个代表了表达式中的另一个操作数。

f1.__add__(f2)

以上代码会要求Fraction对象f1将Fraction对象f2加到自己的值上。可以将其写成标准表达式:f1 + f2。

两个分数需要有相同的分母才能相加。确保分母相同最简单的方法是使用两个分母的乘积作为分母。

a b + c d = a d b d + c b b d = a d + c b b d \frac{a}{b}+\frac{c}{d}=\frac{ad}{bd}+\frac{cb}{bd}=\frac{ad+cb}{bd} ba+dc=bdad+bdcb=bdad+cb

代码清单1-5展示了具体实现。__add__方法返回一个包含分子和分母的新Fraction对象。可以利用这一方法来编写标准的分数数学表达式,将加法结果赋给变量,并且打印结果。值得注意的是,第3行中的\称作续行符。当一条Python语句被分成多行时,需要用到续行符。

__add__方法
1.  def __add__(self, otherfraction):
2.
3.      newnum = self.num * otherfraction.den + \
4.                 self.den * otherfraction.num
5.      newden = self.den * otherfraction.den
6.
7.      return Fraction(newnum, newden)
>>> f1 = Fraction(1, 4)
>>> f2 = Fraction(1, 2)
>>> f3 = f1 + f2
>>> print(f3)
6/8
>>>

虽然这一方法能够与我们预想的一样执行加法运算,但是还有一处可以改进。1/4+1/2的确等于6/8,但它并不是最简分数。最好的表达应该是3/4。为了保证结果总是最简分数,需要一个知道如何化简分数的辅助方法。该方法需要寻找分子和分母的最大公因数(greatest common divisor,GCD),然后将分子和分母分别除以最大公因数,最后的结果就是最简分数。

要寻找最大公因数,最著名的方法就是欧几里得算法。欧几里得算法指出,对于整数m和n,如果m能被n整除,那么它们的最大公因数就是n。然而,如果m不能被n整除,那么结果是n与m除以n的余数的最大公因数。代码清单1-6提供了一个迭代实现。注意,这种实现只有在分母为正的时候才有效。对于Fraction类,这是可以接受的,因为之前已经定义过,负的分数带有负的分子,其分母为正。

gcd函数
1.  def gcd(m,n):
2.      while m%n != 0:
3.          oldm = m
4.          oldn = n
5.
6.          m = oldn
7.          n = oldm%oldn
8.      return n

现在可以利用这个函数来化简分数。为了将一个分数转化成最简形式,需要将分子和分母都除以它们的最大公因数。对于分数6/8,最大公因数是2。因此,将分子和分母都除以2,便得到3/4。代码清单1-7展示了实现细节。

改良版__add__方法
1.  def __add__(self, otherfraction):
2.      newnum = self.num * otherfraction.den + \
3.                  self.den * otherfraction.num
4.      newden = self.den * otherfraction.den
5.      common = gcd(newnum, newden)
6.      return Fraction(newnum//common, newden//common)
>>> f1 = Fraction(1,4)
>>> f2 = Fraction(1,2)
>>> f3 = f1 + f2
>>> print(f3)
3/4
>>>

Fraction对象现在有两个非常有用的方法,如图1-6所示。为了允许两个分数互相比较,还需要添加一些方法。假设有两个Fraction对象,f1和f2。只有在它们是同一个对象的引用时,f1 == f2才为True。这被称为浅相等,如图1-7所示。在当前实现中,分子和分母相同的两个不同的对象是不相等的。

在这里插入图片描述

包含两个方法的Fraction实例

在这里插入图片描述

浅相等与深相等

通过重写__eq__方法,可以建立深相等——根据值来判断相等,而不是根据引用。__eq__是又一个在任意类中都有的标准方法。它比较两个对象,并且在它们的值相等时返回True,否则返回False。

在Fraction类中,可以通过统一两个分数的分母并比较分子来实现__eq__方法,如代码清单1-8所示。需要注意的是,其他的关系运算符也可以被重写。例如,__le__方法提供判断小于等于的功能。

__eq__方法
1.  def __eq__(self, other):
2.      firstnum = self.num * other.den
3.      secondnum = other.num * self.den
4.
5.      return firstnum == secondnum

代码清单1-9提供了到目前为止Fraction类的完整实现。剩余的算术方法及关系方法留作练习。

Fraction类的完整实现
1.   class Fraction:
2.       def __init__(self, top, bottom):
3.           self.num = top
4.           self.den = bottom
5.
6.       def __str__(self):
7.           return str(self.num) + "/" + str(self.den)
8.
9.       def show(self):
10.          print(self.num, "/", self.den)
11.
12.      def __add__(self, otherfraction):
13.          newnum = self.num * otherfraction.den + \
14.                      self.den * otherfraction.num
15.          newden = self.den * otherfraction.den
16.          common = gcd(newnum, newden)
17.          return Fraction(newnum//common, newden//common)
18.
19.      def __eq__(self, other):
20.          firstnum = self.num * other.den
21.          secondnum = other.num * self.den
22.
23.          return firstnum == secondnum
2继承:逻辑门与电路

最后一节介绍面向对象编程的另一个重要方面。继承使一个类与另一个类相关联,就像人们相互联系一样。孩子从父母那里继承了特征。与之类似,Python中的子类可以从父类继承特征数据和行为。父类也称为超类。

图1-8展示了內建的Python集合类以及它们的相互关系。我们将这样的关系结构称为继承层次结构。举例来说,列表是有序集合的子。因此,我们将列表称为子,有序集合称为父(或者分别称为子类列表和超类序列)。这种关系通常被称为IS-A关系(IS-A意即列表是一个有序集合)。这意味着,列表从有序集合继承了重要的特征,也就是内部数据的顺序以及诸如拼接、重复和索引等方法。

在这里插入图片描述

Python集合类的继承层次结构

列表、字符串和元组都是有序集合。它们都继承了共同的数据组织和操作。不过,根据数据是否同类以及集合是否可修改,它们彼此又有区别。子类从父类继承共同的特征,但是通过额外的特征彼此区分。

通过将类组织成继承层次结构,面向对象编程语言使以前编写的代码得以扩展到新的应用场景中。此外,这种结构有助于更好地理解各种关系,从而更高效地构建抽象表示。

为了进一步探索这个概念,我们来构建一个模拟程序,用于模拟数字电路。逻辑门是这个模拟程序的基本构造单元,它们代表其输入和输出之间的布尔代数关系。一般来说,逻辑门都有单一的输出。输出值取决于提供的输入值。

与门(AND gate)有两个输入,每一个都是0或1(分别代表False和True)。如果两个输入都是1,那么输出就是1;如果至少有一个输入是0,那么输出就是0。或门(OR gate)同样也有两个输入。当至少有一个输入为1时,输出就为1;当两个输入都是0时,输出是0。

非门(NOT gate)与其他两种逻辑门不同,它只有一个输入。输出刚好与输入相反。如果输入是0,输出就是1。反之,如果输入是1,输出就是0。图1-9展示了每一种逻辑门的表示方法。每一种都有一张真值表,用于展示输入与输出的对应关系。

3种逻辑门

在这里插入图片描述

通过不同的模式将这些逻辑门组合起来并提供一系列输入值,可以构建具有逻辑功能的电路。图1-10展示了一个包含两个与门、一个或门和一个非门的电路。两个与门的输出直接作为输入传给或门,然后其输出又输入给非门。如果在4个输入处(每个与门有两个输入)提供一系列值,那么非门就会输出结果。图1-10也展示了这一过程。

电路示例

在这里插入图片描述

为了实现电路,首先需要构建逻辑门的表示。可以轻松地将逻辑门组织成类的继承层次结构,如图1-11所示。顶部的LogicGate类代表逻辑门的通用特性:逻辑门的标签和一个输出。下面一层子类将逻辑门分成两种:有一个输入的逻辑门和有两个输入的逻辑门。再往下,就是具体的逻辑门。

逻辑门的继承层次结构

在这里插入图片描述

现在开始通过实现最通用的类LogicGate来实现这些类。如前所述,每一个逻辑门都有一个用于识别的标签以及一个输出。此外,还需要一些方法,以便用户获取逻辑门的标签。

所有逻辑门还需要能够知道自己的输出值。这就要求逻辑门能够根据当前的输入值进行合理的逻辑运算。为了生成结果,逻辑门需要知道自己对应的逻辑运算是什么。这意味着需要调用一个方法来进行逻辑运算。代码清单1-10展示了LogicGate类的完整实现。

超类LogicGate
1.   class LogicGate:
2.
3.       def __init__(self, n):
4.           self.label = n
5.           self.output = None
6.
7.       def getLabel(self):
8.           return self.label
9.
10.      def getOutput(self):
11.          self.output = self.performGateLogic()
12.          return self.output

目前还不用实现performGateLogic函数。原因在于,我们不知道每一种逻辑门将如何进行自己的逻辑运算。这些细节会交由继承层次结构中的每一个逻辑门来实现。这是一种在面向对象编程中非常强大的思想——我们创建了一个方法,而其代码还不存在。参数self是指向实际调用方法的逻辑门对象的引用。任何添加到继承层次结构中的新逻辑门都仅需要实现之后会被调用的performGateLogic函数。一旦实现完成,逻辑门就可以提供运算结果。扩展已有的继承层次结构并提供使用新类所需的特定函数,这种能力对于重用代码来说非常重要。

我们依据输入的个数来为逻辑门分类。与门和或门有两个输入,非门只有一个输入。BinaryGate是LogicGate的一个子类,并且有两个输入。UnaryGate同样是LogicGate的子类,但是仅有一个输入。在计算机电路设计中,这些输入被称作“引脚”(pin),我们在实现中也使用这一术语。

代码清单1-11和代码清单1-12实现了这两个类。两个类中的构造方法首先使用super函数来调用其父类的构造方法。当创建BinaryGate类的实例时,首先要初始化所有从LogicGate中继承来的数据项,在这里就是逻辑门的标签。接着,构造方法添加两个输入(pinA和pinB)。这是在构建类继承层次结构时常用的模式。子类的构造方法需要先调用父类的构造方法,然后再初始化自己独有的数据。

BinaryGate类
  class BinaryGate(LogicGate):
2.
3.       def __init__(self, n):
4.           super().__init__(n)
5.
6.           self.pinA = None
7.           self.pinB = None
8.
9.       def getPinA(self):
10.          return int(input("Enter Pin A input for gate " + \
11.                             self.getLabel() + "-->"))
12.
13.      def getPinB(self):
14.          return int(input("Enter Pin B input for gate " + \
15.                             self.getLabel() + "-->"))
UnaryGate类
1.   class UnaryGate(LogicGate):
2.
3.       def __init__(self, n):
4.           super().__init__(n)
5.
6.           self.pin = None
7.
8.       def getPin(self):
9.           return int(input("Enter Pin input for gate " + \
10.                             self.getLabel() + "-->"))

BinaryGate类增添的唯一行为就是取得两个输入值。由于这些值来自于外部,因此通过一条输入语句来要求用户提供。UnaryGate类也有类似的实现,不过它只有一个输入。

有了不同输入个数的逻辑门所对应的通用类之后,就可以为有独特行为的逻辑门构建类。例如,由于与门需要两个输入,因此AndGate是BinaryGate的子类。和之前一样,构造方法的第一行调用父类(BinaryGate)的构造方法,该构造方法又会调用它的父类(LogicGate)的构造方法。注意,由于继承了两个输入、一个输出和逻辑门标签,因此AndGate类并没有添加任何新的数据。

AndGate类唯一需要添加的是布尔运算行为。这就是提供performGateLogic的地方。对于与门来说,performGateLogic首先需要获取两个输入值,然后只有在它们都为1时返回1。代码清单1-13展示了AndGate类的完整实现。

AndGate类
1.   class AndGate(BinaryGate):
2.
3.       def __init__(self, n):
4.           super().__init__(n)
5.
6.       def performGateLogic(self):
7.
8.           a = self.getPinA()
9.           b = self.getPinB()
10.          if a==1 and b==1:
11.              return 1
12.          else:
13.              return 0

可以创建一个实例来验证AndGate类的行为。下面的代码展示了AndGate对象g1,它有一个内部标签“G1”。当调用getOutput方法时,该对象必须首先调用它的performGateLogic方法,这个方法会获取两个输入值。一旦取得输入值,就会显示正确的结果。

>>> g1 = AndGate("G1")
>>> g1.getOutput()
Enter Pin A input for gate G1-->1
Enter Pin B input for gate G1-->0
0

或门和非门都能以相同的方式来构建。OrGate也是BinaryGate的子类,NotGate则会继承UnaryGate类。由于计算逻辑不同,这两个类都需要提供自己的performGateLogic函数。

要使用逻辑门,可以先构建这些类的实例,然后查询结果(这需要用户提供输入)。

>>> g2 = OrGate("G2")
>>> g2.getOutput()
Enter Pin A input for gate G2-->1
Enter Pin B input for gate G2-->1
1
>>> g2.getOutput()
Enter Pin A input for gate G2-->0
Enter Pin B input for gate G2-->0
0
>>> g3 = NotGate("G3")
>>> g3.getOutput()
Enter Pin input for gate G3-->0
1

有了基本的逻辑门之后,便可以开始构建电路。为此,需要将逻辑门连接在一起,前一个的输出是后一个的输入。为了做到这一点,我们要实现一个叫作Connector的新类。

Connector类并不在逻辑门的继承层次结构中。但是,它会使用该结构,从而使每一个连接器的两端都有一个逻辑门(如图1-12所示)。这被称为HAS-A关系(HAS-A意即“有一个”),它在面向对象编程中非常重要。前文用IS-A关系来描述子类与父类的关系,例如UnaryGate是一个LogicGate。

连接器将一个逻辑门的输出与另一个逻辑门的输入连接起来

在这里插入图片描述

Connector与LogicGate是HAS-A关系。这意味着连接器内部包含LogicGate类的实例,但是不在继承层次结构中。在设计类时,区分IS-A关系(需要继承)和HAS-A关系(不需要继承)非常重要。

代码清单1-14展示了Connector类。每一个连接器对象都包含fromgate和togate两个逻辑门实例,数据值会从一个逻辑门的输出“流向”下一个逻辑门的输入。对setNextPin的调用(实现如代码清单1-15所示)对于建立连接来说非常重要。需要将这个方法添加到逻辑门类中,以使每一个togate能够选择适当的输入。

Connector类
1.   class Connector:
2.
3.       def __init__(self, fgate, tgate):
4.           self.fromgate = fgate
5.           self.togate = tgate
6.
7.           tgate.setNextPin(self)
8.
9.       def getFrom(self):
10.          return self.fromgate
11.
12.      def getTo(self):
13.          return self.togate
setNextPin方法
1.      def setNextPin(self, source):
2.          if self.pinA == None:
3.              self.pinA = source
4.          else:
5.              if self.pinB == None:
6.                  self.pinB = source
7.              else:
8.                  raise RuntimeError("Error: NO EMPTY PINS")

在BinaryGate类中,逻辑门有两个输入,但连接器必须只连接其中一个。如果两个都能连接,那么默认选择pinA。如果pinA已经有了连接,就选择pinB。如果两个输入都已有连接,则无法连接逻辑门。

现在的输入来源有两个:外部以及上一个逻辑门的输出。这需要对方法getPinA和getPinB进行修改(请参考代码清单1-16)。如果输入没有与任何逻辑门相连接(None),那就和之前一样要求用户输入。如果有了连接,就访问该连接并且获取fromgate的输出值。这会触发fromgate处理其逻辑。该过程会一直持续,直到获取所有输入并且最终的输出值成为正在查询的逻辑门的输入。在某种意义上,这个电路反向工作,以获得所需的输入,再计算最后的结果。

修改后的getPinA方法
1.  def getPinA(self):
2.      if self.pinA == None:
3.          return input("Enter Pin A input for gate " + \
4.                             self.getName() + "-->")
5.      else:
6.          return self.pinA.getFrom().getOutput()

下面的代码段构造了图1-10中的电路。

>>> g1 = AndGate("G1")
>>> g2 = AndGate("G2")
>>> g3 = OrGate("G3")
>>> g4 = NotGate("G4")
>>> c1 = Connector(g1, g3)
>>> c2 = Connector(g2, g3)
>>> c3 = Connector(g3, g4)

两个与门(g1和g2)的输出与或门(g3)的输入相连接,或门的输出又与非门(g4)的输入相连接。非门的输出就是整个电路的输出。

>>> g4.getOutput()
Pin A input for gate G1-->0
Pin B input for gate G1-->1
Pin A input for gate G2-->1
Pin B input for gate G2-->1
0
>>>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值