隨手扎
Python的函數(Function)vs方法(method)
前言
※ 本段有些難度,略過並不影響後續理解。
在幾天前,我實現了一個可以傳入函數呼叫的類別(Callable Class),相關可以參考「用Python實現Callable Class」。
起初,我實現Meta-Class的方式,是在defineclz.__new__(clz, *args, **kwargs)
實例(instance)後,將實例增加函數成員(Function Member)。不過這樣做並不太成功。會需要使用instance.__call__(f, *args, **kargs)
去執行。而直接複寫defineclz.__call__
的話,以會造成不斷呼叫的問題,而且無法讓後續繼承子孫,再進一步調整方法。這也是為什麼後來以內部類別實現,並且另擁有__call__
能力的類別,後於defineclz
,好讓自定義的__call__
可以複寫掉預設行為。
※ Note: 本文寫到的函數成員(Function Member),主要是指實例屬性,但這個屬性是一個函數。更多時候,成員(Member)可能就包含方法(Method)和屬性(Attribute),而函數成員(Function Member)可能就直指方法(Method)。
定義方法(method)vs定義函數(function)
※ 尚為去翻找文件,可能有錯誤。
在Python,可以用def
關鍵字去定義一個函數:
def f1():
print("call f1")
f1()
而方法的定義也是使用def
:
class C1:
class_attr1 = {}
def __init__(self):
self.instance_attr1 = {}
def _f1(self):
print("C1: f1")
def f1(self):
self._f1() # if wrote `_f1()`, it will not found
def f2(self):
def _f2():
print("C1.f2: _f2")
_f2()
c1 = C1()
c1.f1()
c1.f2()
但其實這裡就可以看出一些行為的不同。上面_f1()
和f1()
看是在同一個scope,但是如果f1()
需要呼叫_f1()
,必須使用self._f1()
或是C1._f1(self)
,如果直接寫_f1()
會報錯說找不到。相對來說f2()
是C1
類別的方法,但是內部函式_f2
不是,因此就不需要這樣寫。現在在把f1()
移出類別外,變成一般函式看看:
def _f1():
print("call _f1")
def f1():
_f1()
f1()
上面這樣寫就沒問題。
類別方法定義 vs 實例方法
c1.class_attr1 is C1.class_attr1 # => True
id(c1.class_attr1) # => 139962927077200 (it's posible the id is different when you run)
id(C1.class_attr1) # => 139962927077200 (it's posible the id is different when you run)
####################
id(c1.instance_attr1) # => 139962927480048
# C1.instance_attr1 # C1 has not instance_attr1 attribute
####################
c1.f1 is C1.f1 # False
id(c1.f1) # => 139962961427080 (it's posible the id is different when you run)
id(C1.f1) # => 139962865916784 (it's posible the id is different when you run)
####################
實例屬性(instance attributes)是各自獨立的,所以各自都不同;但類別屬性(class attribute)是所有衍生實例共用,因此cl.class_attr1
和C1.class_attr1
是指向相同物件。
這似乎很好理解,當物件實例找不到屬性時,就會嘗試去類別定義尋找。但是,方法(method)的情況就有點有趣了。c1.f1
竟然不是C1.f1
?這實際上與類別內的f1
的__getter__
、__setter__
、__call__
,還有類別本身的__getattr__
行為有關。這有點複雜,以後有機會在更深一步研究來談。但簡單想,也就是在建立實例方法時,會將實例本身自己包裹(wrap)帶入第一個參數。在寫完實現Python裏面的classmethod和staticmethod後,有人告訴我,使用@functools.wraps
作為修飾器將方法包起來,會更好。
※ 不過我自己還未好好看過@functools.wraps
怎麼用。
之前在翻看
pydash.chain
的時候,也看到一些有點有意思的寫法。
動態加入方法 vs 動態加入成員
Python的函式也是第一公民(first class),可以作為參數傳遞,也可以作為回傳值。因為Python的特性,我們可以動態加入實例屬性,甚至這個屬性是個函式,就會變成函式成員。
c1.f2 = lambda self = None: print("call instance c2")
c1.f2()
這看起來就像是c1
有了f2
的方法一樣,但這與方法f1
有著差異:
c1.f1 # => <bound method C1.f1 of <__main__.C1 object at 0x7f4ba4e7f2e8>>
c1.f2 # => <function <lambda> at 0x7f4ba4e79a60>
type(c1.f1) # => <class 'method'>
type(c1.f2) # => <class 'function'>
type(C1.f1) # => <class 'function'>
注意!f1的描述是用「bound method」,而f2只是「function」。兩者是屬於不同類別。
不過,可以對類別增加函數成員,這會變成這個類別的方法。
C1.class_f2 = lambda self: print("call C1 method: f2")
C1.class_f2 # => <function <lambda> at 0x7f4ba4e79a60>
c1.class_f2 # => <bound method <lambda> of <__main__.C1 object at 0x7f4ba4e7f2e8>>
c1.class_f2() # => Output: call C1 method: f2
這樣的特性,是Python經常接觸到,卻又有點深的概念,希望有幫助到你。如果有什麼想法或發現錯誤,請留言讓我知道。
LINE Pay贊助 信用卡小額贊助