Section 2: Classes (17-22)

本文中所使用的譯名,主要參考侯捷的 中英程式譯詞對照

class:類別
object:物件
instance:實體
property:屬性
attribute:屬性
initialize:初始化
method:方法(行為、函式)
function:函式(函數)

Section 2 github 原始碼


Initializing Class Instances

當我們用類別建立一個新實體時:

  1. The object instance is created - 物件實體創建
    Creates a new instance of the class.
    The creation of the object (instance).

  2. The object instance is then further initialized - 物件實體初始化
    Initializes the namespace of the class - 命名空間初始化
    The initialization of the object (instance).

class MyClass:
    language = 'Python'

obj = MyClass()  # obj.__dict__ => {}

Part 1:

我們可以在 __init__ 中,用 print 列印,就可以證明在 instance 創建時,__init__ 就會被執行。

而被綁定的 instance,因為在 __init__ 執行前就創建,所以才可以傳入 __init__ (第一個參數),以下程式碼的 namespace 位址,可以證明。

class Person:
    def __init__(self):
        print(f'Initializing a new Person object: {self}')

p = Person()
# Initializing a new Person object: <__main__.Person object at 0x7f80a022b0f0>

hex(id(p))
# '0x7f80a022b0f0'

Part 2:

正因為在 __init__ 已經傳入 instance,我們可以藉此來操控 instance 其中的屬性(*args, **kwargs)。

<instance>.__init__(self, *args, **kwargs)

class Person:
    def __init__(self, name):
        self.name = name

p = Person('Eric')
p.__dict__
# {'name': 'Eric'}

我們可以通過使用特殊方法來 “攔截” 創建和初始化階段:__new__ & __init__

這裡不是完整程式,只是說明。

class MyClass:
    language = 'Python'  # class attribute (in class namespace)

obj = MyClass() # obj.__dict__ => {}

def __init__(self, version):  # initialization, a method (bound function)
    self.version = version

__init__ 是一個 instance method .

__init__ 被呼叫時,object (instance) 已經被創建了。
__init__ 是綁定(bound)instance 的方法(method)。
__init__ function defined in the class is now treated like a method bound to the instance.

Part 3:

我們也可以不用 __init__,改用自定義的 init。只是這樣在 Initializing Class Instances 時,就不會自動執行,而要自己手動執行。

我們看一下實例:

# 方法一:正常作法 __init__
class Person:
    def __init__(self, name):
        self.name = name

p = Person('Eric')
p.__dict__
# {'name': 'Eric'}

# 方法二:自定義作法 init
class Person:
    def init(self, name):
        self.name = name

p = Person()  # 沒有傳參數(因為沒有實作 __init__)
p.__dict__
# {}  # 空的 dictionary

p.init('Eric')
p.__dict__
# {'name': 'Eric'}  # 結果和方法一相同

那當然是選簡單的方法做啊。


Creating Attributes at Run-Time

這節就講一件事:我們可以在 instance 創建之後,才將 attribute method 綁定到 instance 上。

  1. Attribute 可以是變數,也可以是函式(*args, **kwargs)。

  2. attribute method 可以在 __init__ 初始化時就綁定,也可以之後再綁定。

  3. __init__ 初始化時綁定的 method,每個 instance 都會有;之後再綁定的 method,只有被綁定的 instance 有。

Part I

在程式執行期間,修改 instance 的 attribute。

先看變數範例。以下示範兩件事:一、程式執行期間,修改 instance 的 attribute。二、以一的方法修改 attribute 時,只作用在被修改的 attribute。

class Person:
    pass

p1 = Person()
p2 = Person()

p1.name = 'Alex'
p1.__dict__
# {'name': 'Alex'}

p2.__dict__
{}

接著看函式範例。注意:這裡的函式,未與 instance 綁定(Binding)。

(程式續上方範例)
p1.say_hello = lambda: 'Hello!'

p1.__dict__
#  {'name': 'Alex', 'say_hello': <function __main__.<lambda>()>}

p1.say_hello
#  <function __main__.<lambda>()>

p1.say_hello()
'Hello!'

p2.__dict__
{}

Part II:MethodType

接著就來看,在程式執行期間,將函式與 instance 綁定,成為 method。

MethodType 語法如下。function 是我們要綁定的函式、object 是被綁定的 object (instance)。

from types import MethodType

MethodType(function, object)

MethodType 範例:

from types import MethodType

class Person:
    def __init__(self, name):
        self.name = name

p1 = Person('Eric')
p2 = Person('Alex')

def say_hello(self):
    return f'{self.name} says hello!'

say_hello(p1), say_hello(p2)
# ('Eric says hello!', 'Alex says hello!')

## 複習:MethodType(function, object)
## function 是我們要綁定的函式
## object 是被綁定的 object (instance)
p1_say_hello = MethodType(say_hello, p1) 
p1_say_hello
# <bound method say_hello of <__main__.Person object at 0x7f9750295630>>

## 但 p1 無法呼叫(dotted notation 和 getattr 皆是)
try:
    p1.p1_say_hello()
except AttributeError as ex:
    print(ex)
# 'Person' object has no attribute 'p1_say_hello'

## 將上述 method 加入 instance dictionary 中即可
p1.say_hello = p1_say_hello

p1.__dict__
# {'name': 'Eric', 
 'say_hello': <bound method say_hello of <__main__.Person object at 0x7f9750295630>>}

## 然後就可以呼叫了(dotted notation 和 getattr 皆是)
p1.say_hello()
# 'Eric says hello!'
getattr(p1, 'say_hello')()
'Eric says hello!'

## p2 目前為止,完全置身事外
p2.__dict__
{'name': 'Alex'}

簡單小結:

  1. 寫一個 function

  2. 透過 MethodType 與 instance 綁定

  3. 加入 instance dictionary 中(透過 dotted notation 指定)

p1 = Person('Alex')l
p1.__dict__
# {'name': 'Alex'}

## 上述三件事,一行程式搞定。
p1.say_hello = MethodType(lambda self: f'{self.name} says hello', p1)

p1.say_hello()
# 'Alex says hello'

Part III

假設我們要讓 class 中的同樣 function,依照 instance 各自的 attribute 執行。

繼承是一個解法,但透過上述方式(事後綁定)來做,可以達成類似一種 plugin 的效果。

from types import MethodType

class Person:
    def __init__(self, name):
        self.name = name
        
    def register_do_work(self, func):
        setattr(self, '_do_work', MethodType(func, self))
        
    def do_work(self):
        do_work_method = getattr(self, '_do_work', None)
        # if attribute exists we'll get it back, otherwise it will be None
        if do_work_method:
            return do_work_method()
        else:
            raise AttributeError('You must first register a do_work method')
math_teacher = Person('Eric')
english_teacher = Person('John')

try:
    math_teacher.do_work()
except AttributeError as ex:
    print(ex)
# You must first register a do_work method

數學老師的 do_work

def work_math(self):
     return f'{self.name} will teach differentials today.'

math_teacher.register_do_work(work_math)

math_teacher.__dict__
# {'name': 'Eric',
 '_do_work': <bound method work_math of <__main__.Person object at 0x7f97584cdac8>>}

math_teacher.do_work()
# 'Eric will teach differentials today.'

英文老師的 do_work

def work_english(self):
    return f'{self.name} will analyze Hamlet today.'

english_teacher.register_do_work(work_english)

english_teacher.do_work()
# 'John will analyze Hamlet today.'

instance properties

大家講到 Python Class 中的 attribute 和 method 時,最常拿來和一般的 property 和 function 作比較。所以先強調,本節所講的 property,是在說明 instance 中的 property。

instance properties 和 instance attributes 很類似,主要區別在於我們將使用訪問器方法(Accessor Method)來獲取、設置及刪除關聯的實體值。

補充參考資料:Accessor and Mutator methods in Python - GeeksforGeeks

  • Accessor Method: 此方法用於訪問對象的狀態,即可以通過此方法訪問隱藏在對像中的數據。但是,這個方法不能改變對象的狀態,它只能訪問隱藏的數據。我們可以用 get 來命名這些方法。

  • Mutator Method: 此方法用於改變/修改對象的狀態,即,它改變數據變量的隱藏值。它可以立即將變量的值設置為新值。此方法也稱為更新方法。此外,我們可以用單詞 set 來命名這些方法。

bare attribute 裸屬性

在Python中,裸屬性(Bare attribute)是指直接在類別定義中定義的屬性,而不使用特殊的裝飾器或設置器方法,例如@property@attribute.setter 等等。

裸屬性是一種簡單的數據屬性,只能通過 點記號(dotted notation)直接訪問或設置,無法對訪問或設置行為進行進一步控制。

class Person:
    def __init__(self, name):
        self.name = name

p = Person('Alex')

# 讀取、寫入:dotted notation or the getattr and setattr methods
p.name # 方法一
getattr(p, 'name') # 方法二

setattr(p, 'name', 'Eric')

假設我們要對上述的人名 name 設限,限定為 非空白字串

因為 Python 不像 Java 有 private 變數這種設定,所以採用約定成俗的前置底線 _ 來告訴其他程式設計師(或是忘性極佳的、未來的自己),這是內部專用的私有變數,建議你不要亂搞,否則出槌概不負責。

以下 _name 為範例:

class Person:
    def __init__(self, name):
        self.set_name(name)
        
    def get_name(self):
        return self._name
    
    def set_name(self, value):
        if isinstance(value, str) and len(value.strip()) > 0:
            # this is valid
            self._name = value.strip()
        else:
            raise ValueError('name must be a non-empty string')

然後乖乖的使用 get_name & set_name 來存取修改其值。

p = Person('Alex')

p.set_name('Eric')
p.get_name()

property function

因為程式設計師大多很懶,總想用最簡捷的方式來寫扣,所以 Python 就變出 property function 來滿足大家的需求。

讓程式看起來是使用 點記號(dotted notation),但骨子裡其實還是用 訪問器方法

class Person:
    def __init__(self, name):
        self.name = name  # note how we are actually setting the value for name using the property!
        
    def get_name(self):
        return self._name
    
    def set_name(self, value):
        if isinstance(value, str) and len(value.strip()) > 0:
            # this is valid
            self._name = value.strip()
        else:
            raise ValueError('name must be a non-empty string')
            
    name = property(fget=get_name, fset=set_name)

執行看看:

p = Person('Alex')
p.name

p.name = 'Eric'
try:
    p.name = None
except ValueError as ex:
    print(ex)
# name must be a non-empty string

補充:其實我們也可以直接呼叫 getattr & setattr,結果一樣。

setattr(p, 'name', 'John')  # or p.name = 'John'
getattr(p, 'name')  # or simply p.name

# 我們來看看 instance dictionary
p.__dict__
# {'_name': 'John'}

從上面可以看出,instance dictionary 裡是 _name 這個私有變數,而不是 name (property),請參考以下資訊。

Person.__dict__

# 輸出結果
mappingproxy({'__module__': '__main__',
              '__init__': <function __main__.Person.__init__(self, name)>,
              'get_name': <function __main__.Person.get_name(self)>,
              'set_name': <function __main__.Person.set_name(self, value)>,
              'name': <property at 0x7fbad886e138>, # 注意 name 的 type 是 property 
              '__dict__': <attribute '__dict__' of 'Person' objects>,
              '__weakref__': <attribute '__weakref__' of 'Person' objects>,
              '__doc__': None})

當看到 class 中使用 property function 時,Python 很清楚知道是要用 訪問器方法,而不是點記號(dotted notation)

看看 Python 並不會搞錯 _namename 的角色。

# 續
p = Person('Alex')

p.name
# 'Alex'

p.__dict__
# {'_name': 'Alex'}

p.__dict__['name'] = 'John'
p.__dict__
# {'_name': 'Alex', 'name': 'John'}

p.name # getter method
# 'Alex'

p.name = 'Raymond' # setter method
p.__dict__
# {'_name': 'Raymond', 'name': 'John'}

# 然後用 setattr & getattr 再做一次來證明,本處略。

接著老師示範 deleter method 的作法,一樣用 property function 來讓 Python 知道要用訪問器方法,這裡就不贅述了。

參考資料

Attribute vs. Property

Attribute Property
Attributes are defined by data variables like name, age, height etc. Properties are special type of attributes.
There are two types of attributes - Class attributes & Instance attributes Property method comes with the getter, setter and delete methods like get, set, and delete methods.
Class attributes are defined in the class body not in the functions. We can define getters, setters, and delete methods with the property() function.
Instance attributes are defined in the class body using the self keyword usually it the init() method. To read the property, we can use the @property decorator which can be added above our method.

來源:

講人話的解釋