2023年4月

前言

最近项目上要求升级一个工具包
hutool
的版本,以解决安全漏洞问题,这不升级还好,一升级反而捅出了更大的篓子,究竟是怎么回事呢?

事件回顾

我们项目原先使用的
hutool
版本是5.7.2,在代码中,我们的数据传输对象DTO和数据实体对象中大量使用了工具包中的
BeanUtil.copyProperties()
, 大体代码如下:

  1. 数据传输对象
@Data
@ToString
public class DiagramDTO {

    // 前端生产的字符串id
    private String id;

    private String code;

    private String name;
}
  1. 数据实体对象
@Data
@ToString
public class Diagram {

    private Integer id;

    private String code;

    private String name;
}
  1. 业务逻辑
public class BeanCopyTest {

    public static void main(String[] args) {
        // 前端传输的对象
        DiagramDTO diagramDTO = new DiagramDTO();
        // 如果前端传入的id事包含e的,升级后就会报错
        diagramDTO.setId("3em3dgqsgmn0");
        diagramDTO.setCode("d1");
        diagramDTO.setName("图表");

        Diagram diagram = new Diagram();
        // 关键点,数据拷贝
        BeanUtil.copyProperties(diagramDTO, diagram);
        System.out.println("数据实体对象:" + diagram);
        //设置id为空,自增
        diagram.setId(null);
        //保存到数据库中 TODO
        //diagramMapper.save(diagram);
    }
}

升级前,
hutool
是5.7.2版本下,执行结果如下图。

  • BeanUtil.copyProperties
    虽然字段类型不一样,但是做了兼容处理,所以业务没有影响业务逻辑。

升级后,
hutool
是5.8.8版本,执行结果如下图所示:

  • 执行报错,因为升级后的版本修改了实现,增加了下面的逻辑,如果包含E, 就会抛错,从而影响了业务逻辑,同时这个id是否包含e又是随机因素,到了生产才发现,就悲剧了。

分析探讨

我发现大部分人写代码都喜欢偷懒,在上面的场景中,虽然
BeanUtil.copyProperties
用的一时爽,但有时候带来的后果是很严重的,所以很不推荐这种方式。为什么这么说呢?

比如团队中的某些人偷偷改了数据传输对象DTO,比如修改了类型、删去了某个字段。用
BeanUtil.copyProperties
的方式压根无法在编译阶段发现,更别提修改的影响范围了,这就只能把风险暴露到生产上去了。那有什么更好的方法呢?

推荐方案

  1. 原始的
    get

    set
    方式

我是比较推崇这种做法的,比如现在
DiagramDTO
删去某个字段,编译器就会报错,就会引起你的注意了,让问题提前暴露,无处遁形。

你可能觉得站着说话不腰疼,字段少好,如果字段很多还不得写死啊,我这里推荐一个IDEA的插件,可以帮你智能生成这样的代码。

话不多说,自己玩儿去~~

  1. 使用开源库
    ModelMapper

ModelMapper
是一个开源库,可以很方便、简单地将对象从一种类型映射到另一种类型,底层是通过反射来自动确定对象之间的映射,还可以自定义映射规则。

 private static void testModelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        DiagramDTO diagramDTO = new DiagramDTO();
        diagramDTO.setId("3em3dgqsgmn0");
        diagramDTO.setCode("d1");
        diagramDTO.setName("图表");
        Diagram diagram = modelMapper.map(diagramDTO, Diagram.class);
    }
  1. 使用开源库
    MapStruct

MapStruct
也是Java中另外一个用于映射对象很流行的开源工具。它是在编译阶段生成对应的映射代码,相对于
ModelMapper
底层放射的方案,性能更好。

@Mapper
public interface DiagramMapper {
    DiagramMapper INSTANCE = Mappers.getMapper(DiagramMapper.class);

    DiagramDTO toDTO(Diagram diagram);

    Diagram toEntity(DiagramDTO diagram);
}

private static void testMapStruct() {
    DiagramDTO diagramDTO = new DiagramDTO();
    diagramDTO.setId("3em3dgqsgmn0");
    diagramDTO.setCode("d1");
    diagramDTO.setName("图表");
    Diagram diagram = DiagramMapper.INSTANCE.toEntity(diagramDTO);
}
  • DiagramMapper
    接口使用了
    @Mapper
    注解,用来表明使用
    MapStruct
    处理
  • MapStruct
    中更多高级特性大家自己探索一下。

总结

小结一下,对象在不同层之间进行转换映射,很不建议使用
BeanUtil.copyProperties
这种方式,更加推荐使用原生的
set
,
get
方式,不容易出错。当然这不是将
BeanUtil.copyProperties
一棒子打死,毫无用武之地,在特定场景,比如方法内部对象的转换等影响小的范围还是很方便的,如果你有其他的想法,也可以留下你的想法,一起探讨交流。

欢迎关注个人公众号【JAVA旭阳】交流学习!!

Hooks与事件绑定


React
中,我们经常需要为组件添加事件处理函数,例如处理表单提交、处理点击事件等。通常情况下,我们需要在类组件中使用
this
关键字来绑定事件处理函数的上下文,以便在函数中使用组件的实例属性和方法。
React Hooks

React 16.8
引入的一个新特性,其出现让
React
的函数组件也能够拥有状态和生命周期方法。
Hooks
的优势在于可以让我们在不编写类组件的情况下,复用状态逻辑和副作用代码,
Hooks
的一个常见用途是处理事件绑定。

描述


React
中使用类组件时,我们可能会被大量的
this
所困扰,例如
this.props

this.state
以及调用类中的函数等。此外,在定义事件处理函数时,通常需要使用
bind
方法来绑定函数的上下文,以确保在函数中可以正确地访问组件实例的属性和方法,虽然我们可以使用箭头函数来减少
bind
,但是还是使用
this
语法还是没跑了。

那么在使用
Hooks
的时候,可以避免使用类组件中的
this
关键字,因为
Hooks
是以函数的形式来组织组件逻辑的,我们通常只需要定义一个普通函数组件,并在函数组件中使用
useState

useEffect

Hooks
来管理组件状态和副作用,在处理事件绑定的时候,我们也只需要将定义的事件处理函数传入
JSX
就好了,也不需要
this
也不需要
bind

那么问题来了,这个问题真的这么简单吗,我们经常会听到类似于
Hooks
的心智负担很重的问题,从我们当前要讨论的事件绑定的角度上,那么心智负担就主要表现在
useEffect

useCallback
以及依赖数组上。其实类比来看,类组件类似于引入了
this

bind
的心智负担,而
Hooks
解决了类组件的心智负担,又引入了新的心智负担,但是其实换个角度来看,所谓的心智负担也只是需要接受的新知识而已,我们需要了解
React
推出新的设计,新的组件模型,当我们掌握了之后那就不会再被称为心智负担了,而应该叫做语法,当然其实叫做负担也不是没有道理的,因为很容易在不小心的情况下出现隐患。那么接下来我们就来讨论下
Hooks
与事件绑定的相关问题,所有示例代码都在
https://codesandbox.io/s/react-ts-template-forked-z8o7sv

事件绑定

使用
Hooks
进行普通的合成事件绑定是一件很轻松的事情,在这个例子中,我们使用了普通的合成事件
onClick
来监听按钮的点击事件,并在点击时调用了
add
函数来更新
count
状态变量的值,这样每次点击按钮时,
count
就会加
1

// https://codesandbox.io/s/hooks-event-z8o7sv
import { useState } from "react";

export const CounterNormal: React.FC = () => {
  const [count, setCount] = useState(0);
  const add = () => {
    setCount(count + 1);
  };
  return (
    <div>
      {count}
      <div>
        <button onClick={add}>count++</button>
      </div>
    </div>
  );
};

这个例子看起来非常简单,我们就不再过多解释了,其实从另一个角度想一下,这不是很类似于原生的
DOM0
事件流模型,每个对象只能绑定一个
DOM
事件的话,就不需要像
DOM2
事件流模型一样还得保持原来的处理函数引用才能进行卸载操作,否则是卸载不了的,如果不能保持引用的地址是相同的,那就会造成无限的绑定,进而造成内存泄漏,如果是
DOM0
的话,我们只需要覆盖即可,而不需要去保持之前的函数引用。实际上我们接下来要说的一些心智负担,就与引用地址息息相关。

另外有一点我们需要明确一下,当我们点击了这个
count
按钮,
React
帮我们做了什么。其实对于当前这个
<CounterNormal />
组件而言,当我们点击了按钮,那么肯定就是需要刷新视图,
React
的策略是会重新执行这个函数,由此来获得返回的
JSX
,然后就是常说的
diff
等流程,最后才会去渲染,只不过我们目前关注的重点就是这个函数组件的重新执行。
Hooks
实际上无非就是个函数,
React
通过内置的
use
为函数赋予了特殊的意义,使得其能够访问
Fiber
从而做到数据与节点相互绑定,那么既然是一个函数,并且在
setState
的时候还会重新执行,那么在重新执行的时候,点击按钮之前的
add
函数地址与点击按钮之后的
add
函数地址是不同的,因为这个函数实际上是被重新定义了一遍,只不过名字相同而已,从而其生成的静态作用域是不同的,那么这样便可能会造成所谓的闭包陷阱,接下来我们就来继续探讨相关的问题。

原生事件绑定

虽然
React
为我们提供了合成事件,但是在实际开发中因为各种各样的原因我们无法避免的会用到原生的事件绑定,例如
ReactDOM

Portal
传送门,其是遵循合成事件的事件流而不是
DOM
的事件流,比如将这个组件直接挂在
document.body
下,那么事件可能并不符合看起来
DOM
结构应该遵循的事件流,这可能不符合我们的预期,此时可能就需要进行原生的事件绑定了。此外,很多库可能都会有类似
addEventListener
的事件绑定,那么同样的此时也需要在合适的时机去添加和解除事件的绑定。由此,我们来看下边这个原生事件绑定的例子:

// https://codesandbox.io/s/react-ts-template-forked-z8o7sv?file=/src/counter-native.tsx
import { useEffect, useRef, useState } from "react";

export const CounterNative: React.FC = () => {
  const ref1 = useRef<HTMLButtonElement>(null);
  const ref2 = useRef<HTMLButtonElement>(null);
  const [count, setCount] = useState(0);

  const add = () => {
    setCount(count + 1);
  };

  useEffect(() => {
    const el = ref1.current;
    const handler = () => console.log(count);
    el?.addEventListener("click", handler);
    return () => {
      el?.removeEventListener("click", handler);
    };
  }, []);

  useEffect(() => {
    const el = ref2.current;
    const handler = () => console.log(count);
    el?.addEventListener("click", handler);
    return () => {
      el?.removeEventListener("click", handler);
    };
  }, [count]);

  return (
    <div>
      {count}
      <div>
        <button onClick={add}>count++</button>
        <button ref={ref1}>log count 1</button>
        <button ref={ref2}>log count 2</button>
      </div>
    </div>
  );
};

在这个例子中,我们分别对
ref1

ref2
两个
button
进行了原生事件绑定,其中
ref1
的事件绑定是在组件挂载的时候进行的,而
ref2
的事件绑定是在
count
发生变化的时候进行的,看起来代码上只有依赖数组
[]

[count]
的区别,但实际的效果上差别就很大了。在上边在线的
CodeSandbox
中我们首先点击三次
count++
这个按钮,然后分别点击
log count 1
按钮和
log count 2
按钮,那么输出会是如下的内容:

0 // log count 1
3 // log count 2

此时我们可以看出,页面上的
count
值明明是
3
,但是我们点击
log count 1
按钮的时候,输出的值却是
0
,只有点击
log count 2
按钮的时候,输出的值才是
3
,那么点击
log count 1
的输出肯定是不符合我们的预期的。那么为什么会出现这个情况呢,其实这就是所谓的
React Hooks
闭包陷阱了,其实我们上边也说了为什么会发生这个问题,我们再重新看一下,
Hooks
实际上无非就是个函数,
React
通过内置的
use
为函数赋予了特殊的意义,使得其能够访问
Fiber
从而做到数据与节点相互绑定,那么既然是一个函数,并且在
setState
的时候还会重新执行,那么在重新执行的时候,点击按钮之前的
add
函数地址与点击按钮之后的
add
函数地址是不同的,因为这个函数实际上是被重新定义了一遍,只不过名字相同而已,从而其生成的静态作用域是不同的,那么在新的函数执行时,假设我们不去更新新的函数,也就是不更新函数作用域的话,那么就会保持上次的
count
引用,就会导致打印了第一次绑定的数据。

那么同样的,
useEffect
也是一个函数,我们那么我们定义的事件绑定那个函数也其实就是
useEffect
的参数而已,在
state
发生改变的时候,这个函数虽然也被重新定义,但是由于我们的第二个参数即依赖数组的关系,其数组内的值在两次
render
之后是相同的,所以
useEffect
就不会去触发这个副作用的执行。那么实际上在
log count 1
中,因为依赖数组是空的
[]
,两次
render
或者说两次执行依次比较数组内的值没有发生变化,那么便不会触发副作用函数的执行;那么在
log count 2
中,因为依赖的数组是
[count]
,在两次
render
之后依次比较其值发现是发生了变化的,那么就会执行上次副作用函数的返回值,在这里就是清理副作用的函数
removeEventListener
,然后再执行传进来的新的副作用函数
addEventListener
。另外实际上也就是因为
React
需要返回一个清理副作用的函数,所以第一个函数不能直接用
async
装饰,否则执行副作用之后返回的就是一个
Promise
对象而不是直接可执行的副作用清理函数了。

useCallback

在上边的场景中,我们通过为
useEffect
添加依赖数组的方式似乎解决了这个问题,但是设想一个场景,如果一个函数需要被多个地方引入,也就是说类似于我们上一个示例中的
handler
函数,如果我们需要在多个位置引用这个函数,那么我们就不能像上一个例子一样直接定义在
useEffect
的第一个参数中。那么如果定义在外部,这个函数每次
re-render
就会被重新定义,那么就会导致
useEffect
的依赖数组发生变化,进而就会导致副作用函数的重新执行,显然这样也是不符合我们的预期的。此时就需要将这个函数的地址保持为唯一的,那么就需要
useCallback
这个
Hook
了,当使用
React
中的
useCallback Hook
时,其将返回一个
memoized
记忆化的回调函数,这个回调函数只有在其依赖项发生变化时才会重新创建,否则就会被缓存以便在后续的渲染中复用。通过这种方式可以帮助我们在
React
组件中优化性能,因为其可以防止不必要的重渲染,当将这个
memoized
回调函数传递给子组件时,就可以避免在每次渲染时重新创它,这样可以提高性能并减少内存的使用。由此,我们来看下边这个使用
useCallback
进行事件绑定的例子:

// https://codesandbox.io/s/react-ts-template-forked-z8o7sv?file=/src/counter-callback.tsx
import { useCallback, useEffect, useRef, useState } from "react";

export const CounterCallback: React.FC = () => {
  const ref1 = useRef<HTMLButtonElement>(null);
  const ref2 = useRef<HTMLButtonElement>(null);
  const [count, setCount] = useState(0);

  const add = () => {
    setCount(count + 1);
  };

  const logCount1 = () => console.log(count);

  useEffect(() => {
    const el = ref1.current;
    el?.addEventListener("click", logCount1);
    return () => {
      el?.removeEventListener("click", logCount1);
    };
  }, []);

  const logCount2 = useCallback(() => {
    console.log(count);
  }, [count]);

  useEffect(() => {
    const el = ref2.current;
    el?.addEventListener("click", logCount2);
    return () => {
      el?.removeEventListener("click", logCount2);
    };
  }, [logCount2]);

  return (
    <div>
      {count}
      <div>
        <button onClick={add}>count++</button>
        <button ref={ref1}>log count 1</button>
        <button ref={ref2}>log count 2</button>
      </div>
    </div>
  );
};

在这个例子中我们的
logCount1
没有
useCallback
包裹,每次
re-render
都会重新定义,此时
useEffect
也没有定义数组,所以在
re-render
时并没有再去执行新的事件绑定。那么对于
logCount2
而言,我们使用了
useCallback
包裹,那么每次
re-render
时,由于依赖数组是
[count]
的存在,因为
count
发生了变化
useCallback
返回的函数的地址也改变了,在这里如果有很多的状态的话,其他的状态改变了,
count
不变的话,那么这里的
logCount2
便不会改变,当然在这里我们只有
count
这一个状态,所以在
re-render
时,
useEffect
的依赖数组发生了变化,所以会重新执行事件绑定。在上边在线的
CodeSandbox
中我们首先点击三次
count++
这个按钮,然后分别点击
log count 1
按钮和
log count 2
按钮,那么输出会是如下的内容:

0 // log count 1
3 // log count 2

那么实际上我们可以看出来,在这里如果的
log count 1
与原生事件绑定例子中的
log count 1
一样,都因为没有及时更新而保持了上一次
render
的静态作用域,导致了输出
0
,而由于
log count 2
及时更新了作用域,所以正确输出了
3
,实际上这个例子并不全,我们可以很明显的发现实际上应该有其他种情况的,我们同样先点击
count++
三次,然后再分情况看输出:

  • logCount
    函数不用
    useCallback
    包装。
    • useEffect
      依赖数组为
      []
      : 输出
      0
    • useEffect
      依赖数组为
      [count]
      : 输出
      3
    • useEffect
      依赖数组为
      [logCount]
      : 输出
      3
  • logCount
    函数使用
    useCallback
    包装,依赖为
    []

    • useEffect
      依赖数组为
      []
      : 输出
      0
    • useEffect
      依赖数组为
      [count]
      : 输出
      0
    • useEffect
      依赖数组为
      [logCount]
      : 输出
      0
  • logCount
    函数使用
    useCallback
    包装,依赖为
    [count]

    • useEffect
      依赖数组为
      []
      : 输出
      0
    • useEffect
      依赖数组为
      [count]
      : 输出
      3
    • useEffect
      依赖数组为
      [logCount]
      : 输出
      3

虽然看起来情况这么多,但是实际上如果接入了
react-hooks/exhaustive-deps
规则的话,发现其实际上是会建议我们使用
3.3
这个方法来处理依赖的,这也是最标准的解决方案,其他的方案要不就是存在不必要的函数重定义,要不就是存在应该重定义但是依然存在旧的函数作用域引用的情况,其实由此看来
React
的心智负担确实是有些重的,而且
useCallback
能够完全解决问题吗,实际上并没有,我们可以接着往下聊聊
useCallback
的缺陷。

useMemoizedFn

同样的,我们继续来看一个例子,这个例子可能相对比较复杂,因为会有一个比较长的依赖传递,然后导致看起来比较麻烦。另外实际上这个例子也不能说
useCallback
是有问题的,只能说是会有相当重的心智负担。

const getTextInfo = useCallback(() => { // 获取一段数据
  return [text.length, dep.length];
}, [text, dep]);

const post = useCallback(() => { // 发送数据
  const [textLen, depLen] = getTextInfo();
  postEvent({ textLen, depLen });
}, [getTextInfo, postEvent]);

useEffect(() => {
  post();
}, [dep, post]);

在这个例子中,我们希望达到的目标是仅当
dep
发生改变的时候,触发
post
函数,从而将数据进行发送,在这里我们完全按照了
react-hooks/exhaustive-deps
的规则去定义了函数。那么看起来似乎并没有什么问题,但是当我们实际去应用的时候,会发现当
text
这个状态发生变化的时候,同样会触发这个
post
函数的执行,这是个并不明显的问题,如果
text
这个状态改变的频率很低的话,甚至在回归的过程中都可能无法发现这个问题。此外,可以看到这个依赖的链路已经很长了,如果函数在复杂一些,那复杂性越来越高,整个状态就会变的特别难以维护。

那么如何解决这个问题呢,一个可行的办法是我们可以将函数定义在
useRef
上,那么这样的话我们就可以一直拿到最新的函数定义了,实际效果与直接定义一个函数调用无异,只不过不会受到
react-hooks/exhaustive-deps
规则的困扰了。那么实际上我们并没有减缓复杂性,只是将复杂性转移到了
useRef
上,这样的话我们就需要去维护这个
useRef
的值,这样的话就会带来一些额外的心智负担。

const post = useRef(() => void 0);

post.current = () => {
  postEvent({ textLen, depLen });
}

useEffect(() => {
  post.current();
}, [dep]);

那么既然我们可以依靠
useRef
来解决这个问题,我们是不是可以将其封装为一个自定义的
Hooks
呢,然后因为实际上我们并没有办法阻止函数的创建,那么我们就使用两个
ref
,第一个
ref
保证永远是同一个引用,也就是说返回的函数永远指向同一个函数地址,第二个
ref
用来保存当前传入的函数,这样发生
re-render
的时候每次创建新的函数我们都将其更新,也就是说我们即将调用的永远都是最新的那个函数。这样通过两个
ref
我们就可以保证两点,第一点是无论发生多少次
re-render
,我们返回的都是同一个函数地址,第二点是无论发生了多少次
re-render
,我们即将调用的函数都是最新的。由此,我们就来看下
ahooks
是如何实现的
useMemoizedFn

type noop = (this: any, ...args: any[]) => any;

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>;

function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn);

  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
  fnRef.current = useMemo(() => fn, [fn]);

  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
}

那么使用的时候就很简单了,可以看到我们使用
useMemoizedFn
时是不需要依赖数组的,并且虽然我们在
useEffect
中定义了
post
函数的依赖,但是由于我们上边保证了第一点,那么这个在这个组件被完全卸载之前,这个依赖的函数地址是不会变的,由此我们就可以保证只可能由于
dep
发生的改变才会触发
useEffect
,而且我们保证的第二点,可以让我们在
re-render
之后拿到的都是最新的函数作用域,也就是
textLen

depLen
是能够保证是最新的
,不会存在拿到了旧的函数作用域里边值的问题。

const post = useMemoizedFn(() => {
  postEvent({ textLen, depLen });
});

useEffect(() => {
  post.current();
}, [dep, post]);

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://juejin.cn/post/7194368992025247804
https://juejin.cn/post/7098137024204374030
https://react.dev/reference/react/useCallback

1、装饰器基础介绍

1.1 何为Python中的装饰器?

Python中装饰器的定义以及用途:

装饰器是一种特殊的函数,它可以接受一个函数作为参数,并返回一个新的函数。装饰器可以用来修改或增强函数的行为,而不需要修改函数本身的代码。在Python中,装饰器通常用于实现AOP(面向切面编程),例如日志记录、性能分析、缓存等。装饰器的语法使用@符号,将装饰器函数放在被装饰函数的定义之前

学过设计模式的朋友都知道,设计模式的结构型模式中也有一个叫装饰器模式,那这个和Python中的装饰器有什么不同呢?

设计模式中的装饰器的定义以及用途:

设计模式中的装饰器是一种结构型模式,它可以在不改变原对象的情况下,为对象添加额外的功能。装饰器模式通常用于在运行时动态地为对象添加功能,而不是在编译时静态地为对象添加功能。装饰器模式通常涉及到多个对象之间的协作,而不是单个函数或对象。

因此,Python中的装饰器和设计模式中的装饰器虽然名称相同,但是它们的实现方式和应用场景有很大的不同。

1.2 闭包

那Python种的装饰器是怎么实现的呢?先不用着急,我们先来一起学习学习Python中的闭包。

那什么叫做闭包呢?

闭包是指一个函数和它所在的环境变量的组合,即在函数内部定义的函数可以访问外部函数的变量和参数,即使外部函数已经返回。闭包可以用来实现函数式编程中的柯里化、惰性求值、函数组合等高级特性。

看着上面的文字,是不是感觉有点抽象。我说一说我对闭包的理解

闭包是由外部函数和内部函数,内部函数引用到了外部函数定义的变量,外部函数的返回值是内部函数的函数名。对于这样的函数,我们就称为闭包。

好像也有点抽象,我们来看一断代码,就能够理解上面的话了。

def my_decorator():  # my_decorator 这个就叫做外部函数
    a = 1
    def inner():  # inner 这个叫做内部函数
        print(a)  # 内部函数引用到了外部函数中定义的变量
    return inner  # 外部函数的返回值是内部函数名

2、函数装饰器的实现

上面讲解了装饰器的定义、用途,还有闭包,那怎么去实现一个装饰器呢?不急,接下来我们一起来学习如何实现装饰器。

装饰器不是说可以不改变一个函数源代码的基础上,给这个函数添加额外的功能吗?那怎么做呢?

接下来,我们就一起实现一个装饰器,来计算函数的执行时间。Let‘s go!

2.1 不使用@实现装饰器

首先,使用闭包定义一个统计函数执行时间的功能。

def process_time(func):
    def inner(*args, **kwargs):
        start_time = time.time()
        ret = func(*args, **kwargs)
        end_time = time.time()
        print("函数的执行时间为:%d" % (end_time-start_time))
        return ret
    return inner

接下来定义一个函数,使用比较来计算函数的执行时间。

import time


def process_time(func):
    def inner(*args, **kwargs):
        start_time = time.time()
        ret = func(*args, **kwargs)
        end_time = time.time()
        print("函数的执行时间为:%d" % (end_time-start_time))
        return ret
    return inner


def test(sleep_time):
    time.sleep(sleep_time)


t1 = process_time(test)
t1(1)
print("------------")
t1(2)

执行结果:

函数的执行时间为:1
------------
函数的执行时间为:2

通过上面的代码,我们观察到,我们并没有修改
test
函数的源代码,依旧给
test
函数添加上了统计函数执行时间的功能。

Python中实现上述功能,有更加优雅的方式。下面,我们就一起来看看如何实现的。

2.2 Python中使用语法糖的装饰器(推荐使用)

import time


def process_time(func):
    def inner(*args, **kwargs):
        start_time = time.time()
        ret = func(*args, **kwargs)
        end_time = time.time()
        print("函数的执行时间为:%d" % (end_time-start_time))
        return ret
    return inner


@process_time
def test(sleep_time):
    time.sleep(sleep_time)


test(1)
print("------------")
test(2)

执行结果:

函数的执行时间为:1
------------
函数的执行时间为:2

观察上面的代码变动,发现只有很少的部分修改了。

1、
test
函数上面添加了一行
@process_time

2、
test
函数的调用方式发生了改变。

其他的并没有发生变化,整个代码看起来也更加清爽了。

提示:

当使用@装饰器时,会自动执行 闭包中的外部函数内容。这个可以自行验证。

当使用@装饰器时,Python解释器为我们做了什么?

当使用@装饰器时,Python解释器会将被装饰的函数作为参数传递给装饰器函数,并将其返回值作为新的函数对象替换原来的函数对象。这样,每次调用被装饰的函数时,实际上是调用了装饰器函数返回的新函数对象。

Python 装饰器 @ 实际上是一种语法糖,它可以让我们在不改变原函数代码的情况下,对函数进行扩展或修改。当我们使用 @ 装饰器时,实际上是将被装饰函数作为参数传递给装饰器函数,然后将装饰器函数的返回值赋值给原函数名。因此,@ 装饰器并不会进行内存拷贝。

通过下面的函数,可以得知,
inner

test
函数指向的是同一个内存地址。

import time


def process_time(func):

    print("func id --->", id(func))

    def inner(*args, **kwargs):
        start_time = time.time()
        ret = func(*args, **kwargs)
        end_time = time.time()
        print("函数的执行时间为:%d" % (end_time - start_time))
        return ret

    print("inner id --->", id(inner))
    return inner


@process_time
def test(sleep_time):
    print("test func id --->", id(test))
    time.sleep(sleep_time)


print("test id --->", id(test))

执行结果:

func id ---> 4312377952
inner id ---> 4313983008
test id ---> 4313983008

使用语法糖时,Python解释器底层为我们做了这样的处理。

2.3 多个装饰器的执行顺序

上面的两个例子,都只有一个装饰器,是不是Python只能写一个装饰器呢。其实不是的。主要是为了讲解简单。接下来,我们一起来看看,多个装饰器的执行顺序。

def outer_1(func):
    print("coming outer_1")

    def inner_1():
        print("coming inner_1")
        func()
    return inner_1


def outer_2(func):
    print("coming outer_2")

    def inner_2():
        print("coming inner_2")
        func()

    return inner_2


def outer_3(func):
    print("coming outer_3")

    def inner_3():
        print("coming inner_3")
        func()

    return inner_3


@outer_1
@outer_2
@outer_3
def test():
    print("coming test")


test()

执行结果:

coming outer_3
coming outer_2
coming outer_1
coming inner_1
coming inner_2
coming inner_3
coming test

outer_3 -> outer_2 -> outer_1 -> inner_1 -> inner_2 -> inner_3 -> 被装饰函数

从上面的执行结果,可以得出如下结论:

使用多个装饰器装饰函数时,
外部函数的执行顺序是从下到上的。
内部函数的执行顺序是从下往上的。

多个装饰器装饰函数时,Python解释器底层做了啥

通过下面这段代码验证

def outer_1(func):
    print("coming outer_1, func id -->", id(func))

    def inner_1():
        print("coming inner_1")
        func()

    print("inner_1 id -->", id(inner_1))
    return inner_1


def outer_2(func):
    print("coming outer_2, func id -->", id(func))

    def inner_2():
        print("coming inner_2")
        func()

    print("inner_2 id -->", id(inner_2))
    return inner_2


def outer_3(func):
    print("coming outer_3, func id -->", id(func))

    def inner_3():
        print("coming inner_3")
        func()

    print("inner_3 id -->", id(inner_3))
    return inner_3


@outer_1
@outer_2
@outer_3
def test():
    print("coming test")


test()

执行结果:

coming outer_3, func id --> 4389102784
inner_3 id --> 4389102928
coming outer_2, func id --> 4389102928
inner_2 id --> 4389103072
coming outer_1, func id --> 4389103072
inner_1 id --> 4389103216
coming inner_1
coming inner_2
coming inner_3
coming test

2.4 带参数的装饰器

该如何实现带参数的装饰器呢,其实原理一样的,我们再定义一个外层函数,外层函数的返回值是内存函数的名称,即引用。

下面我们来看一个例子:

def is_process(flag):
    def outer_1(func):
        print("coming outer_1, func id -->", id(func))

        def inner_1():
            print("coming inner_1")
            if flag:
                func()

        print("inner_1 id -->", id(inner_1))
        return inner_1
    return outer_1


@is_process(True)
def test():
    print("coming test")


test()

注意:

  • 我们装饰函数时,装饰器的写法不同了,变成了
    @is_process(True)
    ,这里是调用了
    is_process
    这个函数

3、函数装饰器的注意点(wraps函数)

猜一猜下面函数会输出什么?

def outer_1(func):
    def inner_1():
        print("inner_1, func __name__", func.__name__)
        print("inner_1, func __doc__", func.__doc__)
        func()

    return inner_1


@outer_1
def test():
    """this is test"""
    print("outer_1, func __name__", test.__name__)
    print("outer_1, func __doc__", test.__doc__)


test()

函数执行结果:

inner_1, func __name__ test
inner_1, func __doc__ this is test
test, test __name__ inner_1
test, test __doc__ None

注意到没,在test函数体内打印函数的
__name__、__doc__
属性,居然变成内部函数的了。

这个是为什么呢?

Python装饰器在装饰函数时,会将原函数的函数名、文档字符串、参数列表等属性复制到装饰器函数中,但是装饰器函数并不会复制原函数的所有属性。例如,原函数的
name
属性、
doc
属性、
module
属性等都不会被复制到装饰器函数中。

为了避免这种情况,可以使用functools库中的wraps装饰器来保留原来函数对象的属性。wraps装饰器可以将原来函数对象的属性复制到新的函数对象中,从而避免属性丢失的问题。

from functools import wraps


def outer_1(func):

    @wraps(func)
    def inner_1():
        print("inner_1, func __name__", func.__name__)
        print("inner_1, func __doc__", func.__doc__)
        func()

    return inner_1


@outer_1
def test():
    """this is test"""
    print("test, test __name__", test.__name__)
    print("test, test __doc__", test.__doc__)


test()

执行结果:

inner_1, func __name__ test
inner_1, func __doc__ this is test
test, test __name__ test
test, test __doc__ this is test

4、类装饰器

上面我们都是使用的函数来实现装饰器的功能,那可不可以用类来实现装饰器的功能呢?我们知道函数实现装饰器的原理是外部函数的参数是被装饰的函数,外部函数返回内部函数的名称。内部函数中去执行被装饰的函数。

那么其实类也是可以用来实现装饰器的,因为当我们为 类 定义了
__call__
方法时,这个类就成了可调用对象,实例化后可直接调用。

class ProcessTime:

    def __call__(self, *args, **kwargs):
        print("call")


p = ProcessTime()
p()

4.1 类装饰器的实现

import time


class ProcessTime:

    def __init__(self, func):

        print("coming ProcessTime __init__")
        self.func = func

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        print("coming ProcessTime __call__, id(self.func) -->", id(self.func))
        ret = self.func(*args, **kwargs)
        end_time = time.time()
        print("ProcessTime 函数的执行时间为:%d" % (end_time - start_time))
        return ret


@ProcessTime
def test(sleep_time):
    time.sleep(sleep_time)
    return "tet"


test(1)

执行结果:

coming ProcessTime __init__
coming ProcessTime __call__, id(self.func) --> 4488922160
ProcessTime 函数的执行时间为:1

通过上面的执行结果,我们可以得到,
@ProcessTime
的作用是
test = ProcessTime(test)
。又因为
ProcessTime
定义了
__call__
方法,是可调用对象,所以可以像函数那样直接调用实例化
ProcessTime
后的对象。

这里可以验证,通过注释掉装饰器,手动初始化
ProcessTime
类。得到的结果是一样的。

# @ProcessTime
def test(sleep_time):
    time.sleep(sleep_time)
    return "tet"


test = ProcessTime(test)
test(1)

4.2 多个类装饰器的执行顺序

多个类装饰器的执行顺序是怎么样的呢,这里我们也通过代码来进行验证。

import time


class ProcessTime:

    def __init__(self, func):

        print("coming ProcessTime __init__", id(self))
        self.func = func

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        print("coming ProcessTime __call__, id(self.func) -->", id(self.func))
        ret = self.func(*args, **kwargs)
        end_time = time.time()
        print("ProcessTime 函数的执行时间为:%d" % (end_time - start_time))
        return ret


class ProcessTime2:

    def __init__(self, func):
        print("coming ProcessTime2 __init__", id(self))
        self.func = func

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        print("coming ProcessTime2 __call__, id(self.func) -->", id(self.func))
        ret = self.func(*args, **kwargs)
        end_time = time.time()
        print("ProcessTime2 函数的执行时间为:%d" % (end_time - start_time))
        return ret


@ProcessTime
@ProcessTime2
def test(sleep_time):
    time.sleep(sleep_time)
    return "tet"


# test = ProcessTime2(test)
# test = ProcessTime(test)

t = test(1)

执行结果:

coming ProcessTime2 __init__ 4472235104
coming ProcessTime __init__ 4473162672
coming ProcessTime __call__, id(self.func) --> 4472235104
coming ProcessTime2 __call__, id(self.func) --> 4471735344
ProcessTime2 函数的执行时间为:1
ProcessTime 函数的执行时间为:1

从上面的结果,我们得到,执行顺序是:

ProcessTime2 中的__init__
->
ProcessTime 中的__init__
->
ProcessTime 中的__call__
->
ProcessTime2 中的__call__

特别注意:

ProcessTime 中的__call__
中的代码并不会执行完后再去执行
ProcessTime2 中的__call__
,而是在调用
ret = self.func(*args, **kwargs)
方法后,就回去执行
ProcessTime2 中的__call__
的代码。

4.3 类装饰器存在的问题

其实,类装饰器也存在和函数装饰器一样的问题。它会覆盖原函数的元数据信息,例如函数名、文档字符串、参数列表等。这可能会导致一些问题,例如调试时无法正确显示函数名、文档生成工具无法正确生成文档等。

import time
from functools import wraps


class ProcessTime:

    def __init__(self, func):

        print("coming ProcessTime __init__", id(self))
        self.func = func

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        print("coming ProcessTime __call__, id(self.func) -->", id(self.func))

        ret = self.func(*args, **kwargs)
        end_time = time.time()
        print("ProcessTime 函数的执行时间为:%d" % (end_time - start_time))
        return ret
        

@ProcessTime
def test(sleep_time):
    "tets"
    print("test.__doc__", test.__doc__)
    # print(test.__name__)  --> 报错,AttributeError: 'ProcessTime' object has no attribute '__name__'
    time.sleep(sleep_time)
    return "tet"
  

t = test(1)

那类装饰器该如何解决呢?

我现在还不知道该如何处理,如果有知道的朋友,请不吝赐教,十分感谢!!

5、多个装饰器的执行顺序总结

其实,我觉得不用特别的去记多个装饰器的执行顺序是如何的,我们最重要的是理解到装饰器的执行逻辑是如何的。函数装饰器和类装饰器的初始化顺序都是一样的:
从靠近被装饰的函数开始执行初始化操作
。把这个核心原理理解到后,多个装饰器的执行顺序在使用的时候,就很容易得到了。

不论做什么产品,界面上几乎都少不了「确定」按钮,例如:操作提示时、进行选择时、填写表单数据时……

img

完成、保存、下单……各种代表“确定某一步骤”的按钮,都可以统称为确定按钮。

以前的产品设计,大量以来确定按钮,但是现在的产品设计,确定按钮用得越来越少了。

倒不是因为不需要确定操作了,而是除了「确定按钮」之外,设计师们发现了更好的方式。

能自动「确定」就不需要按钮

有一句话说:

最好的交互就是没有交互

同样,最好的「确定」按钮,其实是没有按钮。

例如,手机锁屏时,输入密码后不需要确定按钮就可以直接验证进入。

img

能这么做是因为手机锁屏密码固定六位数,而电脑的锁屏密码通常不确定,所以还是需要一个「确定」按钮。

img

这说明,如果能够预料到用户的操作数量,且操作很简单的话,是完全可以考虑去掉「确定」按钮的。

img

不是什么时候都适合写「确定」

通常「确定」字面上可以表示:“我知道了/就这样”。

如果是提示或者是简单的操作,则比较适合使用「确定」文案。

img

把确定了什么写出来

如果是发送、登录、购买、支付……这类目的性很强的操作,与其写「确定」还不如直接把操作目的写出来。

不然,如果只写「确定」,让人容易犯迷糊。

img

进行了输入/修改,得用「保存」

输入操作后,用户最怕丢失信息没有保存,回头得重新写。

如果只是简单的表单,例如设置用户名什么的,重来一次倒也无所谓。但如果是填写项达到三、四条甚至更多,看不到「保存」两个字难免担心数据丢失。

如果按钮上写清楚「保存」两个字,也让人安心一些。如果是自动保存,也需要把相应反馈展示出来。

img

只是结束流程而非操作,可以用「完成」

其实「完成」按钮大多可以用「确定」替代。

但是「完成」的特别之处,是表达了“结束流程”的概念。

所以,如果想让用户感觉按下按钮之后不会进行任何其它操作,而仅仅只是结束流程而已,则很时候使用「完成」。

img

工具类产品,可以用「XX并XX」

很多工具类产品,为了操作效率会把两个操作并列起来。

例如我现在打在所用的公众号编辑器,就有一个「保存并群发」的按钮:

img

这样做挺好的,工具类产品效率最重要美观是其次。两个经常要连贯执行的操作,合为一个按钮能够减少没必要的点击。

而一旦把操作合并了,肯定要写清楚比较好。否则如果没说明白,既让人难以理解,而且点起来又不放心,导致学习成本大大增加。

有一个问题是,如果文字太长了,可以考虑简化按钮文案并在旁边加点提示。

img

工具类产品效率最重要美观是其次,因为如果不写清楚很难让人理解。

前往了解
国思RDIF.vNext
低代码开发平台:
http://www.guosisoft.com

海盗分金,GPT-4初露锋芒

GPT系列模型横空出世后,其是否真实具有思考和推理的能力一直被业界关注。GPT-3.5在多条狗问题和海盗分金问题上表现糟糕。GPT-4在这两个谜题上给出的答案令人惊喜,甚至能给出海盗分金问题的详细解析解。 GPT-4表现出色,令人印象深刻。它不仅能给出海盗分金问题的正确答案,还能给出详细的步骤解析。这似乎表明GPT-4具有一定的逻辑思维和推理能力。但是,我们仍然需要采取谨慎态度。理由在于GPT-4的答案可能依赖于大量的数据积累,而非对知识的深度理解与总结。
多条狗问题
海盗分金1
海盗分金2

爱因斯坦谜题,记忆胜过思考

针对爱因斯坦谜题,在原始参数下GPT-4表现完美。但是一旦修改谜题数据,GPT-4的表现就像“弱智”。这证明其答案依赖于记忆而非思考。爱因斯坦谜题考察推理能力,GPT-4在标准测试中表现异常出色。但是,一旦修改谜题条件,GPT-4的表现就暴露出弱点。这说明GPT-4答案的生成依赖于记忆,而非独立思考。一旦遇到新的条件,GPT-4表现就像“弱智”,完全丧失推理能力。这也从侧面证实,GPT-4可能没有真实独立思考的能力。
爱因斯坦1
爱因斯坦2
爱因斯坦3
爱因斯坦4

双信封悖论,表面学习难掩实质

对于双信封悖论,GPT-4给出了主流但错误的答案。提示1美分是真实世界美元最小单位后,GPT-4的表现没有体现纳入新条件后的深度思考。证明其没有真实思考过这个问题。双信封悖论是典型的推理测试题。GPT-4给出的答案证明它没有真正理解这个问题。即使给出提示“1美分是真实世界美元最小单位”,GPT-4的表现也没有改善。这表明GPT-4没有在深入思考这个问题,没有真正理解问题的本质与条件。它的答案依然停留在表面。这也进一步证明GPT-4可能缺乏独立思考的能力。
双信封1
双信封2
双信封3
双信封4

硅基生命,智力待提

总的来说,GPT-4显示出阅读速度快、记忆能力强和分类能力高超的特点。但是,它的智力水平可能只相当于学龄前儿童。GPT-4展现的所谓“思考”和“推理”能力实际上建立在大量的数据积累之上,而不是对知识的深入理解与总结。GPT-4更像是一个高效的分类器和总结器,而非真正的思考者。但是,作为一种新型的人工智能,GPT-4的学习是持续的和正向的。随着更多数据的积累与参数的调整,GPT-4的智力水平也会不断持续提高,这只是一个时间问题。