Metaprogramming(1/4)

中英譯詞對照:

類別實例(class instance)


Metaprogramming 行前提示

  • Metaprogramming 很少用到。除非你要寫 framework 或 library,一般的應用程式是用不到的。

  • 不要學了 Metaprogramming 就想炫技。每個程式都想用 Metaprogramming 來寫,這會讓你的程式可讀性很差(包含一個月後回來看程式的你自己)。

  • 寫程式時,謹守 DRY 原則:Don’t Repead Yourself

  • 那為什麼還要學?因為這是理解 Python OOP 運作的重要入口。

  • 不要急。看不懂就多看幾次;未來感覺困惑時,一樣再回來複習。

  • 動手實作。這個我們強調很多次了,你無法觀看游泳影片就學會游泳。


Decorators and Descriptors


creation vs. initiation

創建 vs. 初始化

創建:類別創建的過程。一定義即創建。

# 以下的程式創建一個名為 MyClass 的類別:
class MyClass:

初始化:類別初始化的過程 :arrow_right: 類別的初始化是透過使用 __init__() 方法來完成

class MyClass:
    # 以下的程式,透過使用 `__init__()` 方法來初始化類別:
    def __init__(self, name, age):
        self.name = name
        self.age = age

#創建:類別創建的過程
class MyClass:

#初始化:類別初始化的過程 :arrow_right: 類別的初始化是透過使用 __init__() 方法來完成
def __init__(self, name, age):

我試著用 LaTeX 數學語法來繪製他們之間的關係:

\begin{array}{|l|} \hline 創建:類別創建的過程\\class\ MyClass:\\ \fbox{$\ \ 初始化:類別初始化的過程\\\ \ def\ \_\_init\_\_(self, name, age):\\\ \ \ \ self.name = name\\\ \ \ \ self.age = age$} & & \\ \\\hline \end{array}

__new__

行前提示

the __init__ method is called (bound to the new object) 綁定物件

Python 會自動執行 __new__ 然後 __init__,兩者參數必須相同。

你也可以自行呼叫,但必須自行逐一呼叫。

__new__(xxx, x, y) :arrow_right: x, y 會被忽略,__new__() 不接受參數,會直接將參數往後傳遞,在 __init__ 中接收 __new__() 傳過來的參數,然後做相關處理。

需使用 super() 才會繼承父物件。object 不會。

範例證明:person => student

__new__ 執行完後,之後會執行(但 Python 內部有些處理) __init__,兩者參數必須相同。

為什麼不直接用 ?? 就好?

  1. Python 會幫你自動執行,你不必重覆寫。

  2. 我們可以在 __new__ 中增加我們想要達成的事

示範 __new__ 中增加我們想要達成的事,不透過 __init__

示範 __new__ __init__ 回傳值不同

__new__ __init__ 回傳值相同時,Python 系統呼叫 __new__ 時,自動跟著呼叫 __init__

__new__ __init__ 回傳值不同時,Python 系統呼叫 __new__ 時,不再自動呼叫 __init__

實作 __new__ 的基本元素

class Person:
    def __new__(cls, name, age):
        # Do somthing
        print(f'Person: Instantiating {cls.__name__}...')

        # create the object to return
        # instance = object.__new__(cls)
        instance = super().__new__(cls)  ## <=== super() 才能繼承 parent 特性
        # Do more things with instance

        # return the object
        return instance
        
    def __init__(self, name):
        print(f'Person: Initializing instance...')
        self.name = name

羅馬 類別是怎麼建成的?

行前提示

  • exec

  • type(object) → the object’s type

  • type(name, bases, dict) → a new type

  • 請參考 Python 官網有關 class 的 中文說明

視覺化程式

Python Tutor 視覺化逐行執行程式碼 看看:



內心戲四部曲

Python Tutor 只能直接秀出關係圖,無法像老師那樣逐步說明。沒關係,我們自己腦補:

  1. Python 從 \color{#FF7F27}{class\ body} 部分(上圖橘框)提取資料(就是一長串文字,但它是有效的代碼)。

  2. 創建一個新字典,作為新類別的命名空間(dict 作為 namespace)。

  3. body code 在上述命名空間內執行,從而填入命名空間。

    和 module 中執行程式的思維相同:執行程式碼後,module namespace(模組命名空間)將包含 planet、name、__init__

  4. 使用 name of the class, the base classes & the populated dictionary,創建一個新的類別實例。

    type(class_name, class_bases, class_dict)

  • ‘Person’ in globals() # True

手工業又來了

和之前的課程相同。老師為了示範整個執行流程,手工一步一步打造類似的步驟。

因為之後會有簡單直接的做法,這裡當作理解流程的參考即可。

Step 1. 提取文字

class_name = 'Circle'

class_body = """
def __init__(self, x, y, r):
    self.x = x
    self.y = y
    self.r = r

def area(self):
    return math.pi * self.r ** 2
"""

Step 2. 創建 dict 作為 namespace

class_bases = ()  # defaults to object

class_dict = {}

Step 3. exec 執行 code,填入 namespace dict

exec(class_body, globals(), class_dict)

class_dict
# 以下為輸出
{'__init__': <function __main__.__init__(self, x, y, r)>,
 'area': <function __main__.area(self)>}

Step 4. 呼叫 type 建立 class

type(name, bases, dict) → a new type

Circle = type(class_name, class_bases, class_dict)

Circle
# 輸出:__main__.Circle

type(Circle)
# 輸出:type

Circle.__dict__
# 以下為輸出
mappingproxy({'__init__': <function __main__.__init__(self, x, y, r)>,  ## <=== class_body
              'area': <function __main__.area(self)>,  ## <=== class_body
              '__module__': '__main__',
              '__dict__': <attribute '__dict__' of 'Circle' objects>,
              '__weakref__': <attribute '__weakref__' of 'Circle' objects>,
              '__doc__': None})

複習:type 的兩種呼叫方式

  1. 一個參數:回傳物件的 type 時
type(Circle)
# 輸出:type
  1. 三個參數:創建 class 時(i.e. 創建 type 的 instance)
Circle = type(class_name, class_bases, class_dict)

客製化 type - 透過繼承 type

簡單的說,就是繼承 type,然後在 __new__ 裡面,做客製化的部分。

class CustomType(type): # 繼承 type

CustomType(name, bases, dict) # 呼叫 CustomType 產生 class(一樣三參數)。

CustomType 也是 metaclass

這節老師又以手工業方式,示範了手動 4步驟,主要的不同如下,其他我們就省略跳過了。

class CustomType(type):
    ...

- Circle = type(name, bases, dict)
+ Circle = CustomType('Circle', (), class_dict)

meta class 元類別

老做手工業當然很麻煩,終於講到正規的作法了(看老師自己也鬆了一口氣)。

class Person(metaclass = MyType):

其實我們一般的 class,就是 Python 預設,不用寫出來 class O_O(metaclass = type):

開始之前

Use those very rarely. They are not used frequently.

Don’t invent a problem just because you’ve got a solution.

整個 idea

class MyType(type):
    # mcls: metaclass(MyType)
	# name: name of class(Person)
	def __new__(mcls, name, bases, cls_dict):
	# Do something

	# create the class itself via delegation
	new_class = super().__new__(mcls, name, bases, cls_dict)
	# Do more thins

	# and return the new class 
	return new_class

# Do all the manual steps: name, code, class dict, bases
# then calls MyType(name, bases, cls_dict)
class Person(metaclass = MyType):
	def __init__(self, name):
	    self.name = name

實際範例:

class CustomType(type):
    def __new__(mcls, name, bases, class_dict):
        print(f'Using custom metaclass {mcls} to create class {name}...')  ## <=== Do something
        cls_obj = super().__new__(mcls, name, bases, class_dict)
        cls_obj.circ = lambda self: 2 * math.pi * self.r ## <=== Do more things
        return cls_obj

class Circle(metaclass=CustomType): ## <=== Custom Type
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        self.r = r
        
    def area(self):
        return math.pi * self.r ** 2

# 輸出:Using custom metaclass <class '__main__.CustomType'> to create class Circle...

檢驗

Circle
# 輸出:__main__.Circle

vars(Circle)
# 以下為輸出:標註處是 Python metaclass 自動幫我們處理的
mappingproxy({'__module__': '__main__',
              '__init__': <function __main__.Circle.__init__(self, x, y, r)>,  ## <===
              'area': <function __main__.Circle.area(self)>,  ## <===
              '__dict__': <attribute '__dict__' of 'Circle' objects>,
              '__weakref__': <attribute '__weakref__' of 'Circle' objects>,
              '__doc__': None,
              'circ': <function __main__.CustomType.__new__.<locals>.<lambda>(self)>})  ## <===


c = Circle(0, 0, 1)
print(c.area())  # 3.141592653589793
print(c.circ())  # 6.283185307179586

適用情境?


compile 時,Python 看到 class,就 create

a class is an instance of type

type 是專門用來 create 其他 class

type 有兩種呼叫方式:

  1. 單一參數:傳回 object’s type

  2. 三參數(name, bases, dict):建立新的 type

help(type)
## 以下為輸出
class type(object)
 |  type(object_or_name, bases, dict)
 |  type(object) -> the object's type
 |  type(name, bases, dict) -> a new type
 |  ...

__new____init__ 通常只需實作一個,而且是 __new__


__init__

__init__ 是類別的初始化方法,通常用來為類別實例設定屬性初始值。它會在每個類別實例被建立時自動呼叫。

__init__ 方法的參數是 selfself 是類別實例本身的參考,可用來在方法中存取類別實例的屬性和方法。

以下程式定義一個 Person 類別,並在 __init__ 方法中設定 nameage 屬性:

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

要建立 Person 類別的實例,可以使用 Person() 語法。以下程式建立一個名為 john 的年齡為 30 歲的 Person 實例:

john = Person("John", 30)

__new__

__new__ 是類別的特殊方法,它在類別實例被建立之前被呼叫。

  • __new__ 方法負責建立類別實例,並返回類別實例。

  • __init__ 方法在 __new__ 方法之後被呼叫,並對類別實例進行初始化。

__new__ 方法沒有任何參數。

__new__ 方法必須返回類別實例。

__new__ 方法可用來控制類別實例的建立。

以下程式碼定義一個類別 Person,並在 __new__ 方法中確保類別實例的年齡必須大於 18 歲:

class Person:
    def __new__(cls, name, age):
        if age < 18:
            raise ValueError("Age must be greater than 18")
        return super().__new__(cls)

要建立 Person 類別的實例,可以使用 Person() 語法。例如,以下程式碼會引發 ValueError 異常,因為 age 小於 18 歲:

john = Person("John", 17)

要建立 Person 類別的實例,可以使用 Person(name, age) 語法。例如,以下程式碼會建立一個名為 john 的年齡為 21 歲的 Person 實例:

john = Person("John", 21)

__new__ 方法可用來在類別實例被建立時執行任何必要的初始化。