【问题标题】:Destructuring dicts and objects in Python在 Python 中解构字典和对象
【发布时间】:2019-07-14 01:44:23
【问题描述】:

在 Javascript 中,我可以使用 destructuring 从一个 javascript 对象中提取我想要的属性。例如:

currentUser = {
  "id": 24,
  "name": "John Doe",
  "website": "http://mywebsite.com",
  "description": "I am an actor",
  "email": "example@example.com",
  "gender": "M",
  "phone_number": "+12345678",
  "username": "johndoe",
  "birth_date": "1991-02-23",
  "followers": 46263,
  "following": 345,
  "like": 204,
  "comments": 9
}

let { id, username } = this.currentUser;
console.log(id) // 24
console.log(username) //johndoe

对于 Python 字典和 Python 对象,我们在 Python 中有类似的东西吗? Python对象的Python方式示例:

class User:
    def __init__(self, id, name, website, description, email, gender, phone_number, username):
        self.id = id
        self.name = name
        self.website = website
        self.description = description
        self.email = email
        self.gender = gender
        self.phone_number = phone_number
        self.username = username
  
current_user = User(24, "Jon Doe", "http://mywebsite.com", "I am an actor", "example@example.com", "M", "+12345678", "johndoe")
    
# This is a pain
id = current_user.id
email = current_user.email
gender = current_user.gender
username = current_user.username
    
print(id, email, gender, username)

写这 4 行(如上例所述)与写一行(如下所述)从对象中获取我需要的值是一个真正的痛点。

(id, email, gender, username) = current_user

【问题讨论】:

  • print(currentUser['id']) 在 Python 中?
  • #e.dan 这个问题更广泛 - 涵盖 dict 和 objects。
  • 关于您的第二个问题 (User),请参阅 attrs 库:attrs.org/en/stable。或者有这种方法:self.__dict__.update(locals())
  • 为什么你首先需要那些“痛苦”的台词?您不能只访问对象中的值吗?

标签: python


【解决方案1】:

Python 的import 功能可以重新用于以方便(尽管可能不推荐)的方式解构字典和对象:

import sys, types

mydict = {'a':1,'b':2}

sys.modules["mydict"] = types.SimpleNamespace(**mydict)

from mydict import a, b

更严重的 hack 是上下文管理器,它临时将模块添加到 sys.modules,并确保模块的 __getattr__ 方法指向对象/dict 的 __getattribute__/__getitem__ 方法问题。

我们可以这样做:

with obj_as_module(mydict, "mydict_module"):
    from mydict_module import a, b

实施:

import sys, types
from contextlib import contextmanager

@contextmanager
def obj_as_module(obj, name):
    "Temporarily load an object/dict as a module, to import its attributes/values"
    module = types.ModuleType(name)
    get = obj.__getitem__ if isinstance(obj, dict) else obj.__getattribute__
    module.__getattr__ = lambda attr: get(attr) if attr != "__path__" else None
    try:
        if name in sys.modules:
            raise Exception(f"Name '{name}' already in sys.modules")
        else:
            sys.modules[name] = module
            yield module
    finally:
        if sys.modules[name] == module:
            del sys.modules[name]

测试:

mydict = {"a": 1, "b": 2}

with obj_as_module(mydict, "mydict"):
    from mydict import a, b

assert a + b == 3, "mydict" not in sys.modules

注意我不知道这会造成多大的破坏。

这是我第一次使用导入系统,只是一个有趣的练习。但我认为import 语句允许通过名称查找内容以及将结果绑定到该名称是一个有价值的观察。


以下是一些可能的扩展:

关键字参数

通过使用关键字参数,我们可以节省输入字符串引号以及一次性加载多个模块:

from contextlib import ExitStack

class kwargs_as_modules(ExitStack):
    "If you like 'obj_as_module', but want to save even more typing"
    def __init__(self, **kwargs):
        super().__init__()
        for name, obj in kwargs.items():
            self.enter_context(obj_as_module(obj, name))

测试:

myobj = types.SimpleNamespace(x=1, y=2)
mydict = {"a": 1, "b": 2}

with kwargs_as_modules(myobj=myobj, mydict=mydict):
    from mydict import a, b
    from myobj import x, y

assert a == x, b == y

类装饰器

为了处理类,我们可以使用装饰器:

def self_as_module(cls):
    "For those who like to write selfless methods"
    cls.as_module = lambda self: obj_as_module(self, "self")
    return cls

允许我们写self.as_module()

@self_as_module
class MyClass:
    def __init__(self):
        self.a = 1
        self.b = 2

    def check(self):
        with self.as_module():
            from self import a, b
        assert a + b == 3

MyClass().check()

【讨论】:

    【解决方案2】:

    通过这种方式,JavaScript 比 Python 拥有更好的对象域。您也可以构建一个方法或函数来复制功能,但 JavaScript 很容易做到。

    Python 上类似的东西可能是应用于字典(JSON 对象)的“打包/解包”功能。

    您可以在互联网上找到相关文档: https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/

    【讨论】:

      【解决方案3】:

      您可以使用.values() 方法解压并提取属性:

       currentUser = {
        "id": 24,
        "name": "John Doe",
        "website": "http://mywebsite.com",
        "description": "I am an actor",
        "email": "example@example.com",
        "gender": "M",
        "phone_number": "+12345678",
        "username": "johndoe",
        "birth_date": "1991-02-23",
        "followers": 46263,
        "following": 345,
        "like": 204,
        "comments": 9
      }
      
      id, _, _, _, _, _, _, username, *other = currentUser.values()
      
      print('distructuring:', { 'id': id, 'username': username })
      

      【讨论】:

      • 如果字典是动态创建的并且调用代码不知道插入顺序或字典项,会不会中断?
      • 可能,打破。你是对的,如果字典结构和顺序是静态的,这有效
      【解决方案4】:

      基于其他答案,我建议还使用 Python 的 dataclasses 并使用 __getitem__ 获取特定字段:

      from dataclasses import astuple, dataclass
      
      @dataclass
      class User:
          id: int
          name: str
          website: str
          description: str
          email: str
          gender: str
          phone_number: str
          username: str
          
          def __iter__(self):
              return iter(astuple(self))
          
          def __getitem__(self, keys):
              return iter(getattr(self, k) for k in keys)
              
      
      current_user = User(id=24, name="Jon Doe", website="http://mywebsite.com", description="I am an actor", email="example@example.com", gender="M", phone_number="+12345678", username="johndoe")
      
      # Access fields sequentially:
      id, _, email, *_ = current_user
      # Access fields out of order:
      id, email, gender, username = current_user["id", "email", "gender", "username"]
      

      【讨论】:

      • 仅在 Py3 中可用
      • 是的,好吧,是时候升级到 Python 3:“截至 2020 年 1 月 1 日,不会对 Python 2 进行新的错误报告、修复或更改,Python 2 不会支持时间更长。” python.org/doc/sunset-python-2
      • 我发现这种方法的一个缺点是astuple 调用也会将嵌套数据类转换为元组。
      【解决方案5】:

      您可以使用标准库中的operator 模块,如下所示:

      from operator import attrgetter
      id, email, gender, username = attrgetter('id', 'email', 'gender', 'username')(current_user)
      print(id, email, gender, username)
      

      如果你有一个像你的例子一样的字典

      currentUser = {
        "id": 24,
        "name": "John Doe",
        "website": "http://mywebsite.com",
        "description": "I am an actor",
        "email": "example@example.com",
        "gender": "M",
        "phone_number": "+12345678",
        "username": "johndoe",
        "birth_date": "1991-02-23",
        "followers": 46263,
        "following": 345,
        "like": 204,
        "comments": 9
      }
      

      只需使用itemgetter 而不是attrgetter

      from operator import itemgetter
      id, email, gender, username = itemgetter('id', 'email', 'gender', 'username')(currentUser)
      print(id, email, gender, username)
      

      【讨论】:

      • 这几乎可以工作。这仍然依赖于设置变量的顺序。即email, id = itemgetter('id', 'email')(currentUser) 为电子邮件和 ID 分配不正确的值。如示例所示,在 javascript 中,设置变量时的顺序无关紧要。
      • @M.Nar 正如你所看到的,这里的所有答案要么完全破坏一个对象(你不应该弄乱可能无法预测的顺序),要么假设你提供了你想要的字段并且以哪个顺序(不搞乱自己提供的顺序要容易得多)。好吧,Python 和 JS 是不同的语言,你不能争论。另一方面,Python 允许您重命名从对象接收到的值,而在 JS 中,名称将与原始对象中的名称相同。
      • @LevRomanov 注意,您可以在 JS 中重命名从对象接收到的值。语法为let { originalName: newName } = obj;
      • @LevRomanov 是否可以像我们在 JS 中那样使用 itemgetter 从嵌套的 dictionary 获取值
      【解决方案6】:

      首先不要把论点弄平。当您像使用 User 那样编写一个 8 元函数时,您肯定会犯错误,例如以错误的顺序传递参数。

      以下哪项会产生您想要的用户?

      1. User(24, "Jon Doe", "http://mywebsite.com", "I am an actor", "example@example.com", "M", "+12345678", "johndoe")
      2. User(24, "Jon Doe", "http://mywebsite.com", "I am an actor", "example@example.com", "+12345678", "M", "johndoe")

      不可能知道!如果你的函数需要一个描述符,你就没有这个问题 -

      class User:
        def __init__ (self, desc = {}):
          self.desc = desc # whitelist items, if necessary
        
        def __str__ (self):
          # invent our own "destructuring" syntax
          [ name, age, gender ] = \
            destructure(self.desc, 'name', 'age', 'gender')
      
          return f"{name} ({gender}) is {age} years old"
      
      # create users with a "descriptor"
      u = User({ 'age': 2, 'gender': 'M' })
      v = User({ 'gender': 'F', 'age': 3 })
      x = User({ 'gender': 'F', 'name': 'Alice', 'age': 4 })
      
      print(u) # None (M) is 2 years old
      print(v) # None (F) is 3 years old
      print(x) # Alice (F) is 4 years old
      

      我们可以将自己的destructure定义为-

      def destructure (d, *keys):
        return [ d[k] if k in d else None for k in keys ]
      

      这仍然可能导致长链,但顺序取决于调用者,因此它不像原始问题中的 8 元函数那样脆弱 -

      [ name, age, gender ] = \
        destructure(self.desc, 'name', 'age', 'gender')
      
      # works the same as
      
      [ gender, name, age ] = \
        destructure(self.desc, 'gender', 'name', 'age')
      

      另一种选择是使用关键字参数 -

      class User:
        def __init__ (self, **desc):
          self.desc = desc # whitelist items, if necessary
      
        def __str__ (self):
          [ name, age, gender ] = \
            destructure(self.desc, 'name', 'age', 'gender')
      
          return f"{name} ({gender}) is {age} years old"
      
      # create users with keyword arguments
      u = User(age = 2, gender = 'M')
      v = User(gender = 'F', age = 3)
      x = User(gender = 'F', name = 'Alice', age = 4)
      
      print(u) # None (M) is 2 years old
      print(v) # None (F) is 3 years old
      print(x) # Alice (F) is 4 years old
      

      【讨论】:

      • 感谢您的回答。您对带有关键字 args 的函数有什么看法,例如 User(name = "Loki", gender="M", age="23")? kwargs 方式是否优于 User({ 'name':'Loki', 'age': 23, 'gender': 'M' })?
      • 谢谢,唯一的缺点是,由于我们将所有内容都存储在 self.attr 中,人们可能无法直观地理解这个类,这也需要重写属性 getter 和 setter。
      • 使用带有可变集合的方法参数作为默认值是一个坏习惯。请改用None。如果您声明def __init__ (self, desc = {}): 并修改某些实例的user1.desc,例如user1 = User(); user1.desc['id'] = 1,则所有具有默认构造函数调用的新实例都将具有此值:user2 = User(); print(user2.desc) => {'id': 1}
      • 如果你有其他坏习惯,比如变异变量,这只是一个坏习惯:D 我使用函数式纪律,所以我没有和其他人一样的担忧。
      【解决方案7】:

      您可以实现__iter__ 方法来启用解包:

      class User:
        def __init__(self, **data):
          self.__dict__ = data
        def __iter__(self):
          yield from [getattr(self, i) for i in ('id', 'email', 'gender', 'username')]
      
      current_user = User(**currentUser)
      id, email, gender, username = current_user
      print([id, email, gender, username])
      

      输出:

      [24, 'example@example.com', 'M', 'johndoe']
      

      编辑:Python2 解决方案:

      class User:
        def __init__(self, **data):
          self.__dict__ = data
        def __iter__(self):
          for i in ('id', 'email', 'gender', 'username'):
            yield getattr(self, i)
      

      编辑 2:

      获取选择属性:

      class User:
        def __init__(self, **data):
           self.__dict__ = data
        def __getattr__(self, _vals):
           yield from [getattr(self, i) for i in _vals.split('_')]
      
      current_user = User(**currentUser)
      id, email, gender, username = current_user.id_email_gender_username
      id, gender = current_user.id_gender
      

      【讨论】:

      • 如果我只想从对象中解压缩有限的字段,这将不起作用。例如:id, email = current_user 这会抛出“ValueError: too many values to unpack”
      • @LokeshAgrawal 您可以使用一次性变量:id, email, *_ = current_user
      • 哦,好的,谢谢你的建议,我认为“yield from”的语法不正确?
      • @LokeshAgrawal 不,yield from 是正确的,它正在创建一个可以稍后迭代的生成器对象
      • @LokeshAgrawal 是的,但是,我为 Python2 添加了一个解决方案
      猜你喜欢
      • 2013-10-07
      • 2018-04-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-19
      • 2011-10-30
      • 2016-01-31
      • 2017-09-04
      相关资源
      最近更新 更多