1-Python - 面向对象之装饰器函数

before

我们本小节聊聊类中常用的三个装饰器。

@property

接下来,让我们通过一个测试BMI示例来学习新的内容。

class People:  
    def __init__(self, name, height, weight):  
        self.name = name  
        self.__height = height  
        self.__weight = weight  
  
    def bmi(self):  
        return self.__weight / self.__height ** 2  
xiao5 = People('xiao5', 1.6, 90)  
print(xiao5.bmi())  # 35.15624999999999  

上例中,我们在实例化对象的时候,传递的三个参数,分别是名字、身高、体重。其中,身高和体重设为私有属性。然后通过bmi方法返回的BMI指数。一切看起来相当完美!

让我们思考一下,方法一般用来描述实例的某些行为,比如人吃饭,喝水,走路这些可以定义为方法,而BMI不能算是动词,在类中定义为属性比较好。既然如此。

class People:  
    def __init__(self, name, height, weight):  
        self.name = name  
        self.__height = height  
        self.__weight = weight  
        self.bmi = self.__weight / self.__height ** 2  
  
xiao5 = People('xiao5', 1.6, 90)  
print(xiao5.bmi)  # 35.15624999999999  

正如上例所示,你可能想在初始化的时候,就把BMI值拿到就好了呀,但是我们不应该在初始化的时候,做太多的事情,避免冗余。

那么我们如何处理bmi方法呢?这里Python已经帮我们做好了,那就是采用property装饰器。

class People:  
    def __init__(self, name, height, weight):  
        self.name = name  
        self.__height = height  
        self.__weight = weight  
    @property  
    def bmi(self):  
        return self.__weight / self.__height ** 2  
xiao5 = People('xiao5', 1.6, 90)  
print(xiao5.bmi)  # 35.15624999999999  

上例中,我们为bmi方法(第7行)上面加上“@property”装饰器,property装饰器的作用就是将方法伪装成属性。这样就可通过调用属性的形式去访问方法。当一个方法被伪装成属性后,我们俗称为这个方法为特性。当对象在调用该特性的时候,根本无法察觉自己想要的结果其实是被一个方法计算出来的。这种特性的使用方式遵循了统一访问的原则。

为什么要有property?如果你学过别的语言,如C++,你可能对知道在C++里一般会将所有的属性都设为私有的,然后提供set和get方法(接口)去设置和获取该属性。

一般的,面向对象的封装有三种方式:

  • public:这种其实就是不封装,是对外公开的 。

  • protected:这种封装方式对外不公开,但对朋友(friend)或者子类公开 。

  • private:这种封装对谁都不公开。

Python并没有将上述三种方式,在语法上内嵌到class中,而是选择通过property来实现。

class People:  
    def __init__(self, name, age):  
        self.name = name  
        self.__age = age  
    @property  
    def age(self):  
        return self.__age  
xiao5 = People('xiao5', 18)  
print(xiao5.age)   # 18  

上例中,当在第8行实例化一个对象后,在第9行打印该对象的年龄并且成功打印出来了。但是,我们分析一下调用过程,通过第4行看到,age属性被定义为私有属性。而第5行property将age方法伪装成了属性,那么当在第9行调用age的时候,其实是调用了第6行的age方法,执行内部代码,将私有属性返回。

那么,你可能会问,要不要这么复杂?定义成和name属性一样不就行了吗?恰恰相反,这么做是为了保护数据,因为:

class People:  
    def __init__(self, name, age, nickname):  
        self.name = name  
        self.nickname = nickname  
        self.__age = age  
    @property  
    def age(self):  
        return self.__age  
xiao5 = People('xiao5', 18, '55开')  
print(xiao5.age)        	# 18  
print(xiao5.nickname)   	# 55开  
xiao5.nickname = '66开'  
print(xiao5.nickname)   	# 66开  
xiao5.age = 19  			# AttributeError: can't set attribute  

上例如第10~13行所示,我们可以很任意的修改对象的昵称。但是,当我们试图去修改年龄时(第14行),却提示没有这个属性。此时,我们知道age是用property将方法伪装成属性的。那么我们当我们有需要修改如age这种特殊的带有特性的"属性"时该如何办呢?

class People:  
    def __init__(self, name, age, nickname):  
        self.name = name  
        self.nickname = nickname  
        self.__age = age  
    @property  
    def age(self):  
        return self.__age  
    @age.setter  
    def age(self, new_age):  
        self.__age = new_age  
  
xiao5 = People('xiao5', 18, '55开')  
print(xiao5.age)        # 18  
xiao5.age = 20  
print(xiao5.age)        # 20  

上例中,property在装饰方法后,当需要修改该特性时,就需要重新定义一个与该特性(第7行)的同名的方法(第10行),然后如第9行所示,为该特性加装一个@age.setter装饰器(age必须于property装饰的方法名一致,理解成为age特性赋予修改权限)。然后在setter装饰器下的方法中传递一个参数,并将该参数赋值给实际的私有age属性(第11行)。经过这么设置之后,我们第15行就可以为property装饰后的特性修改内容了。第15行这种赋值方式跟普通的属性赋值一样,其实Python这么做也是为了保持与普通属性赋值一致。在赋值过程中,第15行的等号右边的20会自动传递给被第9行@age.setter装饰的age方法的new_age参数。

这里你可能又要说,还是觉得好复杂、好麻烦!其实,真的有必要,正如上上个例子中为对象的昵称属性赋值一样,我们可以为昵称属性赋值任何内容。但是我们能为age赋值一个字符串或者列表吗?不符合常理呀,但如果age身为普通的属性,Python并没有为此有什么限制的动作,全靠我们在开发中注意。但这里使用property就可以帮助我们规范这种“非法”赋值的操作了。我们可以在试图修改特性时加入一些限制。

class People:  
    def __init__(self, name, age, nickname):  
        self.name = name  
        self.nickname = nickname  
        self.__age = age  
    @property  
    def age(self):  
        return self.__age  
    @age.setter  
    def age(self, new_age):  
        if isinstance(new_age, int):  
            self.__age = new_age  
        else:  
            print('该属性必须为数值类型')  
  
xiao5 = People('xiao5', 18, '55开')  
print(xiao5.age)        # 18  
xiao5.age = '20'  
print(xiao5.age)        # 20  

如上例第11~14行所示。我们在为特性赋值的方法中加入一些限制。如果为age特性赋值的内容不是数值类型,那么就无法赋值成功。比如第18行为age特性要赋值一个字符串20,就无法通过限制条件。从而达到保护数据安全的目的。

那么,既然特性能修改,也能删除。我们来看一下具体的删除示例。

class People:  
    def __init__(self, name, age, nickname):  
        self.name = name  
        self.nickname = nickname  
        self.__age = age  
    @property  
    def age(self):  
        return self.__age  
xiao5 = People('xiao5', 18, '55开')  
print(xiao5.__dict__)  # {'_People__age': 18, 'name': 'xiao5', 'nickname': '55开'}  
del xiao5.nickname  
print(xiao5.__dict__)  # {'_People__age': 18, 'name': 'xiao5'}  
del xiao5.age   # AttributeError: can't delete attribute  

如上例第11行所示,我们能删除一个对象的属性(结果如第12行打印结果所示)。但是,这种方式无法删除一个特性(第13行)。那如何才能删除特性呢?

class People:  
    def __init__(self, name, age, nickname):  
        self.name = name  
        self.nickname = nickname  
        self.__age = age  
    @property  
    def age(self):  
        return self.__age  
    @age.deleter  
    def age(self):  
        del self.__age  
xiao5 = People('xiao5', 18, '55开')  
print(xiao5.__dict__)  # {'_People__age': 18, 'name': 'xiao5', 'nickname': '55开'}  
del xiao5.nickname  
print(xiao5.__dict__)  # {'_People__age': 18, 'name': 'xiao5'}  
del xiao5.age     
print(xiao5.__dict__)  # {'name': 'xiao5'}  

如上例第9~11行所示,我们采用跟@age.setter同样的方式来删除该特性,就是为该特性加装一个@age.deleter装饰器,当第16行在删除该特性的时候,第10行的方法就会自动执行,在该方法内部,我们删除真正的私有属性age。这样就删掉了一个特性。结果如第17行打印所示。

通过property装饰器,就可以将方法伪装成属性,并通过为该特性加装setter和deleter装饰器的方式,来修改或者删除该特性。再次强调,setter和deleter装饰器使用的前提是property。没有property装饰器,setter和deleter也就无从谈起了。

@classmethod

为什么要用@classmethod?

上一小节开头的bmi指数超标了,那么就要有应对措施将bmi指数回归正常,我们可以多做运动、去超市买点水果吃等措施。那么,既然逛超市,你除了能看到琳琅满目的商品之外,就是各种打折信息了。我们来分析一下打折背后的逻辑。

class Goods:  
    __discount = 0.8  
    def __init__(self, name, price):  
        self.name = name  
        self.__price = price  
    @property  
    def price(self):  
        return self.__price * Goods.__discount  
    def change_discount(self, new_discount):  
        Goods.__discount = new_discount  
apple = Goods('apple', 4)  
print(apple.price)  # 3.2  
apple.change_discount(1)  
print(apple.price)  # 4  

上例中,通过property将商品的价格伪装成属性,并且打八折出售(第6~8行)。并且在第8行又通过change_discount方法修改打折力度。在第11行实例化一个苹果对象后,通过第12行打印可以看到现在显示的是打8折之后的价格。在打折过后,有通过第13行取消打折。这一切看起来相当的美好!

但让我们思考一下,要修改全场的商品的打折力度,为什么要用一个苹果对象来操作呢?那你可能说直接通过类来操作,但我们又不能直接操作私有属性(第2行)。那怎么办呢?

如何用@classmethod?

class Goods:  
    __discount = 0.8  
    def __init__(self, name, price):  
        self.name = name  
        self.__price = price  
    @property  
    def price(self):  
        return self.__price * Goods.__discount  
    @classmethod  
    def change_discount(cls, new_discount):  
        cls.__discount = new_discount  
apple = Goods('apple', 4)  
print(apple.price)  # 3.2  
Goods.change_discount(1)  
print(apple.price)  # 4  

如上例中,Python提供了@classmethod装饰器,被该装饰器装饰的方法就成为类方法,无需通过对象来调用了。被装饰器的方法中(第10行)默认接收一个约定俗称的参数名称cls(class的缩写),如self参数一样。在第14行通过类名调用该方法时,类名Goods自动被cls参数接收,我们只需传递new_discount参数即可。而其apple对象的价格已成功恢复原价。

class Goods:  
    __discount = 0.8  
    def __init__(self, name, price):  
        self.name = name  
        self.__price = price  
    @property  
    def price(self):  
        return self.__price * Goods.__discount  
    @classmethod  
    def change_discount(cls, new_discount):  
        cls.__discount = new_discount  
apple = Goods('apple', 4)  
print(apple.price)  # 3.2  
apple.change_discount(0.4)  
print(apple.price)  # 1.6  

如上例所示,类方法除了能被类调用之外,也能被对象调用。

学到这里,我们可以总结下,类方法是被@classmethod装饰的特殊的方法,普通的方法被装饰后,该方法默认接收一个类名传递给cls参数,该特殊方法可以被类和对象同时调用。

@staticmethod

这里再介绍一个跟classmethod类似的装饰器,那就是@staticmethod装饰器,当在开发中,我们会遇到一些跟类有关系的功能却又不需要实例和类参与的情况,就需要用到staticmethod这个静态方法了。

class Student:  
    def __init__(self, name, pwd):  
        self.name = name  
        self.pwd = pwd  
    @staticmethod  
    def login():  
        user = input('user: ').strip()  # 输入 wang  
        pwd = input('pwd: ').strip()    # 输入 666  
        if user == 'wang' and pwd == '666':  
           obj = Student(user, pwd)  
           return obj  
obj = Student.login()  
print(obj.name, obj.pwd)    # wang 666  

上例中,Student类是跟学生相关的类,而login有跟学生有关系却又相对独立的函数。只有当学生在登录之后,才能操作跟这个类相关的操作。而上例第6~11行,学生在登录成功,就在类中实例化一个对象,这样,学生就能操作类了。

那你可能会问了,为什么不单独写成函数?因为这个login只能被学生登录,而不能被别的角色登录。

@staticmethod将一个普通的方法装饰成特殊的静态方法,其他的如函数别无二致,只是表明了该静态方法属于某个类,提高了代码的易读性。而且这个静态方法无需传递如self或者cls参数。

至此,我们了解了三个能将普通的方法的装饰器。我们来稍作总结:

  • 对象方法:最常用的方法,通过对象调用方法,默认接收一个self参数。

  • 类方法:将一个普通的方法装饰成类方法,类和对象都可以调用,默认接收一个cls参数。

  • 静态方法:将一个普通方法装饰成特殊的函数,默认可以不传参。类和对象都可以调用。

至此,面向对象基础部分讨论完毕。

posted @ 2022-02-21 17:16  听雨危楼  阅读(199)  评论(0编辑  收藏  举报