[TOC]
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)都由乌龟的<u>位置,颜色,前进方向</u>等组成。
每只海龟还具有前进,后退或右转或左转的能力。单个海龟的不同之处在于,尽管它们都是海龟(object),但它们在单个状态属性的具体值上是不同的(可能它们在不同的位置或有不同的标题)。 they differ in the specific values of the individual state attributes
属于同一个 object,区别是所具有的 实例变量的值不同
<img src="https://i.ibb.co/6vmTrwF/image.png" alt="image" border="0" style="zoom:50%;" >
20.3. User Defined Classes
我们已经见过这样的 classes: str
, int
, float
and list
。这些是由Python定义的,供我们使用。然而,在许多情况下,当我们解决问题时,我们需要创建与我们试图解决的问题相关的数据对象。我们需要创建自己的类。
例如,考虑数学点的概念。 在二维中,点是两个数字(坐标),它们被一起视为一个 object。 点通常用括号括起来,并用逗号分隔坐标。例如,
(0,0)
表示原点,而(x,y)
表示点x
向右的单位,y
单位从原点起向上的单位长。 这(x,y)
是点 object 的状态 state。
考虑上面的图,我们可以绘制一个点对象,如下所示。
<img src="https://i.ibb.co/SmqMmnF/image.png" alt="image" border="0" style="zoom:67%;" >
与点相关联的一些典型操作可能是:求点的x坐标,getX,或者求点的y坐标,getY。可能还希望计算一个点到原点的距离,或者一个点到另一个点的距离,或者找到两点之间的中点,或者回答一个点是否落在给定的矩形或圆内的问题。这些称为 这个object 的 methods
<img src="https://i.ibb.co/X5211K9/image.png" alt="image" border="0" style="zoom:67%;" >
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
。然后为“产品”:point1
和point2
各自设置了一个名为 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 instancepoint1
as self when we callgetX
)==因此,当我们说
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
,每个Point
的x
和y
坐标值为0
。但是,由于我们没有要求程序对这些Point
做任何事情,因此看不到其他任何结果。实际上是:<img src="https://i.ibb.co/x2mxRfz/image.png" alt="image" border="0" style="zoom: 67%;" >
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)==<img src="https://i.ibb.co/SmqMmnF/image.png" alt="image" border="0" style="zoom:67%;" >
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
<img src="https://i.ibb.co/j4z4ZSs/image.png" alt="image" border="0">
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步】<img src="https://i.ibb.co/xgsg62q/image.png" alt="image" border="0" style="zoom: 67%;" >
执行该方法的代码,并将变量self绑定到该实例。(深色部分)
在第4步和第5步,将对空的产品 设置两个两个(出厂的)实例变量:x和y都设置为0。
__init__
方法未返回任何内容, but the point object itself is returned from the call toPoint()
.<img src="https://i.ibb.co/zxQSzjH/image-20200316200115514.png" alt="image-20200316200115514" border="0" style="zoom:67%;" >
thus, at step 7,
p
is bound to the new point that was created and initialized.<img src="https://i.ibb.co/j3kbQpH/image-20200316200338306.png" alt="image-20200316200338306" border="0" style="zoom:67%;" >
跳到步骤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中将initX
和initY
的值分配给对象的状态state。
<img src="https://i.ibb.co/SQ7dWVS/image.png" alt="image" border="0" style="zoom:67%;" >
在类的__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)
-
当我们想用 class 来表示这些数据的时候,我们首先要考虑 我们想要用 instance 存储什么数据?
在这里我们假设 我们的每个实例需要存储着所有三个数据
-
然后我会问, 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) # 图二 存疑 为什么输出的是类型
<img src="https://i.ibb.co/9pB35tT/image-20200316222011719.png" alt="image-20200316222011719" border="0">
<img src="https://i.ibb.co/s1FNfmB/image-20200316222256704.png" alt="image-20200316222256704" border="0">
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默认功能告诉您 p
是 Point
类型的对象。 但是,它不会告诉您有关该点的特定状态(state)的任何信息,
但如果类定义中包含一个特殊的方法调用 __str__
,我们可以改进这个表示。请注意,此方法使用与构造函数相同的命名约定,即名称前后有两个下划线。Python对特殊方法使用这种命名技术是很常见的。
__str__
方法的作用是告诉Python在实际打印该对象时如何表示该对象。
__str__
方法负责返回类的创建者所定义的一种字符串表示形式。换句话说,作为程序员,你可以选择一个Point
(instance)在被打印时应该是什么样子。在这个例子里,我们决定__str__
方法需要创建并返回一个字符串,字符串表示 将包括 x
和 y
的值以及一些识别文本。
类的
__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):
self
是将自身作为第一个参数
otherPoint
是另一个实例点将设
print(p1 + p2)
则p1 = self, p2 = otherPoint
<img src="https://i.ibb.co/zxDYTNj/image.png" alt="image" border="0" style="zoom:70%;" >
def __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 tolen
rather than invokinglen()
.
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
- 先搜索
dot
后面的内容是不是对应实例<instancename>
里的 实例变量- 如果找不到实例变量,则检查该类是否具有类变量。 如果是这样,它将使用该值。
- 如果找不到实例或类变量,则会创建运行时错误(实际上,它会先进行其他检查,您将在下一章中进行了解)
当解释器看到格式为<obj>.<varname> = <expression>
的赋值语句时,它将:
- 计算右侧的表达式以产生一些python对象;
- 然后将
<obj>
的实例变量<varname>
绑定到右侧的python对象。 请注意,这种形式的赋值语句从不设置类变量。 它仅设置实例变量。never sets the class variable; it only sets the instance variable.
为了设置类变量,您可以在类定义的顶层使用形式为
<varname> = <expr>
的赋值语句,就像上面代码中的第4行一样,设置类变量printed_rep
。
举例:p1.graph()
is evaluated by:
- looking up p1 and finding that it’s an instance of Point
- looking for an instance variable called graph in
p1
, but not finding one- looking for a class variable called graph in
p1
’s class, the Point class; it finds a function/method object- 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发出请求而获得的数据。
在决定定义一个新类之前,需要牢记一些注意事项,以及应该问自己的问题:
-
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.
-
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
-
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.
-
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.
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
方法:
-
__init__
方法:在__init__
方法中,将hunger
和boredom
初始化为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!
clock_tick
方法:有一个clock_tick
方法,它仅增加hunger
和boredom
实例变量,从而模拟了随着时间的流逝,宠物变得越来越无聊和饥饿的想法。__str__
方法:可产生宠物当前状态的字符串表示形式,特别是它是否无聊或饥饿或是否快乐。 如果无聊实例变量大于阈值(设置为类变量),这很无聊。-
teach()
方法/hi()
方法:为了缓解无聊,宠物主人可以使用teach()
方法教宠物一个新单词,或者使用hi()
方法与宠物互动。作为对
teach()
的回应,宠物将新单词添加到单词列表中。作为对
hi()
方法的响应,它打印出一个它知道的单词,从已知单词列表中随机挑选一个。
hi()
和teach()
都会调用reduce_boredom()
方法。它通过从类变量boredom_decrement
中读取的量来减少 boredom 状态。boredom 状态永远不会低于0。 reduce_boredom()
方法feed()
方法:To relieve hunger, we call thefeed()
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.
不管用户做什么,只要输入一个命令,他们所有的宠物的时钟都会滴答作响。小心,如果你有太多的宠物,你将无法让他们都满意!