Python学习笔记——属性管理工厂

前言

在从鸭子类型看Python面向对象中,我们通过鸭子类型这个概念讨论了Python多态、接口和范型的实现,感受了在没有对接口进行支持,同时不咋抽象和继承的Python架构风格下,通过协议约定的方式,实现了Python风格的面向对象。
当然,除了面向对象最重要的几个特征外,今天我们将讨论剩余的独具Python风格的面向对象实现——对象属性描述符。第一部分将讨论Python对象的引用和垃圾回收的问题,同时讨论一些常见的性能优化方案。第二部分将讨论Python对属性绑定和修改的实现方式,该方式一如既往的解决了样板代码和性能方面的问题,成为了属性绑定问题中近乎完美的解决方案。

对象引用问题

说到引用,首要考虑的问题即是:在函数传值时,到底进行的是类似Java的值传递,还是能像C一样,可以实现引用传递。这个问题的结论是,Python中也只能进行值传递,也就是说,对于非引用型变量(整形变量,字符串等),在函数中是无法修改的,只能对引用型变量进行修改(例如 列表、字典、对象等)。如下例所示:

>>> def f(a,b):
...     a += b
...     return a
... 
>>> x = 1
>>> y = 2
>>> f(x,y)
3
>>> x,y
(1, 2)

示例注解:本示例很清楚的演示了,在Python函数中,形参到实参的传递是一种值传递,无法对非引用型参数进行值修改,在C中,可以直接通过传递非引用型变量的地址引用,从而实现任何类型的参数的值修改。
本示例中的第二个有趣的地方就是a += b这种编写方式了,咱们自然而然会联想到与之呼应的另一种方式a = a + b,那么这两种方式到底有无差别呢,答案是当然。
这里也涉及到有关引用的经典问题。对于前者,是一种就地运算的方式,所谓“就地”是指运算的结果不会创建新的变量进行存储,而直接赋值到原变量中,而后者会将运算结果保存到新的变量中,再将新变量的引用赋值。上述讨论仅针对可变类型,在Python中,任何不可变类型的运算操作,都会产生新的值,并进行引用的更换。可通过以下示例进行理解:

>>> a=[1,2]
>>> b=[3,4]
>>> id(a)
4446285448
>>> a+=b
>>> a
[1, 2, 3, 4]
>>> id(a)
4446285448

>>> a=[1,2]
>>> b=[3,4]
>>> id(a)
4446285320
>>> a = a + b
>>> a
[1, 2, 3, 4]
>>> id(a)
4446589256

深度复制和浅复制

这个话题在任何面向对象语言中都是必不可少的话题,Python这方面和Java完全相似,在做对象克隆时,需要考虑对象属性中的引用对象(例如数组、列表、和字典等)。

对象垃圾回收

Python的垃圾回收显然没有Java那么精彩,依然沿用了Python精简的风格。主要采用了引用计数和分区回收的方式。
对于最普通的变量引用,即通过计数器法,对对象的引用进行计数,当计数器为0时进行析构和回收。另一方面,对于相互引用的对象,则采用分区回收的方式,判断对象不再有引用关系时及时进行回收。

对象有关的其他问题

当然,除了以上简单讨论的话题外,弱引用、强引用也是Python对象中一个重要的话题,但是这一方面和其他语言没有太大区别,因此不作为本文的重点再进行描述。

属性描述符

接下来,将隆重的介绍Python中第三个卓越并优秀的面向对象特性:属性描述符。
乍一听,似乎是一种全新的概念,但说到Java为类设计属性设计的getset方法,大家就耳熟能详了。在Java中,为了能安全的管理对象的属性,我们常常不允许用户直接通过.的方式读取和修改对象属性,我们通常将需要受到保护的属性用private关键字进行修饰,同时为其提供getset方法进行读取和管理。
Java这种类属性保护机制,一方面得益于其良好的权限设计体系,无论是类、方法还是属性,都有着4种(publicdefaultprotectedprivate)系统关键字进行支持保护,一旦被这4种关键字进行修饰,在编译期即可进行部分权限校验,从而让受保护对象不被暴露;另一方面,通过良好的面向对象的设计方式,为特定的需求提供访问和修改的接口。
但是,当你采访任何一个有经验的Java工程师的时候,大家都会抱怨每一次需要为对象提供属性访问或修改接口时,都要去实现大量样板代码,这种代码的编写方式实在太古老、太不优雅了,随着Spring等优秀的IOC框架的诞生,也为Java工程师提供了一种漂亮的解决方案即:将对象整个生命周期和属性管理等“麻烦事”都委托给容器,从而让工程师能去关注更多与业务逻辑有关的部分了。
说到这,大家可以大致的了解到,在Java SE中,属性的管理是依托了大量样板代码实现的,而对于样板代码还常常只能束手无策。同时很多语言狂热者都在批评者Java那种属性的访问和修改方案,认为通过.的原始访问、修改方式才是一种自然而然的形式。那么,接下来就该隆重的介绍Python的属性描述符了,说了那么多,大家也应该猜得到,Python的实现方案一方面不再有大量样板代码,另一方面同样也保持了.的原始方式。
首先,为属性描述符下一个基本的理解定义,通过字面意思来理解,属性描述符就是一种对于属性的描述方式,任何实现了这种描述协议的对象,都能够将属性的管理托管给具体的描述符,也就是说,当你实现了属性描述符以后,不需要定义任何getset方法的情况下,实现对属性的访问、修改和删除。

一个简单示例

class Quantity:  

    def __init__(self, storage_name):
        self.storage_name = storage_name  

    def __set__(self, instance, value):  
        if value > 0:
            instance.__dict__[self.storage_name] = value  
        else:
            raise ValueError('value must be > 0')


class LineItem:
    weight = Quantity('weight')  
    price = Quantity('price')  

    def __init__(self, description, weight, price): 
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

示例注解:上例中主要由两个类组成Quantity和LineItem,很简单的可以了解到Quantity即是我们所指的描述符了,任何创建了Quantity类实例的属性,都能将其setget方法托管给Quantity类,从而获得一种近乎完美的属性管理方式。
当然,对于Python来说,没有自身的权限体系的底层支持,不管怎么样,也是无法严格意义上保护属性不被修改的。也因此,属性描述符作为Python相对独有的属性管理方式,成为了一种让人眼前一亮的完美解决方案。
如果用过Django的朋友可能会眼前一亮,这不就是Django中model对于属性定义的方式么,那么接下来,通过定义继承类的方式,我们可以实现Django model中对于属性过滤条件的支持。

import abc

class AutoStorage:  
    __counter = 0

    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)

    def __set__(self, instance, value):
        setattr(instance, self.storage_name, value)  

class Validated(abc.ABC, AutoStorage):  

    def __set__(self, instance, value):
        value = self.validate(instance, value)  
        super().__set__(instance, value)  

    @abc.abstractmethod
    def validate(self, instance, value):  
        """return validated value or raise ValueError"""


class Quantity(Validated):  
    """a number greater than zero"""

    def validate(self, instance, value):
        if value <= 0:
            raise ValueError('value must be > 0')
        return value

class NonBlank(Validated):
    """a string with at least one non-space character"""
    
    def validate(self, instance, value):
        value = value.strip()
        if len(value) == 0:
            raise ValueError('value cannot be empty or blank')
        return value  

示例注解:在上例中,由AutoStorage类实现属性的管理,由Validated及其子类实现过滤条件,从而实现了对属性条件的管理。
除了展示的属性描述符的基础用法外,还有对覆盖型和非覆盖型的讨论,更多细节可参考《流畅的Python》以及《Python Cookboob》。

属性描述符所带来的思考

到此,相信任何一个编译型语言的疯狂推崇者都会为Python这种令人兴奋的特性所震撼。可能有人会说,为什么说属性描述符是一种特性呢,这不就像是一种设计模式么,在其他面向对象语言中难道不能实现吗?
我的理解是,当然可以实现,但是其他语言中实现的属性描述符会严重的水土不服。就说Java吧,由于Java本身提供的权限体系,就致使Java不可能将受保护的属性让一个外部的类随意访问和修改,在Java中,正确的做法确实只能在类的内部来实现操作并提供保护。那么对于Python而言,首先没有语言底层的权限体系支持,其次,无论怎么做,都难以避免对象属性或类属性被修改的厄运,那么只能靠协议来施加一定的约束了。在这样的背景下,也为Python带来了无穷的灵活性,以至于可以将模版代码降低到最少,从而提供一种近乎完美的实现方案。
哈哈,这就是一种设计哲学呀,要么像Java一样提供一套完整、严谨的权限体系,并由底层关键字做支持,能在编译期就进行权限检查,从而保证最小的出错可能。要么,就类似Python一样,拒绝样板代码,将精简和灵活的设计理念做到极致。

总结

关于Pythonic的面向对象风格至此,就基本全部讨论完毕了,作为一个长期从事Java方面学习和工作的博主来说,第一次接触到Python这种底层设计逻辑的时候,不禁为之赞叹,这是一种多么卓越和完美的实现呀,他们将Java备受诟病的缺点都一一解决了。Python真的将样板代码的问题很大程度的解决了,以一种绝对充满智慧的方式。
当然,作为旧爱,Java的那些架构上的诟病,不能简单的和Python进行比较,毕竟是两个阵营中的语言,底层的设计逻辑就是截然不同的。没有谁好谁坏,孰是孰非。
同样,希望有关Python属性描述符的分享,能为您带来一种全新的视觉,您也能像我一样,为这门“年轻的”语言发自内心的赞叹。

你可能感兴趣的:(Python学习笔记)