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加插件的方式安装)&#…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
pycharm 设置环境出错
pycharm 设置环境出错 pycharm 新建项目,设置虚拟环境,出错 pycharm 出错 Cannot open Local Failed to start [powershell.exe, -NoExit, -ExecutionPolicy, Bypass, -File, C:\Program Files\JetBrains\PyCharm 2024.1.3\plugins\terminal\shell-int…...
