如果你過去是用函式、宣告式或命令式風格寫程式,那麼把重心轉移到物件導向程式設計(OOP)時,可能會覺得有點陌生。
物件導向的做法,會要求程式設計者把問題想成一個或多個會彼此互動、保存狀態,並對資料採取行動的 objects。
物件可以代表真實世界的實體(例如車子或貓咪),也可以是更抽象的概念(例如整數、交通工具或哺乳類)。
每個物件都會在電腦記憶體中成為一個獨特的實例,代表整體模型中的某個部分。
Classes 是新物件型別的定義,新的物件 instances 就是由它們建立出來的。
類別通常會把資料與操作這些資料的程式碼或函式綁在一起。
從這個角度來看,類別就像是_藍圖_,或是一組指示,讓我們能據以建立並使用許多相似型別的物件。
一個複雜的程式可以有許多類別,每個類別都能建立出各種不同風貌的物件。
從類別建立物件的過程稱為 instantiation(也就是建立該類別的實例)。
在 Python 中,類別的定義很直接:
class MyClass:
# Class body goes here
類別欄位(又稱為 properties、attributes、data members 或 variables)可以加進類別的主體中:
class MyClass:
number = 5
string = "Hello!"
只要呼叫 MyClass(就像呼叫函式一樣),就能建立它的實例(物件),並把它綁定到某個名稱上:
>>> new_object = MyClass()
# Class is instantiated and resulting object is bound to the "new_object" name.
# Note: the object address 'at 0x15adc55b0' will vary by computer.
>>> new_object
<__main__.MyClass at 0x15adc55b0>
Class attributes 會由從類別建立的所有物件(或實例)共享,可以透過dot notation存取,也就是把 . 放在物件名稱之後、屬性名稱之前:
>>> new_object = MyClass()
# Accessing the class attribute "number" via dot-notation.
>>> new_object.number
5
# Accessing the class attribute "string" via dot-notation.
>>> new_object.string
'Hello!'
# Instantiating an additional object and binding it to the "second_new_object" name.
>>> second_new_object = MyClass()
>>> second_new_object
# Note: the object address "at 0x15ad99970" will vary by computer.
<__main__.MyClass at 0x15ad99970>
# Second_new_object shares the same class attributes as new_object.
>>> new_object.number == second_new_object.number
True
類別屬性定義在類別本身的主體中,排在所有其他方法之前。 它們屬於類別,因此可以在類別的各個實例之間共享。 正因為這些屬性是被共享的,所以可以_直接_從類別存取與操作它們的值。 改變類別屬性的值,就會改變_所有從該類別實例化出來的物件_的值:
>>> obj_one = MyClass()
>>> obj_two = MyClass()
# Accessing a class attribute from an object.
>>> obj_two.number
5
# Accessing the class attribute from the class itself.
>>> MyClass.number
5
# Modifying the value of the "number" class attribute.
>>> MyClass.number = 27
# Modifying the "number" class attribute changes the "number" attribute for all objects.
>>> obj_one.number
27
>>> obj_two.number
27
讓一群物件隨時都保持同步的資料,其實沒什麼特別用處。
幸運的是,從類別建立的物件可以用自己的 instance attributes(或實例屬性、變數或欄位)來客製化。
顧名思義,實例屬性是每個物件各自獨有的,可以獨立修改。
__init__() 客製化物件實例化特殊的「dunder 方法」(是 "double underscore method" 的簡稱)__init__() 用來客製化類別實例,也可以為物件初始化實例屬性或特性。
由於 __init__() 負責初始化實例屬性,它也常被稱為 class constructor 或 initializer。
__init__() 會接受一個必要的參數,叫做 self,代表剛初始化或建立的物件。
接著,實例屬性或特性的資料就可以當成 __init__() 的引數傳入,接在 self 參數後面。
下面這個 MyClass 現在有兩個實例屬性,叫做 location_x 和 location_y。
如你所見,這兩個屬性被指定成傳入 __init__() 的 location(一個元組)引數的第一個和第二個索引值。
當你實例化這個類別、建立物件時,類別實例的 location_x 和 location_y 屬性就會被初始化:
class MyClass:
# These are class attributes, variables, or fields.
number = 5
string = "Hello!"
# This is the class "constructor", with a "location" parameter that is a tuple.
def __init__(self, location):
# This is an instance or object property, attribute, or variable.
# Note that we are unpacking the tuple argument into two separate instance variables.
self.location_x = location[0]
self.location_y = location[1]
# Create a new object "new_object_one", with object property (1, 2).
>>> new_object_one = MyClass((1, 2))
# Create a second new object "new_object_two" with object property (-8, -9).
>>> new_object_two = MyClass((-8, -9))
# Note that new_object_one.location_x and new_object_two.location_x two different values.
>>> new_object_one.location_x
1
>>> new_object_two.location_x
-8
注意,上面初始化 MyClass 時,你只需要傳入一個引數就好,當類別被呼叫時,Python 會自動幫你傳入 self。
method 是一種 function,它會綁定到類別本身(稱為類別方法,之後的練習會討論)或類別的_實例_(物件)。
要對物件(實例)進行操作的方法,定義時必須把 self 當成第一個參數。
接著,你可以像定義「一般」或非綁定函式那樣,定義其餘的參數:
class MyClass:
number = 5
string = "Hello!"
# Class constructor.
def __init__(self, location):
# Instance properties
self.location_x = location[0]
self.location_y = location[1]
# Instance method. Note "self" as first parameter.
def change_location(self, amount):
self.location_x += amount
self.location_y += amount
return self.location_x, self.location_y
和存取屬性一樣,呼叫方法只要把 . 放在物件名稱之後、方法名稱之前就行了。
被呼叫的方法不需要把物件本身當成第一個參數傳進去,Python 會自動幫你填入 self:
class MyClass:
number = 5
string = "Hello!"
def __init__(self, location):
self.location_x = location[0]
self.location_y = location[1]
def change_location(self, amount):
self.location_x += amount
self.location_y += amount
return self.location_x, self.location_y
# Make a new test_object with location (3,7)
>>> test_object = MyClass((3,7))
>>> (test_object.location_x, test_object.location_y)
(3,7)
# Call change_location to increase location_x and location_y by 7.
>>> test_object.change_location(7)
(10, 14)
實例方法內也能存取類別屬性,方式和在類別外部存取時一樣:
class MyClass:
number = 5
string = "Hello!"
def __init__(self, location):
self.location_x = location[0]
self.location_y = location[1]
# Alter instance variable location_x and location_y
def change_location(self, amount):
self.location_x += amount
self.location_y += amount
return self.location_x, self.location_y
# Alter class variable number for all instances from within an instance.
def increment_number(self):
# Increment the 'number' class variable by 1.
MyClass.number += 1
>>> test_object_one = MyClass((0,0))
>>> test_object_one.number
5
>>> test_object_two = MyClass((13, -3))
>>> test_object_two.increment_number()
>>> test_object_one.number
6
在之前的概念練習和練習題骨架中,你應該看過在函式主體裡用 pass 關鍵字來代替實際的程式碼。
pass 關鍵字是語法上合法的佔位符,可以避免 Python 因為空的函式或類別定義而拋出語法錯誤。
基本上,這就像是在對 Python 翻譯員說:「別擔心!我_之後_會把程式碼放進來,只是還沒動手而已。」
class MyClass:
number = 5
string = "Hello!"
def __init__(self, location):
self.location_x = location[0]
self.location_y = location[1]
# Alter instance variable location_x and location_y
def change_location(self, amount):
self.location_x += amount
self.location_y += amount
return self.location_x, self.location_y
# Alter class variable number for all instances
def increment_number(self):
# Increment the 'number' class variable by 1.
MyClass.number += 1
# This will compile and run without error, but has no current functionality.
def pending_functionality(self):
# Stubbing or placholding the body of this method.
pass
Ellen 正在做一款遊戲,玩家必須在其中對抗外星人。
她剛學會物件導向程式設計(OOP),很期待能運用 classes 為她的程式帶來的好處。
很高興的是,你答應要幫忙,於是她把程式設計這些玩家必須對抗的外星人的任務交給了你。
定義 Alien 類別,並替它加上一個建構式,接受兩個參數<x_coordinate>和<y_coordinate>,將它們存入 x_coordinate 和 y_coordinate 這兩個實例變數。
每個外星人一開始的生命值也都是 3,所以 health 變數也要一併初始化。
>>> alien = Alien(2, 0)
>>> alien.x_coordinate
2
>>> alien.y_coordinate
0
>>> alien.health
3
現在,每個外星人都應該能在內部追蹤自己的位置和生命值。
hit方法Ellen 希望 Alien class 有一個 hit 方法,呼叫時會把外星人物件的生命值減少 1。
這樣一來,她只要呼叫<alien>.hit()就好,不必手動更改外星人的生命值。
hit()要讓生命值剛好降到零還是降到零以下,由你決定。
>>> alien = Alien(0, 0)
# Initialized health value.
>>> alien.health
3
# Decrements health by 1 point.
>>> alien.hit()
>>> alien.health
2
is_alive方法你發現,生命值要是持續下降,總有一天會降到 0(甚至更低!)。
最好加上一個 is_alive 方法,讓 Ellen 可以快速呼叫,確認外星人是不是……呃……還活著。😉
<alien>.is_alive()應該要回傳布林值。
>>> alien.health
1
>>> alien.is_alive()
True
>>> alien.hit()
>>> alien.health
0
>>> alien.is_alive()
False
teleport方法在 Ellen 的遊戲裡,外星人有瞬間移動的能力!
你需要寫一個 teleport 方法,接受新的 x_coordinate 和 y_coordinate 值,並據此改變外星人的座標。
>>> alien.teleport(5, -4)
>>> alien.x_coordinate
5
>>> alien.y_coordinate
-4
collision_detection方法顯然,如果外星人會被東西擊中,牠們就得能夠偵測到這種碰撞發生。
不過,碰撞偵測演算法可能很棘手,而你還不知道該怎麼實作。
Ellen 說她之後會處理,但她還是希望 collision_detection 方法先出現在類別裡,提醒之後要把這個功能做出來。
它需要接受某種變數(大概是另一個物件),但你知道的真的就只有這些。
你要確保把這個方法定義放進類別後,呼叫時不會產生任何錯誤:
>>> alien.collision_detection(other_object)
>>>
Ellen 回來找你,又有新的需求。 她想追蹤整場遊戲期間總共建立了多少個外星人。 她說這跟計分系統有關。
例如:
>>> alien_one = Alien(5, 1)
>>> alien_one.total_aliens_created
1
>>> alien_two = Alien(3, 0)
>>> alien_two.total_aliens_created
2
>>> alien_one.total_aliens_created
2
# Accessing the variable from the class directly
>>> Alien.total_aliens_created
2
Ellen 很喜歡你目前做出來的成果,但她還想再請你幫個忙。
她想要一個獨立(在 Alien() 類別之外)的函式,它會接收一串位置(以 tuples 表示),並建立一個由 Alien() 物件組成的 list。
例如:
>>> alien_start_positions = [(4, 7), (-1, 0)]
>>> aliens = new_aliens_collection(alien_start_positions)
...
>>> for alien in aliens:
... print(alien.x_coordinate, alien.y_coordinate)
(4, 7)
(-1, 0)