ESEE

eye301

25.7.23

一个pycharm使用小技巧

也是Django后端启动命令python manage.py runserver的一个类似快捷方式的技巧。

  1. 先在界面左侧的manage.py右键点击run,跑完之后,上边栏的绿三角run就会出现manage的图标。
  2. 点击该图标,在其中找到Edit Configurations并点击。
  3. 随后在打开的界面的左侧中找到Python下的manage并点击,随后在右侧的Script parameters中输入参数runserver即可使之后每次对该文件run时,带上该参数进行,效果大致如下:

25.7.24

25.7.29

  1. now = timezone.make_aware(datetime.now(timezone.utc))语句中now的类型?
    datetime.now(timezone.utc)获得一个表示当前 UTC 时间的aware对象(有时区信息)
    timezone.make_aware(...)naive datetime对象转换为aware datetime对象(附加时区信息)
    过于冗长了,可以更加简洁:now = datetime.now(timezone.utc)now = timezone.now()
  2. 看一下数据库create等操作中default=的含义是什么?
    create方法中并没有default参数,除非是有定义该字段,否则建议还是按字典赋值的方式赋值即:key = value;但是对于update_or_create,其含义是,拿传入的非defaults的字段的值去检索数据库中对应的一行数据,相当于不在defaults中的字段就是主键,而之后再拿defaults中的字段去更新或创建对应的一行数据中的相应字段,即有则改之,无则创建。
  3. Response改成JsonResponse
    已解决
  4. 成功执行后需要返回数据给前端,尤其是get请求
    已解决
  5. 单列一个view文件存PostOperationInfoView的内容
    已解决
  6. Munk评级的中文含义
    已解决
  7. Django是否有一次性输入数据的操作来替代for循环遍历
    bulk_create()来批量插入数据库
    优点:
    速度非常快: 相比于循环创建和保存,性能提升显著。
    代码简洁。
    缺点:
    不触发信号: 不会触发 pre_save 或 post_save 信号。
    不调用 save() 方法: 不会执行模型中定义的 save() 方法的逻辑。
    不支持 ManyToMany 字段的批量创建: 需要手动处理ManyToMany关系的创建
  8. 测试
    1)修改时会出现此前没有的存在的数据条目嘛?就是这病人的某一个眼别和术后随访类别组合不存在,
    如果可以的话,应该是直接报错还是填入数据库中?

25.8.1

服务器内部可能错误

  1. 传入字段不存在
  2. 传入字段的类型不符合设置的,例如:BigInteger传给Integer
  3. 注意可以使用try .... catch来捕获异常,而不是傻傻的盯着服务器异常看(

25.8.3

手术信息、病人信息修改等事宜

  1. 手术信息和手术其他信息一起按数组的形式传给后端
  2. 病史、既往病史、诊断做成纵表,需要像手术信息那样把其他单拎一个出来表
  3. 将与眼别无关的状态置为2
  4. 术前评分是横表
  5. 上述表全部重新建表

25.8.4

既往史相关信息的修正

也是做成纵表的形式,用”100”这种每一位代表每一层的方式来表示不同字段的组合:
具体的代码形式(我认为的)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 鼻泪管
NASOLA = [(0, "原发性/狭窄/膜性"), (1, "原发性/狭窄/骨性"),
(10, "原发性/阻塞/膜性"), (11, "原发性/阻塞/骨性"),
(100, "先天性/狭窄/膜性"), (101, "先天性/狭窄/骨性"),
(110, "先天性/阻塞/膜性"), (111, "先天性/阻塞/骨性"),
(120, "先天性/缺失(即未发育)"),
(200, "外伤性/狭窄/膜性"), (201, "外伤性/狭窄/骨性"),
(210, "外伤性/阻塞/膜性"), (211, "外伤性/阻塞/骨性"),
(220, "外伤性/缺失"),
(300, "肿瘤性/狭窄/膜性"), (301, "肿瘤性/狭窄/骨性"),
(310, "肿瘤性/阻塞/膜性"), (311, "肿瘤性/阻塞/骨性"),
(320, "肿瘤性/缺失"),
(400, "其他,可填空")]

25.8.7

  1. 纵表的其他内容是”可有可无”的,即只有需要有其他内容才应该要有,否则应该闲置。而对于像某些症状需要补充内容的情况,则跟“其他,请文字输入”一致,都是主表存在并在副表根据主表的主键号来设置关联,不过对于其他的这种是主表点明有副表内容,否则就是用户提供了才有副表内容。
  2. 术后随访信息补充:1.术后带管时间,自由输入,单位月;2.术后并发症:多选(泪点撕裂、人工泪管移位、继发性泪小管炎和填空自由输入)。

25.8.9

  1. 如何进行既往史的数据生成:
    1. 对横表而言:1000000条眼睛数据中每个病人500000条左眼(0),500000右眼(1)信息,只能先生成500000条左眼,然后再生成500000条右眼,分两次生成,由于横表的多选是要采用varchar类型的,因此还是随机生成,例如对于:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
         # 曾行泪道手术
      PRE_TEAR_SUR = [(0, "泪道探通"), (1, "泪道置管术"), (2, "外路泪囊鼻腔吻合术"),
      (3, "经鼻泪囊鼻腔吻合术"), (4, "其他,请文字输入")]```
      则采用正则表达式:`[0-1]/[2-3]/4`的方式生成。
      2. 对纵表而言,由于不考虑那些公有的属性,因此只需要考虑与眼别相关的两个属性,这两个属性每个4项多选(为了方便每个病人的数据数相同),因此一个病人8条单眼数据,则一个病人拥有16条数据,一共62500个病人,分16次录入数据,每个病人按照眼别与与眼别相关属性的选项的组合来生成

      # 25.8.10
      1. 对于术后随访信息表增加其他表来存放术后并发症的其他信息。bingo
      2. **编写接口**(好巧妙的方式)用于针对既往史表的600万条数据(50万个病人信息,100万只眼睛的信息,每只眼睛6条信息) bingo
      3. 针对2的接口生成用于自动化发送请求的python脚本,问问gpt bingo
      4. 数据生成完后将建立相应的索引把查询速度对比记录在一张表中

      # 25.8.11
      1. 在数据库中对于以正则表达式建立的查询在多次同一查询下速度越来越快的可能原因是?
      1). 查询缓存,在语句本身结构和表达式本身并未发生变化的情况下仍可能发生作用。
      2). 正则表达式引擎缓存,避免重复编译。
      3). 预热效应,第一次执行时进行的诸如加载数据等初始化操作在后续执行时可能会被跳过,从而提高查询速度。
      2. 但是对于字段相等的查询则每次的查询时间都有所波动,这可能是什么原因导致的,例如:select * FROM patientinfo_originalpastmedicalhistory WHERE ocular_disease_history REGEXP "1/*";SELECT * FROM patientinfo_originalpastmedicalhistory WHERE patient_id = "66666";例如在使用navicat反复执行这两个语句时第一条语句的查询时间是越来越短,但是第二条语句的查询时间则是来回波动。
      3. 如何处理数据库中纵表的数据,例如一条数据由玩家id、属性(12)、属性名称(对于属性1123个名称、对于属性2456个名称)组成,请问我应该如何编写mysql语句来实现查询同时拥有属性112名称的玩家数据?
      1). 使用自连接(已知属性名称id数的情况下),此方法将表自身连接多次来查找满足所有条件的玩家,例如:
      ```sql
      -- 选择所有满足(4, 1)并且其存在有(4, 3)记录条件的记录,
      SELECT *
      FROM patientinfo_pastmedicalhistory AS pa1
      WHERE pa1.past_medical_history_att = 4
      AND pa1.past_medical_history_att_content = 1
      AND EXISTS (SELECT 1
      FROM patientinfo_pastmedicalhistory AS pa2
      WHERE pa2.patient_id = pa1.patient_id
      AND pa2.past_medical_history_att = pa1.past_medical_history_att
      AND pa2.past_medical_history_att_content = 3)
      -- UNION去重连接,将上下两个查询结果去重合并
      UNION
      -- 选组满足(4, 3)且存在(4, 1)条件的记录
      SELECT *
      FROM patientinfo_pastmedicalhistory AS pa2
      WHERE pa2.past_medical_history_att = 4
      AND pa2.past_medical_history_att_content = 3
      AND EXISTS (SELECT 1
      FROM patientinfo_pastmedicalhistory AS pa1
      WHERE pa1.patient_id = pa2.patient_id
      AND pa1.past_medical_history_att = pa2.past_medical_history_att
      AND pa1.past_medical_history_att_content = 1);
      ```
      2).使用GROUP BY 和 HAVING语句
      例如:
      ```sql
      -- 此处若想查询所有信息即*,可能会遇到sql_mode=only_full_group_by 强制要求 SELECT 语句中除了聚合函数(例如 COUNT(), SUM(), AVG() 等)以外的所有列,都必须出现在 GROUP BY 子句中,或者在功能上依赖于 GROUP BY 子句中的列。 换句话说,SELECT 列表中的非聚合列必须是唯一的,由 GROUP BY 列决定的情况,因此更推荐使用子查询的方式即嵌套查询,先查出id然后根据查出来的id查询所有信息
      SELECT player_id
      FROM player_attributes
      WHERE attribute_id = 1
      AND attribute_name_id IN (1, 2)
      GROUP BY player_id
      HAVING COUNT(DISTINCT attribute_name_id) = 2;
      3).使用子查询,但是可能性能较差
      1
      2
      3
      4
      5
      6
      SELECT DISTINCT player_id
      FROM player_attributes
      WHERE attribute_id = 1
      AND attribute_name_id IN (1, 2)
      AND player_id IN (SELECT player_id FROM player_attributes WHERE attribute_id = 1 AND attribute_name_id = 1)
      AND player_id IN (SELECT player_id FROM player_attributes WHERE attribute_id = 1 AND attribute_name_id = 2);
      对于大型表来说,方法二较为合适,若查找属性名称id数固定且数量较少使用方法一,方法三适合在可读性要求高且性能不是关键考虑的情况下考虑。
  2. Django框架中的xxx.objects.create()方法不接受模型对象,其目的是创建一个新的模型实例并保存到数据库中,接收一个字典作为参数,键值对对应于模型字段名和值,可以直接输入键值对的形式如create(XXX=xxx,...)
    当然也可以将键值对也输入字典dict = {'':'', ...}中,然后使用展开字典:create(**dict)
    那么为什么直接传入模型的构造实例的方法又可以了,例如:XXX.objects.create(XXX()),这是因为XXX()创建了一个模型实例但是,并未放入数据库中,虽然 create() 方法本身不接受模型实例,
    但 Django ORM 在这种情况下会进行特殊处理。它会读取传入模型实例的所有字段的值,并将这些值作为关键字参数传递给内部的 create() 方法。(不过这种方法不推荐使用,相当于内存和数据库中都存入了一模一样的实例,浪费空间)**

25.8.12

  1. 经过比较横纵表的查询效率后又将原来从横表改为纵表又改回横表了(
  2. 主表的其他表对于主表中多个多选需要填入的情况存在局限性,而但是如果将主表中所有的需要输入的字段编号并在其他表中通过属性与属性内容一一对应的方式,即可达到存储的效果,例如:属性:”1/3/5”;属性内容:”xxx/yyy/zzz”
  3. 将所有其他信息表的纵表形式加入主表中

25.8.17

  1. isinstance()是用来检查一个对象是否为某个类的实例的,要使该函数返回值为True,需要传递一个类的实例作为第一参数,传入类本身反而会返回False;那么该如何判断传入的参数是否为某个类本身呢?使用type(obj) is type的方法,前者type(),对于类来说,type(obj) 将返回 <class ‘type’>。 对于类的实例来说,type(obj) 将返回该类本身(例如,<class ‘__main__.MyClass’>),在 Python中,类本身也是对象,它们的类型是 type。 因此,type(obj) is type 用于检查 obj 的类型是否是 type,即它是否是一个类。但是其并不能够去判断其是否是某个特定的类,应该使用如下方式来解决:model is Class来进行判断。这里需要注意is是对象标识判断,也即一定需要两者指向同一内存地址空间才会返回True,因此对于type(model) is Class中当model也是Class时,前者与后者值相同,但是位于不同的内存地址上,因此会返回False

25.8.18

  1. python中使用字典调用函数的时候需要注意不同函数之间对于同一个参数的类型限制需要统一:
    例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    switch_dict = {
    0: self.fill_case0,
    1: self.fill_case1,
    2: self.fill_case2,
    3: self.fill_case3,
    4: self.fill_case4,
    }
    func = (switch_dict.get(type, self.fill_default))
    func(isFirst, num, tempDict, item)
    其中,若在某几个函数中并不需要num可以相加减计算而另一个函数中则需要,那么这两个函数调用在对于num的类型限制上就会有所不同,而编译器察觉到此就会报警告。
    解决方案是可以在函数声明处,显示的标明类型:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    def fill_case0(self, isFirst: bool, num: int, tempDict: dict, item: dict):
    pass

    def fill_case1(self, isFirst: bool, num: int, tempDict: dict, item: dict):
    pass

    def fill_case3(self, isFirst: bool, num: int, tempDict: dict, item: dict):
    # 由于num声明为int,支持__ge__、__sub__,与其他方法一致
    pass

    def fill_default(self, isFirst: bool, num: int, tempDict: dict, item: dict):
    pass

    def fill_data(self, isFirst: bool, num: int, tempDict: dict, item: dict, type: int):
    switch_dict = {
    0: self.fill_case0,
    1: self.fill_case1,
    2: self.fill_case2,
    3: self.fill_case3,
    4: self.fill_case4,
    }
    func = switch_dict.get(type, self.fill_default)
    # 此时参数类型与所有方法的声明一致,警告消除
    return func(isFirst, num, tempDict, item)
  2. Django的serializer限制可以对于进行一些比较复杂的限制嘛?例如:对于CharField类型的字段att需要存在如下限制:“x/y/z”(其中x、y、z都是整数且都满足一定的取值范围例如[0,5]之间)?
    可以使用自定义字段验证器重写序列化器的验证方法
    1. 通过重写(validate_<字段名>),在一个函数中完成验证(其中的value是前端提供的原始数据):
      例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
            from rest_framework import serializers

      class MySerializer(serializers.Serializer):
      att = serializers.CharField()

      def validate_att(self, value):
      """验证 att 字段是否符合 "x/y/z" 格式,且 x,y,z 在 [0,5] 之间"""
      # 1. 检查格式是否为 "x/y/z"(分割后有 3 个部分)
      parts = value.split('/')
      if len(parts) != 3:
      raise serializers.ValidationError("格式必须为 'x/y/z'(x、y、z 为整数)")

      # 2. 检查 x,y,z 是否为整数
      try:
      x, y, z = map(int, parts)
      except ValueError:
      raise serializers.ValidationError("x、y、z 必须为整数")

      # 3. 检查 x,y,z 是否在 [0,5] 之间
      for num in [x, y, z]:
      if not (0 <= num <= 5):
      raise serializers.ValidationError("x、y、z 必须在 0-5 之间")

      # 验证通过,返回原始值
      return value
      ```
      2. 若涉及多个字段的验证,那么可以重写`validate`方法,进行对象级验证,(其中的`data`是经过字段级验证后的所有字段组成的字典,可以任意调用这些验证成功后的字段,进而进行不同字段之间的限制规范):
      例如:
      ```python
      from rest_framework import serializers
      from datetime import date

      class MySerializer(serializers.Serializer):
      start_date = serializers.DateField()
      end_date = serializers.DateField()
      min_age = serializers.IntegerField()
      max_age = serializers.IntegerField()

      def validate(self, data):
      """对象级验证:处理多个字段的关联逻辑"""
      # 1. 从 data 字典中获取需要验证的多个字段
      start_date = data.get('start_date') # 获取 start_date 字段的值
      end_date = data.get('end_date') # 获取 end_date 字段的值
      min_age = data.get('min_age')
      max_age = data.get('max_age')

      # 2. 验证 start_date 和 end_date 的关系
      if start_date and end_date and start_date > end_date:
      # 抛出错误时,可指定错误对应的字段(如 'end_date')
      raise serializers.ValidationError({
      'end_date': '结束日期不能早于开始日期'
      })

      # 3. 验证 min_age 和 max_age 的关系
      if min_age is not None and max_age is not None and min_age > max_age:
      raise serializers.ValidationError({
      'max_age': '最大年龄不能小于最小年龄',
      'min_age': '最小年龄不能大于最大年龄' # 可同时指定多个字段的错误
      })

      # 4. 验证通过后,必须返回完整的 data 字典(可修改后返回)
      return data

25.8.19

  1. hasattrsetattr的用法。
  2. POST 接口的 Body 中,DateTime 类型数据必须以字符串(string)形式传递,核心是确保字符串格式符合后端要求。只要格式正确,后端框架会自动完成字符串到日期时间对象的解析。

25.8.21

  1. 去除术后随访信息表中的重复并发症部分。
  2. serializer.errors包含Django进行的序列化器验证下验证数据失败后所有验证不通过的字段和错误愿意,以字典的格式返回,键为字段名,值为错误信息(字符串或列表),对非字段错误,信息会放在non_field_errors键下,可以在序列化器定义的时候通过error_messages自定义错误提示,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class OperationInfoSerializer(serializers.ModelSerializer):
    name = serializers.CharField(
    max_length=100,
    error_messages={
    "required": "手术名称不能为空",
    "max_length": "手术名称不能超过100个字符"
    }
    )

    class Meta:
    model = OperationInfo
    fields = ['id', 'name', 'date', 'doctor']
  3. serializer定义的字段若没有设定是否可以为空或为null(allow_blankallow_null),则默认都不可,需要手动设定可以。
  4. 手术信息表改成横表,增加入院时间字段
  5. 多进行调试,

25.8.22

  1. 直接将本地表同服务器表进行同步,不要直接在服务器上执行迁移命令
  2. 检索和分析:检索:时间范围、术后时间;分析:根据筛选范围筛选所有符合条件病人,保存用户病人id和眼别的集合,get接口获取病人集合接口

25.8.23

  1. 获得页数的代码按照豆包的润色进行修改
  2. 修改更新接口的逻辑,更新时前端会将所有数据都回传给你,不用担心传空的情况。
  3. 对于上传接口可以充分利用序列化器,使用其save()方法,通过传递序列化器定义时未曾赋予的额外的字段来保存数据;异常捕获范围越仔细越好;采用日志记录来记录报错信息;使用transaction.atomic()来确保操作的原子性,返回创建的实例数据而非原始的数据会更加安全
  4. 更新接口可以使用序列化器的”更新模式”(传入instance参数,而非仅用data);直接使用update()方法来更新已有记录(此处的update方法针对的是查询集,其会批量更新查询集中所有的对象,性能更优,但不会触发save()方法,而不是单个模型实例);直接基于更新数据补充不修改或未提供等字段,便于维护;缺少异常处理和日志记录。
  5. 修改了近乎所有的上传和更新接口
  6. 图片文件的处理:通过os库提供的路径拼接并创建相应目录,对于两个数组按照索引形成映射关系的模式可以按如下方式进行遍历:
    1
    2
    # image_files和image_types为数组
    for idx , (img, img_type) in enumerate(zip(image_files, image_types)):
    此后通过如下方式生成唯一文件名:
    1
    2
    3
    4
    timestamp = int(time.time() * 1000)
    unique_str = str(uuid.uuid4())[:8]
    # img_type为文件类型
    saved_name = f"{img_type}_{timestamp}_{unique_str}{ext}"
    通过如下方式进行文件的保存(记得要try-catch):
    1
    2
    3
    with open(img_path, "wb") as fp:
    for chunk in img.chunks():
    fp.write(chunk)
    其中img.chunks()将文件内容分割成多个小片段,对于大文件来说,可避免一次性占用过多内存,是Django中处理上传文件的标准写法,分块迭代的写入磁盘,保证大文件处理时内存安全同时兼顾写入效率。

25.8.24

  1. Django中目前有三个字段的值但是其中可能有None,需要检索模型BaseInfo的表中的这三个字段联合索引下的信息,应该如何高效的编写代码?
    查询时用isnull条件匹配值为None的字段。大致方法如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 动态构建查询参数
    query_kwargs = {}

    # 处理field1(值为None时用isnull=True,否则直接匹配值)
    if v1 is None:
    query_kwargs['field1__isnull'] = True
    else:
    query_kwargs['field1'] = v1

    # 处理field2
    if v2 is None:
    query_kwargs['field2__isnull'] = True
    else:
    query_kwargs['field2'] = v2

    # 处理field3
    if v3 is None:
    query_kwargs['field3__isnull'] = True
    else:
    query_kwargs['field3'] = v3

    # 执行查询(利用联合索引,效率最高)
    return BaseInfo.objects.filter(**query_kwargs)
  2. 对于通过某一字段外键连接在一起的两张表,可以使用__来进行“跨关联访问字段”:关联名称__字段名,因此treatments__duct等价于通过treatments关联找到Treatment模型的duct字段,则Q(treatments__duct=d)的作用是:通过关联关系筛选出 “有符合条件的Treatment记录” 的Patient
  3. Django为了区分”关联对象”和”关联id”(即“外键关联的字段名”和“实际存储的关联id值”,因为模型可以通过外键字段直接获取到完整的关联对象:treatment.patient_id,但是在数据库中存储的只是关联表的主键id值,而不是完整的关联对象。),这可以避免混淆对象访问和id存储,使在多表关联查询时,逻辑更为清晰。但是如果对于本身就带有_id后缀的字段,这样或许会发生冲突,存储的字段名将变为patient_id_id,这显然不是我们相要得到的,因此可以在定义的时候手动指定数据库列名:db_column=”…”
  4. 通过外键相连接的两个字段的类型一定要相同,可以通过如下sql命令来查看:show INDEX FROM table_name,包括但不限于类型相同,都是char类型的话要字符集和排序规则也都相同。
  5. django.db.utils.IntegrityError: (1452, 'Cannot add or update a child row: a foreign key constraint fails (eye301.#sql-1b14_47, CON STRAINT patientInfo_medicalh_patient_id_d2c054ad_fk_patientIn FOREIGN KEY (patient_id) REFERENCES patientinfo_baseinfo (patient_id))') 此类错误的,可能原因是由于通过外键连接的子表中存在主表中不存在的关联字段值,导致发生冲突。

25.8.25

  1. 错误太多太难处理了,只好重删表了,重新迁移

25.8.26

  1. 论文真的又多又杂,难搞啊:(

25.8.27

  1. list.append() takes no keyword arguments 的错因是:**调用列表的append()方法时使用了关键字参数,但该方法只接受位置参数,不支持关键字参数(如key=value形式)**。因此,不能直接向列表中直接传入键值对,因为键值对就是关键字参数的形式。
  2. Django中,bulk_create(objs, batch_size=None, ignore_conflicts=False)用于批量创建数据库记录,其核心参数是未保存的模型实例列表,可高效插入多条记录的场景。

25.8.30

  1. 注意Q对象的筛选只能筛选字符串(__contains__icontains),因为匹配的是字符串中的字符,或者可以使用__regex(正则表达式)。
  2. 注意python除法得到的将会是浮点数,而浮点数转换为字符串会带有小数点,例如:3.0->’3.0’(显而易见),因此这样匹配的不是’3’而是’3.0’,所以对于涉及到除法的整数映射关系,需要记得转换为int

25.9.8

  1. 在 Python 中使用eval()解析字符串”[{patient_id:1}]”会报错,具体错误为NameError: name ‘patient_id’ is not defined,字符串”[{patient_id:1}]”的语法在 Python 中存在歧义:

从结构上看,它试图表示一个包含字典的列表,类似[{key: value}]。
但字典的键patient_id被视为未定义的变量(而非字符串键)。
在 Python 中,字典的键若为标识符(如patient_id),会被解析为变量名,而不是字符串。由于patient_id未被定义,eval()执行时会找不到该变量,从而抛出NameError。解决方案是将字典的键改为字符串形式

25.9.9