Python 进阶指南(编程轻松进阶):十五、面向对象编程和类
原文:http://inventwithpython.com/beyond/chapter15.html
OOP 是一种编程语言特性,允许你将变量和函数组合成新的数据类型,称为类,你可以从中创建对象。通过将代码组织成类,可以将一个整体程序分解成更容易理解和调试的小部分。
对于小程序来说,OOP 与其说是增加了组织,不如说是增加了官僚主义。虽然有些语言,比如 Java,要求你将所有代码组织成类,但是 Python 的 OOP 特性是可选的。程序员可以在需要时利用类,或者在不需要时忽略它们。Python 核心开发人员 Jack Diederich 在 PyCon 2012 的演讲“停止编写类”(youtu.be/o9pEzgHorH0
)中指出,在许多情况下,程序员编写类时,更简单的函数或模块会工作得更好。
也就是说,作为一名程序员,你应该熟悉什么是类以及它们如何工作的基础知识。在这一章中,你将学习什么是类,为什么在程序中使用它们,以及它们背后的语法和编程概念。OOP 是一个广泛的话题,本章只是作为一个介绍。
真实世界的类比:填写表单
在您的生活中,您很可能已经无数次地填写纸质或电子表单:为了看医生、为了网上购物或为了婚礼回复。表单是另一个人或组织收集他们需要的关于您的信息的统一方式。不同的表格要求不同种类的信息。你会在医生的表格上报告一个敏感的医疗状况,你会在婚礼回复上报告你带来的任何客人,但不是相反。
在 Python 中,类、类型、数据类型含义相同。与纸质或电子表单一样,类是 Python 对象(也称为实例)的蓝图,其中包含表示名词的数据。这个名词可以是医生的病人、电子商务购物或婚礼宾客。类就像一个空白的表单模板,从该类创建的对象就像一个填写好的表单,其中包含了表单所代表的事物的实际数据。例如,在图 15-1 中,RSVP 响应表单就像一个类,而填写好的 RSVP 就像一个对象。
图 15-1:婚礼 RSVP 表单模板就像类,填写好的表单就像对象。
你也可以把类和对象想象成电子表格,如图 15-2 所示。
图 15-2:所有 RSVP 数据的电子表格
列标题组成了类,而每一行组成了一个对象。
在现实世界中,类和对象经常被当作项目的数据模型来谈论,但是不要把映射和领域混淆了。什么进入类取决于程序需要做什么。图 15-3 显示了一些不同类的对象,它们代表了同一个现实世界的人,除了这个人的名字,它们存储了完全不同的信息。
图 15-3:四个对象由不同的类组成,代表同一个真实世界的人,这取决于软件应用需要了解这个人的什么
另外,你的类中包含的信息应该取决于你的程序的需求。许多 OOP 教程使用一个Car
类作为它们的基本例子,却没有注意到什么进入一个类完全取决于你正在编写的软件的种类。没有一个通用的Car
类会明显地有一个honkHorn()
方法或者numberOfCupholders
属性,仅仅因为那些是真实世界的汽车所具有的特征。你的程序可能是一个汽车经销商网络应用,一个赛车视频游戏,或者一个道路交通模拟。汽车经销商 Web 应用的Car
类可能有milesPerGallon
或manufacturersSuggestedRetailPrice
属性(就像汽车经销商的电子表格可能使用这些作为列)。但是视频游戏和道路交通模拟没有这些属性,因为这些信息与它们无关。视频游戏的Car
类可能有一个explodeWithLargeFireball()
方法,但是汽车经销商和交通模拟,但愿不会有。
从类创建对象
您已经在 Python 中使用过类和对象,即使您自己没有创建过类。考虑一下datetime
模块,它包含一个名为date
的类。datetime.date
类的对象(也简称为datetime.date
对象或date
对象)代表一个特定的日期。在交互式 Shell 中输入以下内容,创建一个datetime.date
类的对象:
>>> import datetime
>>> birthday = datetime.date(1999, 10, 31) # Pass the year, month, and day.
>>> birthday.year
1999
>>> birthday.month
10
>>> birthday.day
31
>>> birthday.weekday() # weekday() is a method; note the parentheses.
6
属性是与对象相关联的变量。对datetime.date()
的调用创建了一个新的date
对象,用参数1999
、10
、31
初始化,因此该对象表示日期 1999 年 10 月 31 日。我们将这些参数指定为date
类的year
、month
和day
属性,所有date
对象都有这些属性。
有了这些信息,类的weekday()
方法就可以计算出星期几。在这个例子中,它返回周日的6
,因为根据 Python 的在线文档,weekday()
的返回值是一个整数,从周一的0
开始,到周日的6
。文档列出了date
类的对象拥有的其他几个方法。尽管date
对象包含多个属性和方法,但它仍然是一个可以存储在变量中的对象,比如本例中的birthday
。
创建一个简单的类:WizCoin
让我们创建一个WizCoin
类,它代表一个虚构的巫师货币中的一些硬币。在这种货币中,面额为克努特、西克尔(价值 29 克努特)和加隆(价值 17 西克尔或 493 克努特)。请记住,WizCoin
类中的对象代表一定数量的硬币,而不是一定数量的钱。例如,它会告诉你你拿的是 5 个 25 美分和 1 个 10 美分,而不是 1.35 美元。
在名为wizcoin.py
的新文件中,输入以下代码创建WizCoin
类。注意,__init__
方法名在init
前后有两个下划线(我们将在本章后面的“方法、__init__()
和self
”中讨论__init__
):
class WizCoin: # 1def __init__(self, galleons, sickles, knuts): # 2"""Create a new WizCoin object with galleons, sickles, and knuts."""self.galleons = galleonsself.sickles = sicklesself.knuts = knuts# NOTE: __init__() methods NEVER have a return statement.def value(self): # 3"""The value (in knuts) of all the coins in this WizCoin object."""return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts)def weightInGrams(self): # 4"""Returns the weight of the coins in grams."""return (self.galleons * 31.103) + (self.sickles * 11.34) + (self.knuts * 5.0)
这个程序使用一个class
语句 1 定义了一个名为WizCoin
的新类。创建一个类会创建一个新类型的对象。使用class
语句定义一个类类似于使用def
语句定义新函数。在class
语句后面的代码块中有三个方法的定义:__init__()
(初始化器的缩写) 2 、value()
3 和weightInGrams()
4 。请注意,所有方法都有一个名为self
的第一个参数,我们将在下一节探讨这个参数。
按照惯例,模块名(比如我们的wizcoin.py
文件中的wizcoin
)是小写的,而类名(比如WizCoin
)以大写字母开头。不幸的是,Python 标准库中的一些类,比如date
,并没有遵循这个约定。
为了练习创建WizCoin
类的新对象,在一个单独的文件编辑器窗口中输入下面的源代码,并将文件保存为wcexample1.py
,与wizcoin.py
放在同一个文件夹中:
import wizcoinpurse = wizcoin.WizCoin(2, 5, 99) # The ints are passed to __init__(). # 1
print(purse)
print('G:', purse.galleons, 'S:', purse.sickles, 'K:', purse.knuts)
print('Total value:', purse.value())
print('Weight:', purse.weightInGrams(), 'grams')print()coinJar = wizcoin.WizCoin(13, 0, 0) # The ints are passed to __init__(). # 2
print(coinJar)
print('G:', coinJar.galleons, 'S:', coinJar.sickles, 'K:', coinJar.knuts)
print('Total value:', coinJar.value())
print('Weight:', coinJar.weightInGrams(), 'grams')
对WizCoin()
1 2 的调用创建一个WizCoin
对象,并为它们运行__init__()
方法中的代码。我们将三个整数作为参数传递给WizCoin()
,它们被转发给__init__()
的参数。这些参数被分配给对象的self.galleons
、self.sickles
和self.knuts
属性。注意,正如time.sleep()
函数要求您首先导入time
模块并将time.
放在函数名之前,我们也必须导入wizcoin
并将wizcoin.
放在WizCoin()
函数名之前。
当您运行该程序时,输出将类似于以下内容:
<wizcoin.WizCoin object at 0x000002136F138080>
G: 2 S: 5 K: 99
Total value: 1230
Weight: 613.906 grams<wizcoin.WizCoin object at 0x000002136F138128>
G: 13 S: 0 K: 0
Total value: 6409
Weight: 404.339 grams
如果你得到一个错误信息,比如ModuleNotFoundError: No module named 'wizcoin'
,检查以确保你的文件被命名为wizcoin.py
,并且它和wcexample1.py
在同一个文件夹中。
WizCoin
对象没有有用的字符串表示,所以打印purse
和coinJar
会在尖括号中显示一个内存地址。(你将在第 17 章学习如何改变这一点。)
正如我们可以在一个字符串对象上调用lower()
字符串方法一样,我们也可以在已经分配给purse
和coinJar
变量的WizCoin
对象上调用value()
和weightInGrams()
方法。这些方法根据对象的galleons
、sickles
和knuts
属性计算值。
类和 OOP 可以产生更多的可维护的代码——也就是说,将来更容易阅读、修改和扩展的代码。让我们更详细地探索这个类的方法和属性。
方法、__init__()
和self
方法是与特定类的对象相关联的函数。回想一下lower()
是一个字符串方法,这意味着它是在字符串对象上调用的。你可以在一个字符串上调用lower()
,就像在'Hello'.lower()
中一样,但是你不能在一个列表上调用它,比如['dog', 'cat'].lower()
。另外,注意方法跟在对象后面:正确的代码是'Hello'.lower()
,而不是lower('Hello')
。不像像lower()
这样的方法,像len()
这样的函数不与单一数据类型相关联;您可以将字符串、列表、字典和许多其他类型的对象传递给len()
。
正如您在上一节中看到的,我们通过调用类名作为函数来创建对象。这个函数被称为构造器(或者构造器,或者缩写为ctor
,发音为“see-tore”),因为它构造了一个新的对象。我们还说构造器实例化了一个新的类实例。
调用构造器会导致 Python 创建新对象,然后运行__init__()
方法。不要求类有一个__init__()
方法,但是它们几乎总是有。__init__()
方法是您通常设置属性初始值的地方。例如,回想一下WizCoin
的__init__()
方法如下所示:
def __init__(self, galleons, sickles, knuts):"""Create a new WizCoin object with galleons, sickles, and knuts."""self.galleons = galleonsself.sickles = sicklesself.knuts = knuts# NOTE: __init__() methods NEVER have a return statement.
当wcexample1.py
程序调用WizCoin(2, 5, 99)
时,Python 创建一个新的WizCoin
对象,然后将三个参数(2
、5
和99
)传递给一个__init__()
调用。但是__init__()
方法有四个参数:self
、galleons
、sickles
和knuts
。原因是所有方法都有一个名为self
的第一个参数。当对一个对象调用一个方法时,该对象被自动传入用于self
参数。其余的参数通常被赋给形参。如果您看到一条错误消息,比如TypeError: __init__() takes 3 positional arguments but 4 were given
,您可能忘记了将self
参数添加到方法的def
语句中。
你不必命名一个方法的第一个参数self
;你可以给它起任何名字。但是使用self
是惯例,选择一个不同的名称会使您的代码对其他 Python 程序员来说可读性更差。当你阅读代码时,将self
作为第一个参数是区分方法和函数的最快方法。类似地,如果你的方法的代码从来不需要使用self
参数,这表明你的方法可能只是一个函数。
WizCoin(2, 5, 99)
的2
、5
和99
参数不会自动分配给新对象的属性;为此,我们需要__init__()
中的三个赋值语句。通常情况下,__init__()
参数的名称与属性相同,但是self.galleons
中出现的self
表示它是对象的属性,而galleons
是参数。将构造器的参数存储在对象的属性中是一个类的__init__()
方法的常见任务。上一节中的datetime.date()
调用执行了类似的任务,除了我们传递的三个参数是针对新创建的date
对象的year
、month
和day
属性。
您之前已经调用了int()
、str()
、float()
和bool()
函数在数据类型之间进行转换,例如str(3.1415)
基于浮点值3.1415
返回字符串值'3.1415'
。之前,我们将这些描述为函数,但是int
、str
、float
和bool
实际上是类,而int()
、str()
、float()
和bool()
函数是返回新的整数、字符串、浮点和布尔对象的构造器。Python 的风格指南推荐使用大写的驼峰大小写作为类名(如WizCoin
),尽管 Python 的许多内置类并不遵循这一约定。
注意,调用WizCoin()
构造器会返回新的WizCoin
对象,但是__init__()
方法从来没有一个带有返回值的return
语句。添加返回值会导致此错误:TypeError: __init__() should return None
。
属性
属性是与对象相关的变量。Python 文档将属性描述为“点后的任何名称”例如,考虑上一节中的birthday.year
表达式。year
属性是一个跟在点后面的名称。
每个对象都有自己的属性集。当wcexample1.py
程序创建两个WizCoin
对象并将它们存储在purse
和coinJar
变量中时,它们的属性值不同。您可以像访问任何变量一样访问和设置这些属性。为了练习设置属性,打开一个新的文件编辑器窗口并输入以下代码,将其作为wcexample2.py
保存在与wizcoin.py
文件相同的文件夹中:
import wizcoinchange = wizcoin.WizCoin(9, 7, 20)
print(change.sickles) # Prints 7.
change.sickles += 10
print(change.sickles) # Prints 17.pile = wizcoin.WizCoin(2, 3, 31)
print(pile.sickles) # Prints 3.
pile.someNewAttribute = 'a new attr' # A new attribute is created.
print(pile.someNewAttribute)
当您运行该程序时,输出如下所示:
7
17
3
a new attr
您可以将对象的属性视为类似于字典的键。您可以读取和修改它们的相关值,并为对象分配新属性。从技术上讲,方法也被认为是类的属性。
私有属性和私有方法
在 C++或 Java 之类的语言中,属性可以被标记为具有私有访问,这意味着编译器或解释器只允许类的方法内部的代码访问或修改该类的对象的属性。但是在 Python 中,这种强制是不存在的。所有的属性和方法都是有效的公共访问:类之外的代码可以访问和修改该类中任何对象的任何属性。
但是私有访问是有用的。例如,BankAccount
类的对象可以有一个balance
属性,只有BankAccount
类的方法可以访问这个属性。出于这些原因,Python 的惯例是以单下划线开始私有属性或方法名。从技术上讲,没有什么可以阻止类外的代码访问私有属性和方法,但是最好的做法是只让类的方法访问它们。
打开一个新的文件编辑器窗口,输入以下代码,保存为privateExample.py
。其中,BankAccount
类的对象有私有的_name
和_balance
属性,只有deposit()
和withdraw()
方法可以直接访问:
class BankAccount:def __init__(self, accountHolder):# BankAccount methods can access self._balance, but code outside of# this class should not:self._balance = 0 # 1self._name = accountHolder # 2with open(self._name + 'Ledger.txt', 'w') as ledgerFile:ledgerFile.write('Balance is 0\n')def deposit(self, amount):if amount <= 0: # 3return # Don't allow negative "deposits".self._balance += amountwith open(self._name + 'Ledger.txt', 'a') as ledgerFile: # 4ledgerFile.write('Deposit ' + str(amount) + '\n')ledgerFile.write('Balance is ' + str(self._balance) + '\n')def withdraw(self, amount):if self._balance < amount or amount < 0: # 5return # Not enough in account, or withdraw is negative.self._balance -= amountwith open(self._name + 'Ledger.txt', 'a') as ledgerFile: # 6ledgerFile.write('Withdraw ' + str(amount) + '\n')ledgerFile.write('Balance is ' + str(self._balance) + '\n')acct = BankAccount('Alice') # We create an account for Alice.
acct.deposit(120) # _balance can be affected through deposit()
acct.withdraw(40) # _balance can be affected through withdraw()# Changing _name or _balance outside of BankAccount is impolite, but allowed:
acct._balance = 1000000000 # 7
acct.withdraw(1000)acct._name = 'Bob' # Now we're modifying Bob's account ledger! # 8
acct.withdraw(1000) # This withdrawal is recorded in BobLedger.txt!
当你运行privateExample.py
时,它创建的账本文件不准确,因为我们在类外修改了_balance
和_name
,导致状态无效。AliceLedger.txt
莫名其妙有一大笔钱在里面:
Balance is 0
Deposit 120
Balance is 120
Withdraw 40
Balance is 80
Withdraw 1000
Balance is 999999000
现在有一个BobLedger.txt
文件有一个令人费解的帐户余额,尽管我们从未为 Bob 创建过一个BankAccount
对象:
Withdraw 1000
Balance is 999998000
设计良好的类大多是自包含的,提供了将属性调整为有效值的方法。_balance
和_name
属性被标记为私有的 12,调整BankAccount
类的值的唯一有效方式是通过deposit()
和withdraw()
方法。这两个方法都有检查 35 来确保_balance
没有进入无效状态(比如负整数值)。这些方法还记录每笔交易的账户当前余额 46。
*修改这些属性的类之外的代码,如acct._balance = 1000000000
7 或acct._name = 'Bob'
8 指令,会将对象置于无效状态并引入 bug(以及来自银行审查员的审计)。通过遵循私有访问的下划线前缀约定,可以使调试更加容易。原因是你知道错误的原因会在类的代码中,而不是在整个程序的任何地方。
注意,与 Java 和其他语言不同,Python 不需要私有属性的公共获取器和设置器方法。相反,Python 使用属性,正如在第 17 章中所解释的。
type()
函数和__qualname__
属性
将一个对象传递给内置的type()
函数通过它的返回值告诉我们对象的数据类型。从type()
函数返回的对象是类型对象,也称为类对象。回想一下,术语类型、数据类型和类在 Python 中都有相同的含义。要查看type()
函数针对不同的值返回什么,请在交互式 Shell 中输入以下内容:
>>> type(42) # The object 42 has a type of int.
<class 'int'>
>>> int # int is a type object for the integer data type.
<class 'int'>
>>> type(42) == int # Type check 42 to see if it is an integer.
True
>>> type('Hello') == int # Type check 'Hello' against int.
False
>>> import wizcoin
>>> type(42) == wizcoin.WizCoin # Type check 42 against WizCoin.
False
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> type(purse) == wizcoin.WizCoin # Type check purse against WizCoin.
True
注意,int
是一个类型对象,与type(42)
返回的是同一类对象,但也可以作为int()
构造器调用:int('42')
函数不转换'42'
字符串参数;相反,它根据参数返回一个整数对象。
假设您需要记录一些关于程序中变量的信息,以帮助您稍后调试它们。您只能将字符串写入日志文件,但是将类型对象传递给str()
将会返回一个看起来相当混乱的字符串。相反,使用所有类型对象都有的__qualname__
属性来编写一个更简单、人类可读的字符串:
>>> str(type(42)) # Passing the type object to str() returns a messy string.
"<class 'int'>"
>>> type(42).__qualname__ # The __qualname__ attribute is nicer looking.
'int'
__qualname__
属性最常用于覆盖__repr__()
方法,这将在第 17 章详细解释。
非面向对象与面向对象的例子:井字棋
起初,很难理解如何在程序中使用类。让我们看一个不使用类的简短井字棋程序的例子,然后重写它,使它使用类。
打开一个新的文件编辑器窗口,进入以下程序;然后保存为tictactoe.py
:
# tictactoe.py, A non-OOP tic-tac-toe game.ALL_SPACES = list('123456789') # The keys for a TTT board dictionary.
X, O, BLANK = 'X', 'O', ' ' # Constants for string values.def main():"""Runs a game of tic-tac-toe."""print('Welcome to tic-tac-toe!')gameBoard = getBlankBoard() # Create a TTT board dictionary.currentPlayer, nextPlayer = X, O # X goes first, O goes next.while True:print(getBoardStr(gameBoard)) # Display the board on the screen.# Keep asking the player until they enter a number 1-9:move = Nonewhile not isValidSpace(gameBoard, move):print(f'What is {currentPlayer}\'s move? (1-9)')move = input()updateBoard(gameBoard, move, currentPlayer) # Make the move.# Check if the game is over:if isWinner(gameBoard, currentPlayer): # First check for victory.print(getBoardStr(gameBoard))print(currentPlayer + ' has won the game!')breakelif isBoardFull(gameBoard): # Next check for a tie.print(getBoardStr(gameBoard))print('The game is a tie!')breakcurrentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns.print('Thanks for playing!')def getBlankBoard():"""Create a new, blank tic-tac-toe board."""board = {} # The board is represented as a Python dictionary.for space in ALL_SPACES:board[space] = BLANK # All spaces start as blank.return boarddef getBoardStr(board):"""Return a text-representation of the board."""return f'''{board['1']}|{board['2']}|{board['3']} 1 2 3-+-+-{board['4']}|{board['5']}|{board['6']} 4 5 6-+-+-{board['7']}|{board['8']}|{board['9']} 7 8 9'''def isValidSpace(board, space):"""Returns True if the space on the board is a valid space numberand the space is blank."""return space in ALL_SPACES or board[space] == BLANKdef isWinner(board, player):"""Return True if player is a winner on this TTTBoard."""b, p = board, player # Shorter names as "syntactic sugar".# Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals.return ((b['1'] == b['2'] == b['3'] == p) or # Across the top(b['4'] == b['5'] == b['6'] == p) or # Across the middle(b['7'] == b['8'] == b['9'] == p) or # Across the bottom(b['1'] == b['4'] == b['7'] == p) or # Down the left(b['2'] == b['5'] == b['8'] == p) or # Down the middle(b['3'] == b['6'] == b['9'] == p) or # Down the right(b['3'] == b['5'] == b['7'] == p) or # Diagonal(b['1'] == b['5'] == b['9'] == p)) # Diagonaldef isBoardFull(board):"""Return True if every space on the board has been taken."""for space in ALL_SPACES:if board[space] == BLANK:return False # If a single space is blank, return False.return True # No spaces are blank, so return True.def updateBoard(board, space, mark):"""Sets the space on the board to mark."""board[space] = markif __name__ == '__main__':main() # Call main() if this module is run, but not when imported.
当您运行该程序时,输出将类似于以下内容:
Welcome to tic-tac-toe!| | 1 2 3-+-+-| | 4 5 6-+-+-| | 7 8 9
What is X's move? (1-9)
1X| | 1 2 3-+-+-| | 4 5 6-+-+-| | 7 8 9
What is O's move? (1-9)
`--snip--`X| |O 1 2 3-+-+-|O| 4 5 6-+-+-X|O|X 7 8 9
What is X's move? (1-9)
4X| |O 1 2 3-+-+-X|O| 4 5 6-+-+-X|O|X 7 8 9
X has won the game!
Thanks for playing!
简而言之,这个程序使用字典对象来表示井字棋棋盘上的九个空格。字典的键是字符串'1'
到'9'
,它的值是字符串'X'
、'O'
或' '
。数字空间的排列方式与手机键盘相同。
tictactoe . py中的函数执行以下操作:
main()
函数包含创建新棋盘数据结构的代码(存储在gameBoard
变量中)并调用程序中的其他函数。getBlankBoard()
函数返回一个字典,其中九个空格设置为空白板的' '
。getBoardStr()
函数接受表示棋盘的字典,并返回棋盘的多行字符串表示,可以打印到屏幕上。这就是游戏显示的井字棋棋盘文本。- 如果传递了一个有效的空格数,并且该空格为空,则
isValidSpace()
函数返回True
。 isWinner()
函数的参数接受一个棋盘字典和'X'
或'O'
来确定该玩家是否在棋盘上有连续三个标记。isBoardFull()
函数决定棋盘上是否没有空格,意味着游戏已经结束。updateBoard()
函数的参数接受棋盘字典、空格和玩家的 X 或 O 标记,并更新字典。
注意,许多函数接受变量board
作为它们的第一个参数。这意味着这些函数是相互关联的,因为它们都在一个公共的数据结构上操作。
当代码中的几个函数都在同一个数据结构上操作时,通常最好将它们作为一个类的方法和属性组合在一起。让我们在tictactoe.py
程序中对此进行重新设计,使用一个TTTBoard
类将board
字典存储在一个名为spaces
的属性中。将board
作为参数的函数将成为我们的TTTBoard
类的方法,并使用self
参数而不是board
参数。
打开一个新的文件编辑器窗口,输入以下代码,保存为tictactoe_oop.py
:
# tictactoe_oop.py, an object-oriented tic-tac-toe game.ALL_SPACES = list('123456789') # The keys for a TTT board.
X, O, BLANK = 'X', 'O', ' ' # Constants for string values.def main():"""Runs a game of tic-tac-toe."""print('Welcome to tic-tac-toe!')gameBoard = TTTBoard() # Create a TTT board object.currentPlayer, nextPlayer = X, O # X goes first, O goes next.while True:print(gameBoard.getBoardStr()) # Display the board on the screen.# Keep asking the player until they enter a number 1-9:move = Nonewhile not gameBoard.isValidSpace(move):print(f'What is {currentPlayer}\'s move? (1-9)')move = input()gameBoard.updateBoard(move, currentPlayer) # Make the move.# Check if the game is over:if gameBoard.isWinner(currentPlayer): # First check for victory.print(gameBoard.getBoardStr())print(currentPlayer + ' has won the game!')breakelif gameBoard.isBoardFull(): # Next check for a tie.print(gameBoard.getBoardStr())print('The game is a tie!')breakcurrentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns.print('Thanks for playing!')class TTTBoard:def __init__(self, usePrettyBoard=False, useLogging=False):"""Create a new, blank tic tac toe board."""self._spaces = {} # The board is represented as a Python dictionary.for space in ALL_SPACES:self._spaces[space] = BLANK # All spaces start as blank.def getBoardStr(self):"""Return a text-representation of the board."""return f'''{self._spaces['1']}|{self._spaces['2']}|{self._spaces['3']} 1 2 3-+-+-{self._spaces['4']}|{self._spaces['5']}|{self._spaces['6']} 4 5 6-+-+-{self._spaces['7']}|{self._spaces['8']}|{self._spaces['9']} 7 8 9'''def isValidSpace(self, space):"""Returns True if the space on the board is a valid space numberand the space is blank."""return space in ALL_SPACES and self._spaces[space] == BLANKdef isWinner(self, player):"""Return True if player is a winner on this TTTBoard."""s, p = self._spaces, player # Shorter names as "syntactic sugar".# Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals.return ((s['1'] == s['2'] == s['3'] == p) or # Across the top(s['4'] == s['5'] == s['6'] == p) or # Across the middle(s['7'] == s['8'] == s['9'] == p) or # Across the bottom(s['1'] == s['4'] == s['7'] == p) or # Down the left(s['2'] == s['5'] == s['8'] == p) or # Down the middle(s['3'] == s['6'] == s['9'] == p) or # Down the right(s['3'] == s['5'] == s['7'] == p) or # Diagonal(s['1'] == s['5'] == s['9'] == p)) # Diagonaldef isBoardFull(self):"""Return True if every space on the board has been taken."""for space in ALL_SPACES:if self._spaces[space] == BLANK:return False # If a single space is blank, return False.return True # No spaces are blank, so return True.def updateBoard(self, space, player):"""Sets the space on the board to player."""self._spaces[space] = playerif __name__ == '__main__':main() # Call main() if this module is run, but not when imported.
在功能上,这个程序和非 OOP 的井字棋程序是一样的。输出看起来完全相同。我们已经将原来在getBlankBoard()
中的代码移到了TTTBoard
类的__init__()
方法中,因为它们执行相同的任务,准备棋盘数据结构。我们将其他函数转换成方法,用self
参数代替旧的board
参数,因为它们也有相似的用途:它们都是在井字棋棋盘数据结构上操作的代码块。
当这些方法中的代码需要改变存储在_spaces
属性中的字典时,代码使用self._spaces
。当这些方法中的代码需要调用其他方法时,这些调用的前面也会加上self
和一个句号。这类似于《创建一个简单的类:WizCoin》中的coinJars.values()
在coinJars
变量中有一个对象。在这个例子中,要调用方法的对象在一个self
变量中。
另外,请注意,_spaces
属性以下划线开头,这意味着只有TTTBoard
方法内部的代码才能访问或修改它。类外的代码应该只能通过调用修改_spaces
的方法来间接修改它。
比较两个井字棋程序的源代码会有所帮助。你可以比较这本书里的代码,或者在autbor.com/compareoop
查看并列比较。
井字棋是一个小程序,不需要太多的努力就能理解。但是,如果这个程序有数万行代码,包含数百个不同的函数,会怎么样呢?一个有几十个类的程序比一个有几百个不同函数的程序更容易理解。OOP 将一个复杂的程序分解成更容易理解的程序块。
为现实世界设计类是困难的
设计一个类,就像设计一个纸质表单一样,看似简单。形式和类本质上是它们所代表的现实世界对象的简化。问题是,我们应该如何简化这些对象?例如,如果我们正在创建一个Customer
类,客户应该有一个firstName
和lastName
属性,对吗?但是实际上创建类来模拟现实世界的对象可能会很棘手。在大多数西方国家,一个人的姓是他们的姓,但在中国,姓是第一位的。如果我们不想排除十几亿的潜在客户,我们应该如何改变我们的Customer
阶层?是不是应该把firstName
和lastName
改成givenName
和familyName
?但是有些文化不用姓。例如,前联合国秘书长吴丹是缅甸人,他没有姓:Thant 是他的名,U 是他父亲名的首字母。我们可能想要记录客户的年龄,但是一个age
属性很快就会过时;相反,最好在每次需要时使用birthdate
属性计算年龄。
现实世界是复杂的,设计表单和类来在我们的程序可以运行的统一结构中捕捉这种复杂性是困难的。电话号码格式因国家而异。邮政编码不适用于美国以外的地址。对于德国小村庄 Schmedeswurtherwesterdeich 来说,设定城市名称的最大字符数可能是个问题。在澳大利亚和新西兰,你的法定性别可以是 x。鸭嘴兽是一种产卵的哺乳动物。花生不是坚果。热狗可能是三明治,也可能不是,取决于你问谁。作为一名编写用于现实世界的程序的程序员,你必须驾驭这种复杂性。
要了解更多关于这个主题的信息,我推荐 Carina C. Zona 在 PyCon 2015 的演讲“真实世界的模式”(youtu.be/PYYfVqtcWQY
)和 North Bay 在 Python 2018 的演讲“嗨!我的名字是…”(youtu.be/NIebelIpdYk
)。也有流行的“程序员相信的错误”博客帖子,比如“程序员相信的关于名字的错误”和“程序员相信的关于时区的错误”这些博客文章还涵盖了映射、电子邮件地址等主题,以及许多程序员经常表现不佳的数据。你可以在github/kdeldycke/awesome-falsehood
找到这些文章的链接。此外,你会在 CGP Grey 的视频中找到一个捕捉现实世界复杂性的糟糕执行方法的好例子,“社会保障卡解释”。
总结
OOP 对于组织你的代码是一个有用的特性。类允许您将数据和代码组合成新的数据类型。您还可以通过调用这些类的构造器(作为函数调用的类名)从这些类中创建对象,然后调用类的__init__()
方法。方法是与对象相关联的函数,属性是与对象相关联的变量。所有方法都有一个self
参数作为它们的第一个参数,这个参数在方法被调用时被分配给对象。这允许方法读取或设置对象的属性并调用其方法。
尽管 Python 不允许为属性指定私有或公共访问,但它确实有一个惯例,即对任何方法或属性使用下划线前缀,这些方法或属性只能从类自己的方法中调用或访问。通过遵循这个约定,您可以避免误用类并将其设置为可能导致 bug 的无效状态。调用type(obj)
将返回obj
类型的类对象。类对象有一个__qualname___
属性,该属性包含一个字符串,该字符串具有人类可读形式的类名。
此时,您可能会想,当我们可以用函数完成同样的任务时,为什么还要麻烦地使用类、属性和方法呢?OOP 是一种将代码组织成不仅仅是一个py
文件里面有 100 个函数的有用方法。通过将你的程序分成几个设计良好的类,你可以分别关注每个类。
OOP 是一种关注数据结构和处理这些数据结构的方法的方法。这种方法并不是每个程序都必须使用的,当然也有可能过度使用 OOP。但是 OOP 提供了使用许多高级特性的机会,我们将在接下来的两章中探讨这些特性。第一个特征是继承,我们将在下一章深入探讨。
相关文章:

Python 进阶指南(编程轻松进阶):十五、面向对象编程和类
原文:http://inventwithpython.com/beyond/chapter15.html OOP 是一种编程语言特性,允许你将变量和函数组合成新的数据类型,称为类,你可以从中创建对象。通过将代码组织成类,可以将一个整体程序分解成更容易理解和调试…...

windows下postgresql安装timescaledb
timescaledb是一个时序数据库,可以创建超表hypertable。它并不是一个独立的数据库,它依赖于postgresql,目前相当于postgresql的一个插件或者扩展。 要安装timescaledb,需要先安装postgresql。 这里安装的postgresql是12.14版本&am…...
Linux系统常用命令大全
本教程将介绍Linux系统的基本操作,包括文件操作、用户管理和软件安装等。 1. 文件操作 1.1 查看文件内容 使用cat命令可以查看文件的内容,例如:cat file.txt 1.2 创建新文件 使用touch命令可以创建新文件,例如:to…...

月报总结|Moonbeam 3月份大事一览
本月,Moonbeam在社区治理上进入了全新的阶段 — — 针对第一批生态系统Grants的Snapshot投票结果揭晓,链上公投已在进行中,社区获得了更多表达的机会与权力,这些项目也将为生态注入新的活力。 活动方面,Moonriver Ris…...

多功能料理锅语音播放芯片——NV040C
多功能料理锅就是一锅搭配多个锅盘,可以实现火锅、烤肉、花式煎蛋、丸子等多种烹饪功能。 多功能料理锅语音方案设计需求: 多功能锅本身体积有限,按钮比较少,相应功能的字体要贴按钮旁边,字体也是比较小的,…...

vue23自定义svg图标组件
可参考: 未来必热:SVG Sprites技术介绍 懒人神器:svg-sprite-loader实现自己的Icon组件 在Vue3项目中使用svg-sprite-loader 前置知识 在页面中,虽然可以通过如下的方式使用img标签,来引入svg图标。但是,…...

相机雷达时间同步(基于ROS)
文章目录运行环境:思路:同步前和同步后效果对比1.1创建工作空间1.2创建功能包2.1编写源文件2.2编写头文件2.3编写可执行文件2.4配置文件3.1编译运行4.1录制时间同步后的rosbag4.2rviz可视化rosbag运行环境: ubuntu20.04 noetic usb_cam 速腾R…...
素数环PrimeRing [3*]
目录 素数环PrimeRing [3*] 程序设计 程序分析 素数环PrimeRing [3*] 把1~N这N个整数摆成一个环,要求任意相邻两个数的和为素数。按字典序打印出以1开始的素数环 Input 一个整数N (<=10) Output 每行一个素数环。每个数之间用一个空格隔开。 无解输出 No Solution Sampl…...

mongodb 连接池配置
参考官方描述: 如果spring使用以下mongodb的配置,则默认是没有连接池的 spring:data:mongodb:host: 地址port: 27017database: 数据库名username: 账号password: 密码 每隔一两分钟没有去请求的话就会断开连接重连,每次都要等待5-10秒之间才…...

数据在内存中的存储(深度剖析)
目录 1.数据类型介绍 1.1类型分类 2.整形在内存中的存储 2.1原码,反码,补码 2.2大小端介绍 2.3练习 3.浮点型在内存中的存储 3.1浮点数存储规则 引入: 有正负的数据可以存放在有符号的变量中 只有正数的数据可以存放在无符号的变量…...

python 实现二叉搜索树的方法有哪些?
树的介绍 树不同于链表或哈希表,是一种非线性数据结构,树分为二叉树、二叉搜索树、B树、B树、红黑树等等。 树是一种数据结构,它是由n个有限节点组成的一个具有层次关系的集合。用图片来表示的话,可以看到它很像一棵倒挂着的树。…...
ORM概述
1_ORM概述[理解] 解释: 对象关系映射模型特点: 1.将类名,属性, 映射成数据库的表名和字段2.类的对象,会映射成为数据库表中的一行一行的数据 优缺点: 优点: 1.不再需要编写sql语句2.不再关心使用的是什么数据库了 缺点: 1.由于不是直接通过sql操作数据库,所以有性能损失 2_…...

程序员必知必会7种UML图(类图、序列图、组件图、部署图、用例图、状态图和活动图)画法盘点
众所周知,软件开发是一个分阶段进行的过程。不同的开发阶段需要使用不同的模型图来描述业务场景和设计思路,在不同的阶段输出不同的设计文档也是必不可少的,例如,在需求分析阶段需要输出领域模型和业务模型,在架构阶段…...

基于asp的搜索引擎开发和实现
随着因特网的迅猛发展、WEB信息的增加,用户要在信息海洋里查找信息,就像大海捞针一样,搜索引擎技术恰好解决了这一难题。目前,搜索引擎系统可以分类三大类,分别是:目录式搜索引擎:以人工方式或半…...

代码随想录刷题-字符串-实现 strStr()
文章目录实现 strStr()习题暴力解法kmp 解法实现 strStr() 本节对应代码随想录中:代码随想录,讲解视频:帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili、帮你把KMP算法学个通透!࿰…...

前端已死?金三银四?你收到offer了吗?
目录 一、前言 二、“唱衰” 三、不局限于框架、前端 四、打动面试官 五、正向加成 六、小结 一、前言 最近在脉脉、知乎等平台都有人在渲染前端从业人员的危机,甚至使用“前端已死”的字眼,颇有“语不惊人死不休”的意味,对老鸟来说&a…...

C生万物 | 十分钟带你学会位段相关知识
结构体相关知识可以先看看这篇文章 —— 链接 一、什么是位段 位段的声明和结构是类似的,有两个不同: 位段的成员必须是 int、unsigned int 或signed int位段的成员名后边有一个冒号和一个数字 在下面,我分别写了一个结构体和一个位段&…...

Spring Boot基础学习之(十):修改员工的信息
注意:spring boot专栏是一个新手项目,博文顺序则是功能实现的流程,如果有看不懂的内容可以到前面系列去了解。 本次项目所有能够使用的静态资源可以免费进行下载 静态资源 在本篇代码DAO层将通过Java文件去实现,在这里就不连接数…...
闭关十几天,我完成了我的毕业设计
个人简介 👀个人主页: 前端杂货铺 🙋♂️学习方向: 主攻前端方向,也会涉及到服务端(Node.js) 📃个人状态: 在校大学生一枚,已拿多个前端 offer(…...
认识rust的项目管理工具--cargo
cargo 提供了一系列的工具,从项目的建立、构建到测试、运行直至部署,为 Rust 项目的管理提供尽可能完整的手段。不过,我们无需再手动安装,之前安装 Rust 的时候(用rustup或者vscode加插件的方式安装)&#…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...

ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...

自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...

在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
LangFlow技术架构分析
🔧 LangFlow 的可视化技术栈 前端节点编辑器 底层框架:基于 (一个现代化的 React 节点绘图库) 功能: 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...