建议35:分清 staticmethod 和 classmethod 的适用场景

:分清 staticmethod 和 classmethod 的适用场景

类方法:

如果我们想写一些仅仅与类交互而不是和实例交互的方法,我们可以在类外面写一个简单的方法来做这些,如下:

def get_no_of_instances(cls_obj):
    return cls_obj.no_inst
class Kls(object):
    no_inst = 0
    def __init__(self):
        Kls.no_inst = Kls.no_inst + 1
ik1 = Kls()
ik2 = Kls()
print(get_no_of_instances(Kls))
# OUT: 2

但是这样做就扩散了类代码的关系到类定义的外面。这种场景,我们应该使用类方法:即只在类中运行而不在实例中运行的方法

调用类方法装饰器@classmethod,会隐式地传入该对象所对应的类,可以动态生成对应的类的类变量,同时如果我们期望根据不同的类型返回对应的类的实例,类方法才是正确的解决方案。

class Kls(object):
    no_inst = 0
    def __init__(self):
        Kls.no_inst = Kls.no_inst + 1
    @classmethod
    def get_no_of_instance(cls_obj):
        return cls_obj.no_inst
ik1 = Kls()
ik2 = Kls()
print ik1.get_no_of_instance()
print Kls.get_no_of_instance()
# OUT: 2, 2

 这样的好处是: 不管这个方式是从实例调用还是从类调用,它都用第一个参数把类传递过来。

静态方法:

当我们所定义的方法既不跟特定的实例相关也不跟特定的类相关,可以将其定义为静态方法,这样使我们的代码能够有效地组织起来,提高可维护性。

有一些跟类有关系的功能但在运行时又不需要实例和类参与的情况下,需要用到静态方法。

比如更改环境变量或者修改其他类的属性等能用到静态方法. 这种情况可以直接用函数解决, 但这样同样会扩散类内部的代码,造成维护困难.

IND = 'ON'
def checkind():
    return (IND == 'ON')
class Kls(object):
     def __init__(self,data):
        self.data = data
    def do_reset(self):
        if checkind():
            print('Reset done for:', self.data)
    def set_db(self):
        if checkind():
            self.db = 'new db connection'
            print('DB connection made for:',self.data)
ik1 = Kls(12)
ik1.do_reset()
ik1.set_db()
# OUT: Reset done for: 12
# OUT: DB connection made for: 12

如果使用@staticmethod就能把相关的代码放到类里面了

下面这个更全面的代码和图示来展示这两种方法的不同
@staticmethod 和 @classmethod的不同

class Kls(object):
    def __init__(self, data):
        self.data = data
    def printd(self):
        print(self.data)
    @staticmethod
    def smethod(*arg):
        print('Static:', arg)
    @classmethod
    def cmethod(*arg):
        print('Class:', arg)
>>> ik = Kls(23)
>>> ik.printd()
23
>>> ik.smethod()
Static: ()
>>> ik.cmethod()
Class: (<class '__main__.Kls'>,)
>>> Kls.printd()
TypeError: unbound method printd() must be called with Kls instance as first argument (got nothing instead)
>>> Kls.smethod()
Static: ()
>>> Kls.cmethod()
Class: (<class '__main__.Kls'>,)

尽管classmethodstaticmethod非常的相似,但是两者在具体的使用上还是有着细微的差别:

classmethod必须使用类对象作为第一个参数,而staticmethod则可以不传递任何参数。

示例,让我们假设有处理日期信息的类:

class Date(object):
    day = 0
    month = 0
    year = 0
    
    def __init__(self, day=0, month=0, year=0):
        self.day = day
        self.month = month
        self.year = year

这个类很显然可以被用来存储某些日期信息

这里定义了__init__,典型的类实例初始化方法,它作为典型的instancemethod接受参数,其中第一个传递的必要参数是新建的实例本身。

类方法

有一些可以通过使用classmethod很好解决的任务。

假设我们有很多('dd-mm-yyyy')格式字符串的日期信息,想要把它们创建成Date类实例。我们不得不在项目的不同地方做这些事情。

所以我们必须要做到:

  1. 分析得到的年月日字符串,把它们转化成三个整形变量或者拥有三个元素的元组的变量。
  2. 通过传递这些值实例化Date

得到:

day, month, year = map(int, string_date.split('-'))
date1 = Date(day, month, year)

C++拥有重载的特性可以达到这种目的,但是Python缺乏此类特性。所以,python使用classmethod的方式。让我们尝试一种另类的构造函数。

    @classmethod
    def from_string(cls, date_as_string):
        day, month, year = map(date_as_string.split('-'))
        date1 = cls(day, month, year)
        return date1

date2 = Date.from_string('11-09-2012')

进一步分析一下以上代码的执行,以及它的优势:

1.在一个地方解析日期字符串并且重复使用它。
2.做到很好的封装(相对于把执行字符串解析作为一个单独的函数在任何地方执行,这里使用的方法更符合OOP的范式)
3.cls表示类对象,而不是类实例。这样很酷,因为如果我们继承Date类,那么所有的子类也都将拥有from_string这个方法。

classmethod的典型使用场合:
1) 直接用类来调用函数,而不用借助类实例
2) 更优雅地实现某个类的实例的构造(类似于Factory Pattern)
通常情况下,类实例是解释器自动调用类的__init__()来构造的,但借助classmethod可以在解释器调用__init__前实现一些预处理逻辑,然后将预处理后的参数传入类的构造函数来创建类实例。如dict类型支持的fromkeys()方法就是用classmethod实现的,它用dict实例当前的keys构造出一个新的dict实例。在文档Descriptor HowTo Guide最后部分给出了它对应的Python pseudo-code,感兴趣的话可以去研究一下。
关于实现类实例构造的另一个典型case,可以参考StackOverflow上的这篇问答帖。帖中Best Answer作者给出了一个典型场景,这个场景用非classmethod的方法也可以实现类实例的构造,但借助classmethod语法,可以实现的更优雅(a. 与__init__构造实例相比,classmethod方法也保证了构造逻辑代码复用度而且实现的更精简,如解析date_as_string为(year, month, day)的代码也可以被复用;b. 它不用通过类的实例调用,直接用类来调用即可构造新的实例;c. 与定义实现相同功能的全局函数相比,更符合OOP思想;d. 基类被继承时,基类中定义的classmethod也会被继承到继承类中)。

静态方法

那么staticmethod又是什么呢?它和classmethod非常的相似,但是不强制要求传递参数(但是做的事与类方法或实例方法一样)。

让我们来看一个使用的例子。

我们有一个日期字符串需要以某种方式验证。这个任务与之前一样要定义在Date类内部,但是不要求实例化它。

静态方法在这种情况下就非常有用。看一下下面这个代码片段:

    @staticmethod
    def is_date_valid(date_as_string):
        day, month, year = map(int, date_as_string_split('-'))
        return day <= 31 and month <= 12 and year <= 3999
    
    is_date = Date.is_date_valid('11-09-2012')

现在正如我们了解到的staticmethod的使用,我们不需要访问它所属的类,它本质上就是一个函数,调用方式和调用函数一样,不同的是它不关注对象和对象内部属性。

与classmethod的装饰器语法糖类似,@staticmethod会自动调用staticmethod(f)。
Python中类静态方法的语义跟C++/Java类似,即类的静态成员属于类本身,不属于类的实例,它无法访问实例的属性(数据成员或成员函数)。定义为staticmethod的函数被调用时,解释器不会自动为其隐式传入类或类实例的参数,它的实际参数列表与调用时显式传入的参数列表保持一致。
staticmethod的典型应用场景:
若类的某个函数确认不会涉及到与类实例有关的操作时,可以考虑将该函数定义为类的staticmethod。比如,根据业务逻辑,可将全局函数封装到一个类中并声明为staticmethod,这样看起来更符合OOP思想,具体的例子可以参考这篇Blog 。当然,这只是一种符合OOP的封装思路,并非意味着碰到全局函数就一定要这样做,需要看个人习惯或业务需求。
再次强调:定义为staticmethod类型的函数,其函数体中最好不要涉及与类实例有关的操作(包括创建类实例或访问实例的属性),因为一旦涉及到类实例就意味着这些实例名是硬编码的,在类被继承的场景下,调用这些staticmethod类型的函数会创建基类或访问基类属性,而这通常不是业务预期的行为。具体的case可以参考StackOverflow这篇问答帖的第2个高票答案。

@property
根据Python文档的说明,property([fget[, fset[, fdel[, doc]]]])为new-style类创建并返回property对象,该对象是根据传入的参数(fget/fset/fdel)创建的,它可以决定外部调用者对new-style类的某些属性是否具有读/写/删除权限。以官网文档给出的demo为例:

[python] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. class C(object): ## NOTICE: property只对new style classes有效  
  2.     def __init__(self):  
  3.         self._x = None  
  4.   
  5.     def getx(self):  
  6.         return self._x  
  7.   
  8.     def setx(self, value):  
  9.         self._x = value  
  10.   
  11.     def delx(self):  
  12.         del self._x  
  13.   
  14.     x = property(getx, setx, delx, "I'm the 'x' property.")  

上述示例中,x是类C的property对象,由于创建时传入了3个函数对象,故通过访问该属性可以实现对self._x的读/写/删除操作。具体而言,调用C().x时,解释器最终会调用getx;调用C().x = value时,解释器后最终调用setx;调用del C().x时,解释器会最终调用delx。
由于property()的第1个参数是fget,利用这一点,可以很容易实现一个只有read-only权限的类属性:

[python] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. >>> class C(object):  
  2. ...     def __init__(self):  
  3. ...         self._name = 'name'  
  4. ...     @property  
  5. ...     def get_name(self):  
  6. ...         return self._name  
  7. ...       
  8. ...   
  9. >>> c = C()  
  10. >>> c.get_name  
  11. 'name'  
  12. >>> c.get_name = 'new name'  
  13. Traceback (most recent call last):  
  14.   File "<stdin>", line 1in <module>  
  15. AttributeError: can't set attribute  

 

当然,这里所说的"read-only",只是指这个属性名不会被赋值操作重新绑定新对象而已。如果这个属性名初始绑定的是个可变对象(如list或dict),则即使通过@property装饰,其绑定的对象的内容也可以通过属性名来修改。
如果想通过类的实例对象来修改或删除类实例的属性,则需用下面的代码来实现:

 

[python] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. >>> class C(object):  
  2. ...     def __init__(self):  
  3. ...         self._name = 'name'  
  4. ...     @property  
  5. ...     def name(self):  
  6. ...         return self._name  
  7. ...     @name.setter  
  8. ...     def name(self, value):  
  9. ...         self._name = value  
  10. ...     @name.deleter  
  11. ...     def name(self):  
  12. ...         del self._name  
  13. ...   
  14. >>> c = C()  
  15. >>> c.name  
  16. 'name'  
  17. >>> c.__dict__  
  18. {'_name''name'}  
  19. >>> c.name = 'new name'  
  20. >>> c.__dict__  
  21. {'_name''new name'}  
  22. >>> c.name  
  23. 'new name'  
  24. >>> del c.name  
  25. >>> c.__dict__  
  26. {}  

上述代码中,@property、@name.setter及@name.deleter均是装饰器语法糖,其中:@name.setter中的name指代的是经@property装饰后的对象(即property(name)返回的名为name但类型为property object的对象),setter是这个名为name的property对象的built-in函数,其目的是通过name.setter(name)为这个property对象提供修改其所属类的属性的功能。@name.deleter同理。
至于property对象支持的函数setter()和deleter()的来历,文档Descriptor HowTo Guide在介绍property原理时给出了property类底层实现的Python伪码,值得精读。
从伪码还可以看到,property类实现了__get__()、__set__()和__delete__()方法,这意味着property类是个遵循descriptor protocol的data descriptor,根据文档Descriptor HowTo Guide关于Invoking Descriptors的说明,data descriptor会影响解释器对属性名的查找链,具体而言,当上面的代码中调用c.name时,解释器会将其转化成type(c).__dict__['name'].__get__(c, type(c))(备注:通过print type(c).__dict__可以验证,name确实存在于dict中),故这个转化后的调用链会调用到property对象的__get__()方法,而根据property的实现伪码,在__get__()中最终会调用到类C对应的name()函数。
上面这段话所描述的流程正是property魔法背后的原理。
当然,要想真正理解还需要仔细研究property的python伪码逻辑。

@property除可以实现属性的只读权限功能外,还可以用在这种场景下:
类属性已经暴露给外部调用者,但由于业务需求,需要针对这个属性进行业务逻辑的修改(如增加边界判定或修改属性计算方法,等等),则此时引入property()或@property语法糖可以在修改逻辑的同时保证代码的后向兼容,外部调用者无需修改调用方式。具体的case可以参考这篇Blog中提到的场景。

posted on 2014-03-22 16:33  myworldworld  阅读(1829)  评论(0)    收藏  举报

导航