1. 程式人生 > 其它 >(轉)[Python]例項方法、類方法、靜態方法

(轉)[Python]例項方法、類方法、靜態方法

原文:https://zhuanlan.zhihu.com/p/40162669

一、類(class)和例項(instance)

類是建立例項的模板,而例項則是一個一個具體的物件,各個例項擁有的資料都互相獨立,互不影響。以Dog類為例,類就像一個物件工廠,可以生產一個或多個例項物件。

>>> class Dog(object):
...     pass
...
>>> Dog
<class '__main__.Dog'>

>>> dog = Dog()
>>> dog
<__main__.Dog object at 0x10cd194a8>

>>> dog2 = Dog()
>>> dog2
<__main__.Dog object at 0x10cd19518>

二、初探例項方法、類方法、靜態方法

先直接上程式碼:

class MyClass(object):
    # 例項方法
    def instance_method(self):
        print('instance method called', self)
    
    # 類方法
    @classmethod
    def class_method(cls):
        print('class method called', cls)
    
    # 靜態方法
    @staticmethod
    def static_method():
        print('static method called')

上述MyClass類中分別定義了三種不同型別的方法。

這三種方法在定義上(形式上)有如下的不同之處:

  1. 例項方法是一個普通的函式,類方法和靜態方法都是通過函式裝飾器的方式實現的;
  2. 例項方法需要傳入self,類方法需要傳入cls引數,靜態方法無需傳入self引數或者是cls引數(但不等同於不能傳入引數)

注意:self引數和cls引數的區別,下文會提到。

下面分別嘗試呼叫三個方法:

my_class = MyClass()        # 例項化

my_class.instance_method()  # 例項方法
my_class.class_method()     # 類方法
my_class.static_method()    # 靜態方法

1️⃣當例項化時,my_class指向例項化物件:

2️⃣當呼叫例項方法(instance method)時,self引數指向的是剛剛例項化出的my_class例項物件:

3️⃣當呼叫類方法(class method)時,cls引數指向的是一開始定義的MyClass類物件(注意不是例項物件):

4️⃣當呼叫靜態方法(static method)時:

本小節內容,主要區別了例項方法、類方法、靜態方法的定義形式,以及區別了self、cls引數的指向物件。

注意:不管self引數,還是cls引數,都是一種約定俗成的用法,其實是可以使用其他的引數名代替。但是不建議使用其他引數名,畢竟程式碼不是隻是寫給自己看的。

三、再探例項方法、類方法、靜態方法

上文都是通過例項化出的物件,對三種方法進行的呼叫,並且都得到了我們想要的結果;如果不通過例項化物件,而是直接通過類物件(Python中一切皆物件,包括類本身)呼叫,會有什麼結果呢?

MyClass.instance_method()   # 例項方法
MyClass.class_method()      # 類方法
MyClass.static_method()     # 靜態方法

1️⃣當通過類物件呼叫例項方法時,直接報錯,提示缺引數self。

因為點語法會將類物件MyClass傳給instance_method()方法,但是instance_method()引數需要指向的是一個例項物件,而非類物件。

TypeError: static_method() missing 1 required positional argument: 'self'

2️⃣當通過類物件呼叫類方法時,這裡的結果與通過例項物件呼叫是完全一樣的。

Python官方文件的解釋:It can be called either on the class (such asC.f()) or on an instance (such asC().f()). The instance is ignored except for its class. If a class method is called for a derived class, the derived class object is passed as the implied first argument.

大致意思,類方法可以通過類物件或者例項物件呼叫。如果是通過例項物件呼叫的,那麼例項物件會被忽略,而是轉而通過其類物件進行呼叫。類物件,會以引數形式傳給類方法(點語法),作為類方法的第一個引數,也就是cls引數。

3️⃣當通過類物件呼叫靜態方法時,這裡的結果與通過例項物件呼叫是完全一樣的。通過例項物件呼叫靜態方法時,例項物件也會被忽略,而是通過其類物件進行呼叫,這一點與類方法的呼叫是一樣的。

說明:為了簡化所舉的例子,本文中所定義的MyClass類並沒有採用self或者cls之外的引數,三種方法都可以使用額外的引數,跟函式定義引數是一樣的。

四、類方法使用場景

由於類方法無須建立例項物件呼叫,所以類方法的呼叫較例項方法更為靈活。以Django中View檢視類原始碼舉例:

class View(object):
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in six.iteritems(kwargs):
            setattr(self, key, value)

    @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

    def dispatch(self, request, *args, **kwargs):
        # 省略部分程式碼
        pass

    def http_method_not_allowed(self, request, *args, **kwargs):
        # 省略部分程式碼
        pass

    def options(self, request, *args, **kwargs):
        # 省略部分程式碼
        pass

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

Django中通過類對檢視方法實現了封裝,上述程式碼就是Django中定義的View檢視的部分原始碼,其中as_view()方法就使用了類方法。但是如果觀察仔細的話,你會發現,as_view()方法並不是使用的@classmethod裝飾器,而是使用的@classonlymethod。在定義形式上,傳入的引數是cls類物件,而不是self例項物件,那麼這裡as_view()方法是不是類方法呢?是的。

因為Django中classonlymethod繼承了classmethod,但是classonlymethod只能通過類物件呼叫,而不能通過例項物件呼叫as_view()方法,即‘閹割’了例項呼叫的方式。

class classonlymethod(classmethod):
    def __get__(self, instance, cls=None):
        if instance is not None:
            raise AttributeError("This method is available only on the class, not on instances.")
        return super(classonlymethod, self).__get__(instance, cls)

在呼叫as_view()方法時,無需建立例項物件,只需傳入cls類物件,就能針對不同請求方法實現自動派發檢視處理邏輯。

五、靜態方法使用場景

靜態方法既不需要傳入self引數,也不需要傳入cls引數,這使得呼叫靜態方法並不能獲得類中定義的屬性和其他方法,也就是說並不會影響類物件或例項物件的狀態,這點與例項方法或類方法不一樣。

靜態方法有點像附屬於類物件的“工具”。與普通函式不同,呼叫靜態方法時,只能通過類物件(或者例項物件),而不能脫離類物件使用,即靜態方法被‘束縛’在類物件中。比如,建立一個pizza類物件:

import math


class Pizza(object):
    """模擬pizza店接收顧客自定義訂購pizza"""

    def __init__(self, ingredients, radius):
        self.ingredients = ingredients
        self.radius = radius

    def show_ingredients(self):
        # 選購pizza食材
        pass

    def show_size(self):
        # 選購pizza大小
        pizza_size = self.cal_area(self.radius)
        return pizza_size

    @staticmethod
    def cal_area(r):
        return int(math.pi * r * r)


if __name__ == '__main__':
    # 測試一下,半徑為20釐米的pizza大小,如果顧客覺得大了,訂一個小一點的
    print(Pizza.cal_area(20))
    # 經過上述測試,發現半徑為20釐米的pizza有點大,訂購一個小一點的
    # 建立pizza例項物件
    my_pizza = Pizza('番茄牛肉pizza', 15)
    pizza_size = my_pizza.show_size()
    # pizza大小合適,滿意
    print(pizza_size)

上述Pizza類中的靜態方法,不僅提供了測試功能,還將計算pizza大小的邏輯從show_size()方法中抽離出來,如果需要修改cal_area()方法的內部處理邏輯,只需要修改該方法,而不需要修改show_size(),這樣有利於程式碼的後期維護。

如果把cal_area()方法定義在Pizza類物件之外,也是可以的,不過這樣也不利於程式碼維護,將物件的相關處理邏輯“束縛”在物件體內,這樣封裝得會更好些。

六、小結

  1. 定義形式上:

a. 類方法和靜態方法都是通過裝飾器實現的,例項方法不是;

b. 例項方法需要傳入self引數,類方法需要傳入cls引數,而靜態方法不需要傳self或者cls引數。

2. 呼叫方式上:

例項方法只能通過例項物件呼叫;類方法和靜態方法可以通過類物件或者例項物件呼叫,如果是使用例項物件呼叫的類方法或靜態方法,最終都會轉而通過類物件呼叫。

3. 應用場景:

a. 例項方法使用最多,可以直接處理例項物件的邏輯;類方法不需要建立例項物件,直接處理類物件的邏輯;靜態方法將與類物件相關的某些邏輯抽離出來,不僅可以用於測試,還能便於程式碼後期維護。

b. 例項方法和類方法,能夠改變例項物件或類物件的狀態,而靜態方法不能。

參考資料:

Python's Instance, Class, and Static Methods Demystified – Real Python​realpython.com/instance-class-and-static-methods-demystified/ 2. Built-in Functions​docs.python.org/3/library/functions.html 技術連結