当前位置:首页|资讯|编程|ChatGPT

从一个示例聊描述符

作者:刘涉水是二次元发布时间:2023-09-22

   前言

    假设,我们现在有一个非常简单的需求:我们需要创建一个类叫 Circle, 它就是一个圆。我们不关心它的坐标的情况下,它就只有一个关键的初始化属性:半径 r。它可以计算周长也可以计算面积(这里我们就只讨论面积 area)。请根据业务需求完成代码。

    非常简单。把以上代码复制给 Chatgpt,可以得到“标准答案”:

    本文到此结束。

    以上代码有什么问题?

    我提出以下异议:圆的面积,到底是圆本身的属性,还是圆的一种方法?

    这个答案因人而异。我们先走第一条路:它是圆的属性。当然它并不是客观意义上是圆的属性,这里的例子很简单是一个圆,假设我们创建的类是一个数据库表的模型,它的查询、写入语句也是这个模型的属性吗?可能得是方法。反之,圆的面积可能也不一定能百分之百说是圆的属性:如果圆的面积是属性,那么圆的内接十七边形是不是也是圆的属性?当然,面积很简单也很常用,我们确实可以把它当成属性。

    我们就当成属性来做,所以我们不希望在获取面积时使用 circle.area()。于是乎有以下多种解决方法,我们先说第一种,做成特性。

    以上代码有什么问题?

    问题在于,我们每次获取这个特性的时候,都进行了一次计算。假设这个计算很复杂,我们重复的调用它,就会增加无意义的计算量。于是我们稍作修改,增加一个缓存功能。

    缓存也有很多种实现。

    这是最原始的方法,通过一个“安全”的保护属性来缓存面积。

    以上代码有什么问题?当你需要的属性变多的时候,你需要在 __init__ 中增加对应的保护属性,并创建与之对应的 @property。一是不方便,二是可能会使得 __init__ 变得冗长。

    当然这不是最冗长的,或许你会这么写:

    到这里我才第一次使用我的 IDE,之前都是口述给 Chatgpt 写的。这样直接把相关属性在实例化的过程中计算了,或许我一开始写代码的时候也会这么写,把所有要计算的东西都丢进一个方法里。总之,我不推荐这么做,因为随着要用的属性增多,可能你的 __init__ 会无限变长,或许你需要自己写一个 def init(self): 来囊括这些过程,而且你最好保证它们都会用到,不然就做了无意义的计算。

    另一种缓存方式是这样的:

    稍稍跑跑测试你就会发现,它确实实现了缓存功能。@property 在上保证“只读”这个功能的强制描述符起作用,@cache 本质是在装饰器内部创建一个用于缓存的命名空间,然后控制是否从命名空间读取结果还是运行被装饰函数。我需要强调的是 @cache 本质是装饰器而不是描述符,你可以点开它的源码,它本质是古早的 lru_cache,而 lru_cache 就是一个闭包

    在最新的版本中你可以使用 @cache_property,实现大致一样的功能,但你必须要注意,它们的实现逻辑完全不一样,我贴上 @cache_property 的源码:

    它是一个描述符没问题吧,如果你不知道什么是描述符,可以看看我之前的文章。它只实现了 __get__,说明它是一个非覆盖型描述符。它的核心功能就是,因为我们知道 area 这个方法本质存在的位置是类的命名空间(因为方法也是描述符),当实例的命名空间没有同名的属性的时候,它就给实例创建一个,创建之后实例属性就会覆盖方法(因为这个方法被费覆盖型描述符的装饰器装饰了,变成了一个非覆盖型描述符)。它的行为和 @cache 完全不一样!因为它会直接修改你实例的 __dict__,不信的话我们可以在 @cache_property 和 @cache 的例子下跑以下代码:

    你会发现在使用 @cache_property 的情况下,实例的 __dict__ 中多了一个叫 area 的属性,而 @cache 则不会。所以实际上 @cache_propety 出问题的几率是比 @cache 要多的。当然,简单的功能上基本上不会有什么差错。我这里会讲 @cache_property,就是因为它用到的是描述符,我们着重要讲的也是描述符。我用以下代码来代替 @cache_property 的功能:

    效果几乎一样。

    当然,使用缓存是我推荐的写法,算是这个示例我比较喜欢的解决方法。

    现在我们讨论,如果我们觉得:面积不是圆的属性,该怎么写?

    它带来的问题是,我们需要在外部创建一个变量来保存 area,圆这个类以及其实例不会帮你保存。当我们需要在很多不同的地方计算 area 的时候,它不仅需要我们开辟额外的命名空间,也无法避免重复计算。

    那么我们反过来给 area 再 set 回去可不可以呢?

    这样的代码是很矛盾的,在常规的 IDE 中,self.area 赋值的那一行就会有提示,你正在为类创建一个在 __init__ 中没有创建的属性,这个属性只有调用了 get_area 才存在,我不知道是否该给你提示这个类有这个属性,因为如果我告诉你有,但是你没有调用 get_area,那么你会得到一个 AttributeError,等于实际上没有。如果我告诉你没有,但它实际上又可能有。

    而且最大的问题是,当我需要获取一个圆的面积的时候,我到底是调用 circle.area 还是 circle.get_area 呢?这让人疑惑。

    当然,以上函数一样可以使用缓存。我们先讲使用缓存的情况:

    你会发现,缓存同样是存在问题的,这个问题在哪怕把面积当成圆的属性的时候还是会出现:当我们用非法手段修改一个圆的初始化参数的时候,我们会得到错误的缓存:因为无论是 @cache_property 还是 @cache,由于 get_area 本身没有额外传参,所以它们记录的缓存只有唯一一个识别符: get_area 这个函数名称。所以这到底是谁的错?是缓存的错,还是我们的错?我们是不是应该强制规定无法修改 self.r?

    所以实际上更加规范的写法会对 r 做限定要求,比如这样:

    实际上你也可以自己为 r 创建一个描述符,而不用只读特性 @property,比如这样:

    当然,这么做没什么必要,这就是一个盗版的 @property,实际上它的具体实现更像是我之前写的盗版 @cache_property。它们都一定程度上防止了 r 属性的写入,但是防君子不防小人,你永远可以通过手动修改 __dict__ 对这些属性动手脚。

    我们回到 get_area 上,如果 get_area 是获取 area 的唯一途径,那么我们就不需要 area 属性,这会让我们混淆:我到底是通过什么获取 area,是一个方法还是一个属性?但当我们使用 set_area 的时候,获取情况就会发生变化:我们把获取 area 的唯一方法规定为调用 circle.area,但在获取前我们需要 set_area。

    之前讨论过,我们在给属性赋值的时候,这个属性最好是在 __init__ 中存在,所以 self.area = 0 不可或缺。但是这样带来了问题,当我们忘了调用 set_area 的时候,直接读取 circle.area 会返回一个错误的 0 或者一个让人疑惑的 None。这是我们不希望的。

    所以我们给 area 做一个非覆盖型描述符,当实例没有 area 属性的时候,它提示我们需要先 set 它:

    这是一种我比较喜欢的实现方式,它像极了其他语言里先声明一个变量的过程,或许很多人会觉得这样写就一点都不 pythonic 了,但是它确实会更规范。

    当然还有使用 __slots__ 来解决的办法:

    __slots__ 确实可以解决属性提示的问题,但你一定要知道,__slots__ 也会带来诸多的不便,__slots__ 的终极意义是节省内存,所以当你没用到节省内存这一功能的时候,不要轻易使用 __slots__。

    如果面积既不是圆的属性也不是圆的计算方法,而是在圆基础上的一种加工呢?这是一个很好的想法:确实,如果你单纯把圆当做一个数据类,它只存储基础数据,那么其他的计算指标确实是在之上的加工!我们常常难以判断什么是事物的属性,什么是事物的行为,什么是对事物的加工。我提出以下终极问题:

  • 圆的半径是圆的属性吗?

  • 圆的面积到底是圆的属性?还是圆的一种行为?还是对圆的加工?

  • 圆的内接三百一十四边形,是圆的一种属性?还是圆的一种行为?还是对圆的加工?

    没有标准答案,但我认为,假设一块木头被雕刻成了雕塑,那么这一定不是木头的属性,也不是木头的行为,而是人的行为,是人对木头的加工。面积这个东西处在一个很灰色的地带,它都可以!

    假设我们把圆的面积当做对圆的加工,那我们的代码就变成了这样:

    当然这里要讨论的核心问题是,接受的这个 circle obj,是放在 __init__ 中,还是放在 get_area 中,甚至让 get_area 成为一个 classmethod。其实最大的区别是在于:你是要对一个对象做多个操作,还是对多个类似的对象做同一个操作。如果是前者,那么就像我这样写代码就可以。这很像业务的代码,它除了解耦并没有什么特别的优势。它更易拓展,你可以给 Circle 写一个抽象基类叫 Shape,给 CircleProcessor 写一个抽象基类叫 IProcessor。

结语

    我非常欢迎你们把自己对这个示例的实现发给我或者发到评论区。

    今天的小题大做到此结束。


Copyright © 2024 aigcdaily.cn  北京智识时代科技有限公司  版权所有  京ICP备2023006237号-1