Object Oriented Programming and Classes
- Creating and Using a Class
- Working with Classes and Instances
- OOP三大架構:繼承(Inheritance)、封裝 (Encapsulation)、多型(Polymorphism)
- Class Methods
- Data Class
Creating and Using a Class
最基本的類別(Class)的用法
假設我要設計一個 Animal 的 class 類別,那程式會長這樣,裡面什麼東西都沒有的話要加上一個 pass 語句,
class Animal:
pass
The init() Method
建構子(Constructor)
那我要加入 Animal 類別一個建構子的話(參數為空),是這樣寫的,加入 init 函式,第一個參數必須為self,self 表示這個類別
class Animal:
def __init__(self):
pass
__init__()
方法是一個特殊的方法,有兩個前導下劃線和兩個尾隨下劃線有助於防止Python
的默認方法名與您的方法名衝突。
成員變數(Class member)
那我要讓建構子傳入 name 參數並且保存到成員變數的話,self.name 表示這個類別的 name 成員變數,pass 移除是因為建構子裡面有寫東西了所以不用擺 pass 語句了
class Animal:
def __init__(self, name):
self.name = name
self
參數在方法定義中是必需的,並且必須在任何其他參數之前出現。它必須包含在定義中,因為當Python
稍後調用此方法時,方法調用將自動傳遞self
參數。- 在
__init__()
方法的主體中定義的兩個變量都具有前綴self
。任何以self
為前綴的變量(稱為實例屬性)都可用於類中的每個方法,我們還可以通過從類創建的任何實例來訪問這些變量,這些變量可以在實例之間不同。 self.name = name
這行將與參數name
關聯的值賦給變量name
,然後將其附加到正在創建的實例。self.age = age
也是同樣的過程。像這樣通過實例訪問的變量稱為(實例)屬性。
成員函式(Member function)
這個 Animal 類別要有一個 eat 的函式,並且印出該動物吃的是什麼東東,那這個食物就讓 Animal 類別建構的時候一併傳進來保存到成員變數裡,
class Animal:
def __init__(self, name, food):
self.name = name
self.food = food
def eat(self):
print("I am " + self.name + ", I eat " + self.food)
Working with Classes and Instances
The Car
Class
class Car:
"""A simple attempt to represent a car."""
def __init__(self, make, model, year):
"""Initialize attributes to describe a car."""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
"""Return a neatly formatted descriptive name."""
long_name = f"Created in {self.year}, {self.make} {self.model}"
return long_name.title()
def read_odometer(self):
"""Print a statement showing the car's mileage."""
print(f"This car has {self.odometer_reading} miles on it.")
- 首先定義了
__init__()
方法 - 給它其他參數:
make
,model
,year
和odometer_reading
。__init__()
方法接收這些參數並將它們分配給從此類創建的實例相關聯的屬性。
當我們創建一個新的Car
實例時,我們需要為我們的實例指定一個make
,model
和year
。我們定義了一個名為get_descriptive_name()
的方法,它將一輛汽車的year
,make
和model
放入一個字符串中,並將汽車描述得很好。要在此方法中使用屬性值,我們使用self.make
,self.model
和self.year
。
當創建一個實例時,屬性可以在不作為參數傳遞的情況下定義。這些屬性可以在__init__()
方法中定義,該方法分配了一個默認值。在上面的示例中,名為odometer_reading
的屬性始終從0開始。最後,有一個方法read_odometer()
,它可以幫助我們讀取每輛汽車的里程表。
Modifying Attribute Values
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
# Output: This car has 23 miles on it.
有時候,有些方法可以幫助我們更新屬性值。我們不直接訪問屬性,而是將新值傳遞給一個方法,該方法在內部處理更新。
class Car:
"""A simple attempt to represent a car."""
def __init__(self, make, model, year):
"""初始化屬性"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
"""回傳一個整潔的描述性名稱"""
long_name = f"Created in {self.year}, {self.make} {self.model}"
return long_name.title()
def read_odometer(self):
"""把車的里程表讀出來"""
print(f"This car has {self.odometer_reading} miles on it.")
## We add these there methods!
def update_odometer(self, mileage):
"""
把里程表設定為指定的值
禁止把里程表往回調
"""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
"""里程表增加指定的量"""
self.odometer_reading += miles
def fill_gas_tank(self):
"""填滿油箱"""
print("The gas tank is now full!")
Car
類中唯一的修改是添加update_odometer()
。此方法接收里程值並將其分配給self.odometer_reading
。- 檢查新的讀數是否合理,然後再修改屬性。如果里程數提供的值大於或等於現有里程數
self.odometer_reading
,則可以將里程表讀數更新為新里程數。如果新里程小於現有里程,則會收到無法返回里程表的警告 - 此外,我們還定義了一個新方法
increment_odometer()
,它接收一個里程數並將此值添加到self.odometer_reading
。 - 最後,還向類別添加了一個方法
fill_gas_tank()
。
my_new_car = Car('audi', 'a4', 2023)
print(my_new_car.get_descriptive_name())
my_new_car.update_odometer(23)
my_new_car.read_odometer()
my_new_car.fill_gas_tank()
my_new_car.increment_odometer(100)
my_new_car.read_odometer()
# Created In 2023, Audi A4
# This car has 23 miles on it.
# The gas tank is now full!
# This car has 123 miles on it.
__repr__
and __str__
method
Python
文檔指出,__repr__
返回對象的“官方”字符串表示形式。
我們還定義了__str__
特殊方法,該方法用於在某些情況下替換__repr__
的行為。
當您使用內置函數str()
將對象轉換為字符串時,將調用此方法,例如在打印對象或明確調用str()
時。
print(my_new_car)
str(my_new_car)
# Output: <__main__.Car object at 0x0000020E4F6F4E80>
像
__init__()
,__str__()
和__repr__
這樣的特殊方法被稱為dunder methods(雙下劃線)。 有許多dunder方法可用於自定義類
OOP三大架構:繼承(Inheritance)、封裝 (Encapsulation)、多型(Polymorphism)
Inheritance (繼承)
“is a” releationship
繼承主要是讓程式碼可以重複被使用,借此可以減少浪費在重複性工作的時間。
當子類需要繼承父類的 Attribute 時,需加上 super().init(),並於第二個括號中填入欲繼承的屬性。
範例中 ElectricCar 直接繼承了 Car 的全部屬性,
若再加上 ElectricCar 需求的屬性,general。故於建立 Object 時,若需呼叫子類則必須填上4個變數,而非原本父類的3個。
The __init__()
method for a Child Class
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
- 定義子類時,必須在括號內指定父類的名稱。
- 方法
super()
是一個特殊函數,它幫助Python將父類和子類關聯起來。這行代碼讓Python調用ElectricCar
的父類的方法__init__()
,讓ElectricCar
實例包含父類的所有屬性。 - 父類也稱為超類(superclass),名稱
super
來自這個名詞。
Overriding Methods from the Parent Class
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
self.battery_size = 75
def describe_battery(self):
print(f"This car has a {self.battery_size}-kWh battery.")
- 定義了一個名為
describe_battery()
的方法,它顯示一條描述電瓶容量的消息。 - 這個方法覆蓋了父類
Car
中的方法describe_battery()
。在子類ElectricCar
中定義的這個方法將忽略父類Car
中的這個方法。
Use composition
to organize the code
有時候,你會發現你的類描述的是一個物件的部分行為,而不是整個行為。在這種情況下,你可以將這個類作為另一個類的屬性,這被稱為組合。
class Battery:
def __init__(self, battery_size=75):
self.battery_size = battery_size
print(f"This car has a {self.battery_size}-kWh battery.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
self.battery = Battery()
- 定義了一個名為
Battery
的新類,它沒有繼承任何類。Battery
類有兩個屬性和一個方法:__init__()
,battery_size
和describe_battery()
。 - 在
ElectricCar
類中,我們添加了一個名為self.battery
的屬性。這行代碼讓Python創建一個新的Battery實例(由於沒有指定大小,因此獲得預設值75),並將該實例存儲在屬性self.battery中。每當方法__init__()
被調用時,都會執行該操作;因此,現在每個ElectricCar實例都包含一個自動創建的Battery實例。 - 我們創建了一個名為
self.battery
的Battery實例,並將該實例存儲在屬性self.battery中。這樣,就可以在需要時使用電瓶了。
當我們需要知道一個對象的類型時,我們可以將對象傳遞給內置的type()
函數。
但是,如果我們正在對對象進行類型檢查,最好使用更靈活的isinstance()
內置函數。如果對象是給定類的子類,則isinstance()
函數將返回True
。
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(isinstance(my_tesla, ElectricCar)) # True
print(isinstance(my_tesla, Car)) # True
print(isinstance(my_tesla, Battery)) # False
Encapsulation (封裝)
大多數面向對象的編程語言都可以將對象的數據封裝 (encapsulate)(或隱藏)起來。這些語言中的數據被稱為私有數據 (private data)。
Python
沒有私有數據。相反,您可以使用命名約定來設計類,以鼓勵正確使用。
按照慣例,**Python程序員知道以下劃線(_
)開頭的任何屬性名僅用於類的內部使用。**代碼應使用類的方法來與每個對象的內部使用數據屬性交互。其標識符不以下劃線(_
)開頭的屬性被認為是公開可訪問的。
Let’s develop a Time
class that stores the time in 24-hour clock format with hours in the range 0–23 and minutes and seconds each in the range 0–59:
class Time:
"""Represents the time of day.
attributes: hour, minute, second
"""
def __init__(self, hour=0, minute=0, second=0):
"""Initializes a time object.
hour: int
minute: int
second: int or float
"""
self.hour = hour
self.minute = minute
self.second = second
Time
類有三個屬性:hour
,minute
和second
。這些屬性將存儲整數值,但也可以存儲浮點值。__init__()
方法的參數提供了默認值,因此如果我們只傳遞一個時間參數,則分鐘和秒鐘將為零。- 為了創建一個新的
Time
對象,我們調用Time
類的構造函數。我們將Time
對象分配給start
變量。
wake_up = Time(hour=6, minute=30)
wake_up.get_hour()
# Instead of wake_up._hour
wake_up.set_hour(8)
# Instead of wake_up._hour = 8
類Time
的getter和setter定義了類的公共接口,即程序員應該用來與該類的對象交互的屬性集。就像上面的私有屬性一樣,並非所有方法都需要作為類的接口的一部分。有些方法只用作類內部使用的utility methods,不是用於類的公共接口的一部分。此類方法應以單個下劃線開頭命名。在其他面向對象的語言(如C ++,Java和C#)中,此類方法通常實現為private methods。
Polymorphism (多型)
多態性允許將一種類型的對象視為另一種類型的對象。
- 例如
len()
函數返回傳遞給它的參數的長度。
您可以將string
傳遞給len()
以查看它有多少個字符,也可以將list
或dictionary
傳遞給len()
以查看它有多少個項目或鍵值對。函數的這種多態性稱為通用函數或method/function overloading,因為它可以處理許多不同類型的對象。 - 多態性還包括operator overloading,其中運算符(例如
+
或*
)可以根據它們正在操作的對象的類型而表現出不同的行為。例如,當操作兩個integer
或float
值時,+
運算符執行數學加法,但是當操作兩個字符串時,它執行字符串連接。
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
def speak(animal):
print(animal.speak())
animals = [Dog("Rufus"), Cat("Whiskers"), Dog("Buddy")]
# method overriding
for animal in animals:
print(f'{animal.name} : {animal.speak()}')
# method overloading
for animal in animals:
speak(animal)
# Output:
# Rufus : Woof!
# Whiskers : Meow!
# Buddy : Woof!
# Woof!
# Meow!
# Woof!
在這個例子中,Animal
類定義了speak
方法作為pass
語句,這意味著它什麼也不做。
但是,Dog
和Cat
類都覆蓋了該方法,並使用它們的方法實現該方法。這種方法稱為method overriding,也是多態的一種形式。
speak()
函數接受實現speak()
方法的任何對象,這意味著它可以處理不同類型的動物。在這裡,我們可以將Dog
和Cat
對象都傳遞給speak()
函數,因為它們都從Animal
類繼承了speak()
方法。
Simulating “Private” Attributes
下面的代碼定義了一個名為PrivateClass
的類,其中包含一個公共屬性和一個私有屬性。
私有屬性的名稱以兩個下劃線開頭,但沒有下劃線結尾。是一種規定,用於區分公共屬性和私有屬性。
class PrivateClass:
"""Class with public and private attributes."""
def __init__(self):
"""Initialize the public and private attributes."""
self.public_data = "public" # public attribute
self.__private_data = "private" # private attribute
my_object = PrivateClass()
my_object.public_data
# 'public'
my_object.__private_data
# AttributeError: 'PrivateClass' object has no attribute '__private_data'
我們可以訪問my_object
的公共屬性,但不能訪問私有屬性。如果我們嘗試訪問私有屬性,則會出現AttributeError
。
即便如此,我們仍然可以訪問私有屬性。我們可以通過使用
_Classname__private_data
來訪問私有屬性。
Class Methods
類方法是與類本身相關聯的函數。它們與類的實例無關。我們可以使用@classmethod
裝飾器來定義類方法。類方法的第一個參數是cls
,它是類本身。類方法可以訪問類的屬性,但不能訪問類的實例的屬性。
class ExampleClass:
def exampleRegularMethod(self):
print('This is a regular method.')
@classmethod # This is the "decorator" that takes another function as input, extends or modifies its behavior, and returns a new function
def exampleClassMethod(cls):
print('This is a class method.')
# Call the class method without instantiating an object:
ExampleClass.exampleClassMethod()
obj = ExampleClass()
# Given the above line, these two lines are equivalent:
obj.exampleClassMethod()
obj.__class__.exampleClassMethod()
cls
參數的作用類似於self
,但是self
指的是一個對象,而cls
參數指的是對象的類。這意味著類方法中的代碼不能訪問單個對象的屬性或調用對象的常規方法。類方法只能調用其他類方法或訪問類屬性。我們使用cls
的名稱,因為class
是Python關鍵字。我們通常通過類來調用類方法,例如ExampleClass.exampleClassMethod()
。但是我們也可以通過類的任何對象來調用它們,例如obj.exampleClassMethod()
。
類方法不常用。最常見的用例是除了__init__()
之外提供替代構造函數。例如,如果構造函數可以接受新對象所需的數據的string
,或者包含新對象所需數據的文件名的string
,該怎麼辦?我們不希望__init__()
方法的參數冗長且令人困惑。相反,讓我們使用類方法來返回一個新對象。例如,讓我們創建一個AsciiArt
類:
class AsciiArt:
def __init__(self, characters):
""" Approach1: Initialize it with string """
self._characters = characters
@classmethod
def fromFile(cls, filename):
""" Approach2: Initialize it with filename """
with open(filename) as fileObj:
characters = fileObj.read()
return cls(characters) # This calls the __init__ function
def display(self):
print(self._characters)
# Other AsciiArt methods would go here...
Class Attributes
以前我們寫實例屬性時,都將屬性的初值設定放在建構子constructor之內。但是類別屬性卻不一樣,它們必須放在建構子的「外面」,前置後置都行,就是不能位於constructor裡面:
class Tree():
count = 0 # 放在constructor外面(前置)的是class attributes。
total_age = 0
average_age = 0
def __init__(self, breed: str, age: int): # constructor
...
或:
class Tree():
def __init__(self, breed: str, age: int): # constructor
...
count = 0 # 放在constructor外面(後置)的是class attributes。
total_age = 0
average_age = 0
不過,類別屬性一般都寫在建構子的前面。
Static Methods
靜態方法通常會以@staticmethod
裝飾器包裝。
- 用類別.方法()或物件.方法()兩種方式都可以呼叫
- 靜態方法本來就屬於整個類別,和物件無關,即使未建立任何物件也可以使用。
- 靜態方法和非靜態方法相比,語法上有一個很大的不同:靜態方法的第一個參數既不是self也不是cls。事實上靜態方法根本「不能有」self或cls。原因為:static methods不會接收隱藏的第一個參數。
- 正因其參數列沒有self或cls,靜態方法無法存取類別內的任何屬性,也不能呼叫類別的任何方法,連其他靜態方法也沒法呼叫。靜態方法是「獨善其身」。
class ExampleClassWithStaticMethod:
@staticmethod
def sayHello():
print('Hello!')
# Note that no object is created, the class name precedes sayHello():
ExampleClassWithStaticMethod.sayHello()
# Output: Hello!
Static Methods在其他沒有Python靈活語言功能的語言中更常見。 Python包含靜態方法模仿其他語言的功能,但實際價值不大。
Operator overloading
Python
有幾種dunder方法。
您已經熟悉__init__()
dunder方法名稱,但是Python
還有其他幾種方法。
我們通常使用它們來進行運算符重載-即添加自定義行為,使我們能夠使用我們類的對象與Python
運算符(例如+
或> =
)一起使用。其他dunder方法讓我們類的對象與Python
的內置函數一起使用,例如len()
。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __eq__(self, other):
return (self.x == other.x) and (self.y == other.y)
# 調用__add__方法
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2
p4 = Point(4, 6)
print(p3.x, p3.y) # Output: 4 6
print(p3 == p4)
Data Class
中文翻譯:Data classes是Python 3.7最重要的新功能之一。它們通過使用更簡潔的表示法並自動生成大多數類中常見的“樣板”代碼來幫助您更快地構建類。
數據類自動生成__init__
,__repr__
和__eq__
,節省您的時間。
數據類可以自動生成重載<
,<=
,>
和>=
比較運算符的特殊方法。
當您更改數據類中定義的數據屬性,然後在腳本或交互式會話中使用它時,自動生成的代碼會自動更新。 因此,您需要維護和調試的代碼更少。
某些靜態代碼分析工具和IDE可以檢查變量註釋,並在代碼使用錯誤類型時發出警告。 這可以幫助您在執行代碼之前找到代碼中的邏輯錯誤。
詳細:Here以獲取更多詳細信息。