【Leccture 4.1】Classes and Objects - the Basics

Week by week

In week one the video lectures and activities from the Runestone textbook will cover the basics of classes and instances, which combine functions and data into a clear object structure. By the end of the week, you will have learned how to create custom classes and instances, and work them into a larger codebase.

在第一周,Runestone教科书中的视频讲座和活动将涵盖类和实例的基础知识,这些知识将函数和数据组合成清晰的对象结构。 到本周末,您将学习如何创建自定义类和实例,以及如何将它们用于更大的代码库。

In week two you will learn about class inheritance, which allows you to re-use class code more effectively. By the end of the week, you will understand how to create subclasses and superclasses. You will also learn more detail about how the Python interpreter looks up instance attributes and how to override methods and variables.

在第二周中,您将学习类继承,这将使您更有效地重用类代码。 到本周末,您将了解如何创建子类和超类。 您还将了解有关Python解释器如何查找实例属性以及如何覆盖方法和变量的更多详细信息。

In week three you will learn about exceptions and unit testing. By the end of the week, you will learn how to write test cases to be more confident that your code works as expected. You will also be able to write try/except clauses that allow you to handle runtime error and give you more control over how your code is executed.

在第三周中,您将了解异常和单元测试。 到本周结束时,您将学习如何编写测试用例,以更加确信自己的代码可以按预期工作。 您还将能够编写try / except子句,这些子句允许您处理运行时错误,并提供对代码执行方式的更多控制。


20.1. Introduction: Classes and Objects - the Basics

20.1.1. Object-oriented programming

Python is an object-oriented programming language. That means it provides features that support object-oriented programming (OOP).

到目前为止,我们一直在编写的程序使用 过程编程范例(use a procedural programming paradigm)。 在过程编程中,重点在于编写对数据(data)进行操作的功能或过程。 在面向对象的编程中,重点是创建同时包含数据和功能的对象( the creation of objects which contain both data and functionality)。

通常,每个 对象定义 对应于现实世界中的 某个对象或概念,并且对该对象进行操作的函数对应于现实世界对象的交互方式。

20.2. Objects Revisited

In Python, every value is actually an object. 在Python中,每个值实际上都是一个对象

无论是*字典,列表,还是整数,*它们都是对象。 程序通过与对象执行计算或要求它们执行方法(methods)来操纵这些对象。

To be more specific, we say that an object has a state and a collection of methods that it can perform.

一个对象的状态(state)代表了该对象对自身的了解。状态存储在实例变量(instance variables)中。例如,正如我们在 turtle object 上看到的,每只乌龟的状态(state)都由乌龟的位置,颜色,前进方向等组成。

每只海龟还具有前进,后退或右转或左转的能力。单个海龟的不同之处在于,尽管它们都是海龟(object),但它们在单个状态属性的具体值上是不同的(可能它们在不同的位置或有不同的标题)。 they differ in the specific values of the individual state attributes

属于同一个 object,区别是所具有的 实例变量的值不同

image-20200617203651473

20.3. User Defined Classes

我们已经见过这样的 classes: str, int, float and list。这些是由Python定义的,供我们使用。然而,在许多情况下,当我们解决问题时,我们需要创建与我们试图解决的问题相关的数据对象。我们需要创建自己的类。

例如,考虑数学点的概念。 在二维中,点是两个数字(坐标),它们被一起视为一个 object。 点通常用括号括起来,并用逗号分隔坐标。例如,(0,0) 表示原点,而 (x,y) 表示点 x 向右的单位,y 单位从原点起向上的单位长。 这(x,y) 是点 object 的状态 state。

考虑上面的图,我们可以绘制一个点对象,如下所示。

image-20200617203709824

与点相关联的一些典型操作可能是:求点的x坐标,getX,或者求点的y坐标,getY。可能还希望计算一个点到原点的距离,或者一个点到另一个点的距离,或者找到两点之间的中点,或者回答一个点是否落在给定的矩形或圆内的问题。这些称为 这个object 的 methods

image-20200617203734786

class Point:
    pass    # 类的主体

在我 fill 这个类之前我要做的是创建一个实例。如果你把一个类想成一个工厂,实例就是工厂生产的东西。方法是:

class Point:
    pass    # 类的主体

# 创建这个工厂的两个产品 instances:
point1 = Point()
point2 = Point()

尽管这看起来很像调用一个函数,这实际上与 calling the function 略有不同。因为前面是类的名字,然后是开括号和闭括号,因此这是创建这个Point类的一个实例。

现在,我们可以对instance做的第一件事是,我们可以设置一个instance variable。实例变量是一种存在于实例内部的变量。如果是 point1.x = 5,假设点 point2.x = 10。然后为“产品”:point1point2 各自设置了一个名为 x 的 instance variable。

现在我们来 fill 这个类:by defining what’s called a method. So a method is like a function except it belongs to a class. 例如我们定义一个 方法: getX(self) :方法和函数的一个最重要的区别是,方法总是至少有一个参数叫做self:

class Point:
    def getX(self):    # 类的主体
        return self.x  # 返回实例变量 x(或者理解为产品的x属性)
    
print(point1.get())    # 调用这个方法时没有 argument

当我们调用 point1.getX 这个方法(method)时,记住方法和函数之间的区别之一就是方法属于一个类。 因此,每当我们调用一个方法(method)时,我们都会在特定的实际实例上调用该方法==(So whenever we call a method we have a particular actually instance that we’re calling that method on.)==。

所以在这种情况下,我们不是只调用 getX,还要说的是 point1.getX()。当我们说 point1.getX() 时,实际发生的是当我们调用 getX 时,我们自动将这个实例 point1 作为方法的参数 self 传递。(we’re automatically passing in this instance point1 as self when we call getX

因此,当我们说 point1.getx()时,即使我们在这里没有参数,Python所做的是,它也会自动what’s before the dot or the instance调用,并把它作为self的值传递给方法传递进去给 argument。

既然我们理解了 a point object 的样子,我们就可以定义一个新的类了。我们希望每个点都有一个x和一个y属性(attribute),所以我们的第一个类定义是这样

class Point:
    '''Point class for representing and manipulating x,y coordinates.'''
    def __init__(self):
        """ Create a new point at the origin """
        self.x = 0
        self.y = 0

如果 class header 之后的第一行是字符串,则它将成为该类的文档字符串(docstring of the class),并会被各种工具识别。 (文档字符串也适用于函数)

This initializer method: 每个类都应该有一个特殊名称为__init__的方法(通常称为构造函数constructor function)。每当创建一个新的 Point 实例时,都会自动 call 这个初始化方法。

它为程序员提供了通过给定初始状态值 initial state values的方式 来设置新实例内所需属性的机会 set up the attributes required within the new instance。self 参数会自动设置为reference需要初始化的新创建的对象。

现在,让我们使用新的Point类。 如果您还记得关于Turtle图形的一章中有关如何创建Turtle类实例的某些语法,则下一部分应该看起来有点熟悉。

class Point:
    """ Point class for representing and manipulating x,y coordinates. """
    
    def __init__(self):
        
        self.x = 0
        self.y = 0
        
p = Ponit()   # Instantiate an object of type Point 类的实例化
q = Point()
print("Nothing seems to have happened with the points")

在对象的初始化过程中,我们为每个对象创建了两个名为x和y的属性,并给它们赋值0。你会注意到,当你运行程序时,什么都没有发生。但实际上并非如此。实际上,已经创建了两个 Point,每个Pointxy 坐标值为 0。但是,由于我们没有要求程序对这些 Point 做任何事情,因此看不到其他任何结果。实际上是:

image-20200617203809950

A ‘function’ like Point() that creates a new object instance is called a constructor.

用来创建一个新的对象的实例(产品)的类似 Point(<>) 的“函数”被称作一个构造器(constructor),而每当调用这个“函数”创建了这个类的实例时就会自动调用一个特殊的方法(method)__init__() :被称为类的构造函数或初始化方法。

将 class 视为制造 objects 的工厂可能会有所帮助。 class 本身不是点的 instance ,但是class 包含制作点实例的 machinery 。 每次调用构造函数__init__()时,您都在要求工厂为您创建一个新对象。 当对象离开生产线时,将执行其初始化方法以使用其出厂默认设置正确设置对象。(例如初始化 x=0, y =0)

image-20200617203845923

The combined process of “make me a new object” and “get its settings initialized to the factory default settings” is called instantiation. “给我制造一个新对象”和“将其设置初始化为出厂默认设置”的组合过程称为实例化。

class Point:
    """ Point class for representing and manipulating x,y coordinates. """
    
    def __init__(self):
        
        self.x = 0
        self.y = 0

image-20200617203905661

p = Point()         # Instantiate an object of type Point
q = Point()         # and make a second point

The execution of p = Point(), occurs at steps 3-5. First, at step 3, you can see that a empty instance of the class has been created, and is passed as the first (and only parameter) to the __init__ method.

当调用了 Point() 的时候,首先创建了这个类的 empty instance,并把这个 empty instance 传递到 __init__ 方法的第一参数self【第3步】

image-20200617203924545

执行该方法的代码,并将变量self绑定到该实例。(深色部分)

在第4步和第5步,将对空的产品 设置两个两个(出厂的)实例变量:x和y都设置为0。__init__方法未返回任何内容, but the point object itself is returned from the call to Point().

image-20200617203948495

thus, at step 7, p is bound to the new point that was created and initialized.

image-20200617204006020

跳到步骤14之前,p和q分别绑定到不同的Point实例。 即使x和y实例变量都设置为0,它们都是不同的对象。 因此,p是q的结果为False

20.4. Adding Parameters to the Constructor

到目前为止,我们的构造函数只能在位置(0,0)创建点。 如果我们想要在位置(7,6)创建一个(初始)点。 由于构造函数只是专门命名的“函数”,因此我们可以使用参数(如前所述)来提供特定信息。

通过将额外的参数放入__init__方法,可以使我们的类构造函数更通用,如本示例所示。

class Point:
     """ Point class for representing and manipulating x,y coordinates. """
        
        def __init__(self, initx, inity):
            # 给定任意初坐标
            self.x = initX
            self.y = initY
            
p = Point(7,6)

当实例(产品)point 被创建时,在实例变量x和y中将initXinitY的值分配给对象的状态state。

image-20200617204026496

在类的__init__方法中这是常见的事情:接受一些参数并将其保存为实例变量instance variables。 为什么这有用? 请记住,方法完成执行后,参数变量 parameter variables 将消失。 但是,实例变量仍然可以在对象实例具有句柄的任何位置访问。 这是保存那些在调用类构造函数时提供的初始值的方法。

稍后,您将看到__init__方法所做的类不仅仅是将参数保存为实例变量。 例如,它可能解析这些变量的内容并对它们进行一些计算,然后将结果存储在实例变量中。 它甚至可以建立Internet连接,下载一些内容,并将其存储在实例变量中

20.5. Adding Other Methods to a Class

使用像Point这样的class而不是像简单元组(7,6)这样的东西的主要优势现在变得很明显。我们可以向Point类中添加一些方法,这些方法是对点的合理操作

创建一个像Point这样的类给我们的程序和思维带来了大量的“组织力量”。我们可以将合理的操作和它们所应用的数据类型组合在一起,并且类的每个实例都可以有自己的状态 each instance of the class can have its own state

A method behaves like a function but it is invoked on a specific instance. (构造器也像一个function但是前面是类名)

For example, with a list bound to variable L: list是一个类, L 是一个实例,L.append(7)调用类内的方法 append(self, x),列表本身作为第一个参数,7 作为第二个参数。方法使用点标记法访问(dot notation)。这就是为什么L.append(7)有两个参数,尽管你可能认为它只有一个:存储在变量L中的列表是第一个参数值,而7是第二个参数值。

让我们添加两种简单的方法,让一个点向我们提供有关其状态的信息。 调用getX方法时,将返回x坐标的值。

由于我们已经知道如何编写返回值的函数,因此该方法的实现非常简单。需要注意的一点是,即使 getX 方法不需要任何其他参数信息来完成它的工作,但仍然有一个形式参数self。 如前所述,在类中定义的,对该类的对象进行操作的所有方法都将self作为其第一个参数。 同样,这用作对对象本身的引用,而对象本身又可以访问对象内部的状态数据。

class Point:
    """ Point class for representing and manipulating x,y coordinates. """
    
    def __init__(self, initX, initY):
        
        self.x = initX
        self.y = initY
        
    def getX(self):
        return self.x
    
    def getY(self):
        return self.y
p = Point(7,6)
print(p.getX()) # 调用对象方法
print(p.getY()) 
---------
7
6

注意,getX 方法只是从对象自身返回实例变量x的值。 换句话说,该方法的实现是转到对象本身的状态并获得x的值。 同样,getY 方法看起来几乎相同。

让我们添加另一个方法,distanceFromOrigin,来更好地了解方法是如何工作的。除了存储在实例变量中的数据(状态)之外,这个方法也不需要任何额外的信息来完成它的工作。它将执行更复杂的任务。

class Point:
    
    def __init__(self, initX, initY):
        self.x = initX
        self.y = initY
        
    def getX(self):
        return self.x
    def getY(self):
        return self.y
    
    def distanceFromOrigin(self):
        d = (self.x ** 2 + self.y**2) ** 0.5
        return d
    
p = Point(7,6)
print(p.distanceFromOrigin())
------
使用 class 来表示 data
cityNames = ['Detroit', 'Ann Arbor', 'Pitts', 'Mars', 'New York']
populations = [680250, 117070, 304391, 1683, 840600]
states = ['MI', 'MI', 'PA', 'PA', 'NY']

# 遍历多个列表,把对应的元素组成一个元组列表
city_tuples = zip(cityNames, populations, states) 
  1. 当我们想用 class 来表示这些数据的时候,我们首先要考虑 我们想要用 instance 存储什么数据?

    在这里我们假设 我们的每个实例需要存储着所有三个数据

  2. 然后我会问, what do I want every class to you represent? ?我想让 class 代表什么信息?

    在这种情况下,有一点显而易见,每个实例应该代表一个单独的城市,每个单独的城市都有一个名字,人口和州,因此我要称之为 city class。

cityNames = ['Detroit', 'Ann Arbor', 'Pitts', 'Mars', 'New York']
populations = [680250, 117070, 304391, 1683, 840600]
states = ['MI', 'MI', 'PA', 'PA', 'NY']

# 遍历多个列表,把对应的元素组成一个元组列表
city_tuples = zip(cityNames, populations, states) 

class City:
    def __init__(self, n, p, s):
        self.name = n
        self.population = p
        self.state = s
        
    # 定义返回一个 str 语句 包含我们想要的信息 ?
    def __str__(self):
        info = '{},{}(pop:{})'.format(self.name, self.state, self.population)

# 操作
cities = []
for city_tup in city_tuples:
    # 复习元组数据的赋值形式
    name, pop, state = city_tup
    # 创建一个类的实例
    city = City(name, pop, state) 
    # 打印这个类
    print(city) # 图一
    cities.append(city)
print(cities) # 图二 存疑 为什么输出的是类型

image-20200617204048195

image-20200617204111572

list comprehension 版本

cityNames = ['Detroit', 'Ann Arbor', 'Pitts', 'Mars', 'New York']
populations = [680250, 117070, 304391, 1683, 840600]
states = ['MI', 'MI', 'PA', 'PA', 'NY']

# 遍历多个列表,把对应的元素组成一个元组列表
city_tuples = zip(cityNames, populations, states) 

class City:
    def __init__(self, n, p, s):
        self.name = n
        self.population = p
        self.state = s
        
    # 定义返回一个 str 语句 包含我们想要的信息 ?
    def __str__(self):
        info = '{},{}(pop:{})'.format(self.name, self.state, self.population)

# list comprehension
cities = [City(n, p, s) for (n, p, s) in city_tuples if True]
print(cities)

20.6. Objects as Arguments and Parameters

您可以以通常的方式将对象作为参数传递给函数。

这是一个叫做距离的简单函数,它涉及到我们新的点对象。这个函数的作用是计算两点之间的距离

import math

class Point:
    """ Point class for representing and manipulating x,y coordinates. """
    def __init__(self, initX, initY):
        self.x = initX
        self.y = initY
        
    def getX(self):
        return self.x
    
    def getY(self):
        return self.Y
    
    def distanceFromOrigin(self):
        d = (self.x ** 2 + self.y ** 2) ** 0.5
        return d
    
# 将对象作为参数传递给函数
def distance(ponit1, point2):
    '''计算两点之间的距离'''
    xdiff = point2.getX() - point1.getX()
    ydiff = point2.getY() - point1.getY()
    
    # 数学公式
    dist = math.sqrt(xdiff**2 + ydiff**2)
    return dist

p = Point(4,3)
q = Point(0,0)
# 打印给定两点的距离 dist
print(distance(p,q))

20.7. __str__ 方法

当我们使用类和对象时,通常需要打印一个对象(即打印对象的状态)。 例如对于上面的例子,如果我们打印 instance 本身我们将会看到:

class Point:
    """ Point class for representing and manipulating x,y coordinates. """

    def __init__(self, initX, initY):

        self.x = initX
        self.y = initY

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distanceFromOrigin(self):
        return ((self.x ** 2) + (self.y ** 2)) ** 0.5


p = Point(7,6)
print(p)
<__main__.Point object>

上面显示的 print 函数生成点 p 的 a string representation。 Python默认功能告诉您 pPoint 类型的对象。 但是,它不会告诉您有关该点的特定状态(state)的任何信息,

但如果类定义中包含一个特殊的方法调用 __str__,我们可以改进这个表示。请注意,此方法使用与构造函数相同的命名约定,即名称前后有两个下划线。Python对特殊方法使用这种命名技术是很常见的。

__str__ 方法的作用是告诉Python在实际打印该对象时如何表示该对象。

__str__方法负责返回类的创建者所定义的一种字符串表示形式。换句话说,作为程序员,你可以选择一个Point (instance)在被打印时应该是什么样子。在这个例子里,我们决定__str__方法需要创建并返回一个字符串,字符串表示 将包括 xy 的值以及一些识别文本。

类的__str__方法 return 的字符串是当我们 print 该类的任何 instance 时所打印的字符串。因此,类的__str__方法返回的字符串通常应该包含instance variables的值。如果一个点的x值为3,y值为4,而另一个点的x值为5,y值为9,那么这两个点对象在打印时应该会有所不同,对吗?

class Point:
    """ Point class for representing and manipulating x,y coordinates. """

    def __init__(self, initX, initY):

        self.x = initX
        self.y = initY

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distanceFromOrigin(self):
        return ((self.x ** 2) + (self.y ** 2)) ** 0.5

    def __str__(self):
        return "x = {}, y = {}".format(self.x, self.y)
        # return 'Point({},{})'.format(self.x, self.y)

p = Point(7,6)
print(p)
x = 7, y = 6
#Point(7,6)

因此,在创建类时,您通常希望设置 __str__ 方法,以便在实际打印出任何特定实例时可以打印出更具可读性和可理解性的内容。

特殊下划线方法

假设我们已经有了一个类:

class Point:
    """ Point class for representing and manipulating x,y coordinates. """

    def __init__(self, initX, initY):

        self.x = initX
        self.y = initY
    def __str__(self):
        return 'Point({},{})'.format(self.x, self.y)

    
p1 = Point(-5, 10)
p2 = Point(15, 20)
print(p1)
print(p2)
# 因此,假设我们希望能够将两个点加在一起, 例如
print(p1 + p2) # 但是 python 并不知道怎么把两个 instance 相加

但是我们可以 override 覆写 __add__ 方法来告诉 python如何将这两个 instance 相加

	def __add__(self, otherPoint)
  1. self 是将自身作为第一个参数

  2. otherPoint 是另一个实例点

  3. 将设 print(p1 + p2)p1 = self, p2 = otherPoint

    image-20200617204140706def add(self, otherPoint):

    # 一个新的 Point 并且在print时会调用 __str__
    

    ​ return Point(p1.x + otherPoint.x,

    ​ p1.y + otherPoint.y)

print(p1 + p2)
---
Point(10,20)

同理我们也可以覆写 subtraction

	def __sub__(self, otherPoint)# 一个新的 Point 并且在print时会调用 __str__
    	return Point(p1.x - otherPoint.x, 
                     p1.y - otherPoint.y)  

还有许多可以被 override 的 methods 都是这种模式。

20.8. Instances as Return Values

函数和方法可以返回对象。这实际上并不新鲜,因为Python中的所有东西都是一个对象, we have been returning values for quite some time.。(您也可以返回对象实例的列表或元组等。)这里的区别在于,我们希望在类中的方法使用构造函数创建一个对象,然后将其作为方法的 值 返回。

return Point(mx, my)   # 方法返回一个新的对象

假设您有一个 a point object,并希望在它和其他目标点之间找到 midpoint 。 我们想编写一个方法 method,将其称为 halfway,这个Point的 method 将另一个 Point对象 用作参数,并返回位于该 point 与其接受作为输入的 target point 之间的一半作为新的 Point 实例。

class Point:

    def __init__(self, initX, initY):

        self.x = initX
        self.y = initY

    def getX(self):
        return self.x

    def getY(self):
        return self.y

    def distanceFromOrigin(self):
        return ((self.x ** 2) + (self.y ** 2)) ** 0.5
    
    def halfway(self, target):
        mx = (self.x + target.x) / 2
        my = (self.y + target.y) / 2
        return Point(mx, my)   # 方法返回一个新的对象
 
    def __str__(self):
        return "x = {}, y = {}".format(self.x, self.y)
    

    
p = Point(3,4)
q = Point(5,12)
mid = p.halfway(q) # should return a new Point that is halfway between p and q.

print(mid)
print(mid.getX())
print(mid.getY())
----
x = 4.0, y = 8.0
4.0
8.0

所得的Point实例:mid 的 x 值为4,y 值为8。我们也可以对 mid使用任何其他 method,因为它是Point对象。

20.9. Sorting Lists of Instances

您以前学习过如何对列表进行排序。对一个类的实例列表进行排序与对任何其他类型的对象列表进行排序没有根本的区别。 您应该只提供 a key function as a parameter to sorted 键函数作为要排序(或排序)的参数。

之前,已经了解了如何使用这种函数 对其他类型的对象列表进行排序。 例如,给定一个字符串列表,可以通过传递键参数(a key parameter)以其长度的升序 (in ascending order of their lengths)对其进行排序。

请注意,如果通过名称引用函数,则要在函数名称后加上括号,因为您需要函数对象本身。而不添加括号意味着,排序中的函数将负责调用该函数,并传递列表中的当前项目。 因此,在下面的示例中,我们编写key = len而不是key = len()

用作键参数的函数的特点是输入 列表中的元素返回数字

L = ["Cherry", "Apple", "Blueberry"]

print(sorted(L, key=len))
#alternative form using lambda, if you find that easier to understand
print(sorted(L, key= lambda x: len(x)))
['Apple', 'Cherry', 'Blueberry']
['Apple', 'Cherry', 'Blueberry']

当列表中的每个项目都是一个类的实例时,您需要提供一个将一个实例作为输入并返回数字的函数。 实例将按其编号排序。

L = [Fruit("Cherry", 10), Fruit("Apple", 5), Fruit("Blueberry", 20)]
class Fruit():
    def __init__(self, name, price):
        self.name = name
        self.price = price

for f in sorted(L, key=lambda x: x.price):
    print(f.name)

Apple
Cherry
Blueberry

有时,您会发现为类定义一个对实例中的数据进行计算的方法很方便

但我们的类太简单了,无法真正说明这一点。但是为了模拟它,我定义了一个方法 sort_priority,它只返回存储在 instance 中的 price。现在,方法 sort_priority 将一个instance作为输入并返回一个 number。因此,它符合为排序提供的 key parameter。

Here it can get a little confusing: to refer to that method, without actually invoking it, you can refer to Fruit.sort_priority. This is analogous to the code above that referred to len rather than invoking len().

class Fruit(): # ?括号
    def __init__(self, name, price):
        self.name = name
        self.price = price
        
    def sort_priority(self):
        return self.price
L = [Fruit("Cherry", 10), Fruit("Apple", 5), Fruit("Blueberry", 20)]
print('------按照价格排序, refer 一个 类的方法')
for f in sorted(L, key=Fruit.sort_priority):
    print(f.name)
    
print('----- 另一种方法------x 是,每一个单个的 fruit instance')
for f in sorted(L, key=lambda x: x.sort_priority())

key参数使用了类中定义的一个方法,注意返回数字,实现key参数为lambda函数同样的效果,

20.10. Class Variables and Instance Variables

您已经看到,一个类的每个实例都有自己的名称空间和自己的实例变量。Point类的两个实例各有自己的实例变量x。在一个实例中设置x不会影响另一个实例。

一个类也可以具有类变量。 类变量被设置为类定义的一部分。

例如,考虑以下版本的Point类。 在这里,我们添加了一个graph方法,该方法生成一个字符串,该字符串表示一个基于文本的小图表,在该图形上绘制了Point。 它不是一个非常漂亮的图形,部分是因为y轴像橡皮筋一样被拉伸,但是您可以从中得到灵感。

请注意,在第4行上有一个对printed_rep变量的赋值。它不在任何方法中。 这使其成为类变量。 它的访问方式与实例变量相同。 例如,在第16行,有对self.printed_rep的引用。 如果更改第4行,则让它在图形中Point的x,y坐标处打印不同的字符。

class Point:
    """ Point class for representing and manipulating x,y coordinates. """

    printed_rep = "*"

    def __init__(self, initX, initY):

        self.x = initX
        self.y = initY

    def graph(self):
        rows = []
        size = max(int(self.x), int(self.y)) + 2
        for j in range(size-1) :
            if (j+1) == int(self.y):
                special_row = str((j+1) % 10) + (" "*(int(self.x) -1)) + self.printed_rep
                rows.append(special_row)
            else:
                rows.append(str((j+1) % 10))
        rows.reverse()  # put higher values of y first
        x_axis = ""
        for i in range(size):
            x_axis += str(i % 10)
        rows.append(x_axis)

        return "\n".join(rows)


p1 = Point(2, 3)
p2 = Point(3, 12)
print(p1.graph())
print()
print(p2.graph())

4
3 *
2
1
01234

3
2  *
1
0
9
8
7
6
5
4
3
2
1
01234567890123

为了能够推断出类变量和实例变量,了解python解释器使用的规则会很有帮助。 这样,您就可以在心中模拟解释器的工作

当解释器看到形式为 <obj>.<varname>的表达式时,它:

<instancename>.<instance_var/class_methods/class_var>

注 类内定义的 methods 也被称作 类变量 class variable

  1. 先搜索 dot 后面的内容是不是对应实例 <instancename> 里的 实例变量
  2. 如果找不到实例变量,则检查该类是否具有类变量。 如果是这样,它将使用该值。
  3. 如果找不到实例或类变量,则会创建运行时错误(实际上,它会先进行其他检查,您将在下一章中进行了解)

当解释器看到格式为<obj>.<varname> = <expression>的赋值语句时,它将:

  1. 计算右侧的表达式以产生一些python对象;
  2. 然后将 <obj> 的实例变量<varname>绑定到右侧的python对象。 请注意,这种形式的赋值语句从不设置类变量。 它仅设置实例变量。never sets the class variable; it only sets the instance variable.

为了设置类变量,您可以在类定义的顶层使用形式为<varname> = <expr>的赋值语句,就像上面代码中的第4行一样,设置类变量 printed_rep

举例:p1.graph()is evaluated by:

  1. looking up p1 and finding that it’s an instance of Point
  2. looking for an instance variable called graph in p1, but not finding one
  3. looking for a class variable called graph in p1’s class, the Point class; it finds a function/method object
  4. Because of the () after the word graph, it invokes the function/method object, with the parameter self bound to the object p1 points to.

20.11. 编写class之前 关于类和实例的思考

您还可以使用自定义类来保存数据——例如,通过向REST API发出请求而获得的数据。

在决定定义一个新类之前,需要牢记一些注意事项,以及应该问自己的问题:

  1. First is, what kind of data you want to represent with your class? 你想要用 class 表示什么数据?

    • Is it a list of songs?

    • Is it a list of students?

    • A list of cars et cetera.

  2. what does one particular instance represent of this class? class 类的一个具体 instance 代表什么?

    • So if it’s a list of songs, one particular instance might represent a song.

    • One particular instance of a list of students might represent one particular student

  3. what are the instance variables? What’s unique to every instance that I might have? 实例具有的实例变量包括哪些?

    • So if it’s a list of students, it might be something like a name, a student ID.

    • If it’s a list of songs, it might be the artist, the track name, the length, et cetera.

  4. what methods might you actually want? 你想要定义什么样的类内方法?

    • So, if every instance is a particular song, then you might have a method, for example, to paying an external API to get the lyrics for that song.

    • If it’s a student, you might want to have a method to, for example, send a message to that student.

  5. what does the printed representation of an instance look like? 实例的打印表示格式是什么样的?

((This question will help you determine how to write the __str__ method.)

So if I print out a particular song,

then I might want to print out the track name,

and then the album name, and maybe the length.

请记住,类定义(如函数定义)是对类的每个实例应具有的内容的一般描述(每个Point 都有一个x和一个y)

而类实例是特定的: 例如,具有特定 x 和 y 的点。您可能有一个 x 值为 3、y 值为 2 的点,因此对于类Point 的特定实例,您应该将 3 和 2 传递给构造函数__init__方法,就像您在上一节中看到的那样:new_point = Point(3,2)

20.13. A Tamagotchi Game 一个案例

还有很多有趣的方式可以使用用户定义的类,这些类不涉及来自互联网的数据。 让我们以比Point类更有趣的方式将所有这些机制组合在一起。 还记得小小的电子宠物Tamagotchis吗? 随着时间的流逝,他们会变得饥饿或无聊。 您必须清理它们,否则他们会生病。 您只需在设备上单击几个按钮即可完成所有操作。

我们将对此做一个简化的基于文本的版本。 在您的问题集中以及有关继承**<chap_inheritance>**的章节中,我们将进一步扩展。


First, let’s start with a class Pet. Each instance of the class will be one electronic pet for the user to take care of. Each instance will have a current state, consisting of three instance variables:

  • hunger, an integer
  • boredom, an integer
  • sounds, a list of strings, each a word that the pet has been taught to say

方法:

  1. __init__方法:在__init__方法中,将hungerboredom初始化为0到各自阈值之间的随机值。sounds实例变量被初始化为具有相同名称的class变量的副本。

    我们复制列表的原因是我们将执行破坏性操作(destructive operations)(将声音添加到列表中)。 如果我们不进行复制,那么那些destructive operations将影响到 class variable 指向的列表, and thus teaching a sound to any of the pets would teach it to all instances of the class!

  2. clock_tick方法:有一个clock_tick方法,它仅增加 hungerboredom实例变量,从而模拟了随着时间的流逝,宠物变得越来越无聊和饥饿的想法。

  3. __str__方法:可产生宠物当前状态的字符串表示形式,特别是它是否无聊或饥饿或是否快乐。 如果无聊实例变量大于阈值(设置为类变量),这很无聊。

  4. teach()方法/hi()方法:为了缓解无聊,宠物主人可以使用teach()方法教宠物一个新单词,或者使用hi()方法与宠物互动。

    • 作为对 teach() 的回应,宠物将新单词添加到单词列表中。

    • 作为对 hi() 方法的响应,它打印出一个它知道的单词,从已知单词列表中随机挑选一个。

    hi()teach() 都会调用 reduce_boredom() 方法。它通过从类变量 boredom_decrement 中读取的量来减少 boredom 状态。boredom 状态永远不会低于0。

  5. reduce_boredom()方法

  6. feed() 方法:To relieve hunger, we call the feed() method.

from random import randrange

class Pet():
    # 类变量
    boredom_decrement = 4
    hunger_decrement = 6
    boredom_threshold = 5
    hunger_threshold = 10
    sounds = ['Mrrp']
    
    # 构造函数
    # 默认参数
    # 随机函数的使用
    # 列表复制
    def __init__(self, name = 'Kitty'):
        self.name = name
        self.hunger = randrange(self.hunger_threshold)
        self.boredom = randrange(self.boredom_threshold)
        self.sounds = self.sounds[:] # copy the class attribute, so that when we make changes to it, we won't affect the other Pets in the class
    
    # 调用实现时间增长 对应的实例变量 无聊和饥饿值增加
    def clock_tick(self):
        self.bredom += 1
        self.hunger += 1
    
    # 情绪
    def mood(self):
        if self.hunger <= self.hunger_threshold and self.boredom <= self.boredom_threshold:
            return 'happy'
        elif self.hunger > self.hunger_threshold:
            return 'hungery'
        else:
            return 'bored'
        
    # print 字符表示
    def __str__(self):
        state = "    I'm " + self.name + ". "
        state += ' I feel ' + self.mood() + '. '
        
        return state
    
    # 随机输出一个 已有的 词并减少 无聊值
    def hi(self):
        print(self.sounds[randrange(len(self.sounds))])
        self.reduce_boredom
        
    def feed(self):
        self.reduce_hunger()
    # 减少饥饿值并保证最小是0    
    def reduce_hunger(self):
        self.hunger = max(0, self.hunger - self.hunger_decrement)
        
    def reduce_boredom(self):
        self.boredom = max(0, self.boredom - self.boredom_decrement)
p1 = Pet("Fido")  	 # 创建一个 Pet 实例 默认实例变量以及命名
print(p1)			# 看 __str__
for i in range(10):  # 循环10次, 对 这个instance 执行 时间流逝
    p1.clock_tick()
    print(p1)        # 查看现在的状态
p1.feed()            # 对p1 执行 feed + 
p1.hi()
p1.teach("Boo")     
for i in range(10):
    p1.hi()
print(p1)

如果你想通过编写python代码来与宠物互动,那太好了。让我们做一个非程序员可以玩的游戏。

我们将使用监听器循环模式(Loop <chap_listener> pattern)。在每次迭代中,我们将显示一个文本提示,提醒用户哪些命令可用。

用户将有一个宠物列表,每个宠物都有一个名字。用户可以发布一个命令来收养一只新宠物,这将创建一个新的宠物实例。或者,用户可以通过问候、教导或喂食命令与现有的宠物互动。

The user will have a list of pets, each with a name. The user can issue a command to adopt a new pet, which will create a new instance of Pet. Or the user can interact with an existing pet, with a Greet, Teach, or Feed command.

不管用户做什么,只要输入一个命令,他们所有的宠物的时钟都会滴答作响。小心,如果你有太多的宠物,你将无法让他们都满意!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值