การต่ออนุกรม Python namedtuple เป็น json


87

อะไรคือวิธีที่แนะนำในการทำให้อนุกรม a namedtupleถึง json โดยที่ชื่อเขตข้อมูลยังคงอยู่

การต่ออนุกรม a namedtupleถึง json จะส่งผลให้เฉพาะค่าที่ถูกทำให้เป็นอนุกรมและชื่อเขตข้อมูลจะสูญหายไปในการแปล ฉันต้องการให้เขตข้อมูลยังคงอยู่เมื่อ json-ized และด้วยเหตุนี้:

class foobar(namedtuple('f', 'foo, bar')):
    __slots__ = ()
    def __iter__(self):
        yield self._asdict()

ข้างต้นทำให้เป็นอนุกรมเป็น json ตามที่ฉันคาดหวังและทำงานเหมือนnamedtupleในที่อื่น ๆ ที่ฉันใช้ (การเข้าถึงแอตทริบิวต์ ฯลฯ ) ยกเว้นผลลัพธ์ที่ไม่ใช่ทูเพิลในขณะที่ทำซ้ำ (ซึ่งใช้ได้กับกรณีการใช้งานของฉัน)

อะไรคือ "วิธีที่ถูกต้อง" ในการแปลงเป็น json โดยที่ชื่อฟิลด์ยังคงอยู่


สำหรับ python 2.7: stackoverflow.com/questions/16938456/…
lowtech

คำตอบ:


55

นี้เป็นเรื่องยุ่งยากสวยเนื่องจากเป็นโรงงานที่ส่งกลับเป็นชนิดใหม่ที่ได้มาจากnamedtuple() tupleแนวทางหนึ่งคือให้คลาสของคุณสืบทอดจากUserDict.DictMixinแต่tuple.__getitem__มีการกำหนดไว้แล้วและคาดว่าจำนวนเต็มแสดงถึงตำแหน่งขององค์ประกอบไม่ใช่ชื่อของแอตทริบิวต์:

>>> f = foobar('a', 1)
>>> f[0]
'a'

หัวใจของมันคือ namedtuple เป็นแบบแปลก ๆ สำหรับ JSON เนื่องจากเป็นประเภทที่สร้างขึ้นเองซึ่งมีการแก้ไขชื่อคีย์เป็นส่วนหนึ่งของคำจำกัดความประเภทซึ่งแตกต่างจากพจนานุกรมที่ชื่อคีย์ถูกเก็บไว้ในอินสแตนซ์ สิ่งนี้จะป้องกันไม่ให้คุณ "ปัดเศษ" ชื่อทูเปิลเช่นคุณไม่สามารถถอดรหัสพจนานุกรมกลับไปที่ชื่อไฟล์ทูปเปิ้ลโดยไม่มีข้อมูลอื่น ๆ เช่นเครื่องหมายประเภทเฉพาะแอปในคำสั่ง{'a': 1, '#_type': 'foobar'}ซึ่งค่อนข้างแฮ็ก

สิ่งนี้ไม่เหมาะอย่างยิ่ง แต่หากคุณต้องการเพียงแค่เข้ารหัสชื่อไฟล์ลงในพจนานุกรมอีกวิธีหนึ่งคือการขยายหรือแก้ไขตัวเข้ารหัส JSON ของคุณเป็นกรณีพิเศษประเภทเหล่านี้ นี่คือตัวอย่างของ subclassing json.JSONEncoderหลาม สิ่งนี้ช่วยแก้ปัญหาในการตรวจสอบว่ามีการแปลงชื่อรายการที่ซ้อนกันเป็นพจนานุกรมอย่างถูกต้อง:

from collections import namedtuple
from json import JSONEncoder

class MyEncoder(JSONEncoder):

    def _iterencode(self, obj, markers=None):
        if isinstance(obj, tuple) and hasattr(obj, '_asdict'):
            gen = self._iterencode_dict(obj._asdict(), markers)
        else:
            gen = JSONEncoder._iterencode(self, obj, markers)
        for chunk in gen:
            yield chunk

class foobar(namedtuple('f', 'foo, bar')):
    pass

enc = MyEncoder()
for obj in (foobar('a', 1), ('a', 1), {'outer': foobar('x', 'y')}):
    print enc.encode(obj)

{"foo": "a", "bar": 1}
["a", 1]
{"outer": {"foo": "x", "bar": "y"}}

12
หัวใจของมันคือ namedtuple เป็นแบบแปลก ๆ สำหรับ JSON เนื่องจากเป็นประเภทที่สร้างขึ้นเองซึ่งมีการแก้ไขชื่อคีย์เป็นส่วนหนึ่งของคำจำกัดความประเภทซึ่งแตกต่างจากพจนานุกรมที่ชื่อคีย์ถูกเก็บไว้ในอินสแตนซ์ ความคิดเห็นที่ลึกซึ้งมาก ฉันไม่ได้คิดเกี่ยวกับเรื่องนั้น ขอบคุณ. ฉันชอบ namestuples เนื่องจากมีโครงสร้างที่ไม่เปลี่ยนรูปที่ดีพร้อมความสะดวกในการตั้งชื่อแอตทริบิวต์ ฉันจะยอมรับคำตอบของคุณ ต้องบอกว่ากลไกการทำให้เป็นอนุกรมของ Java ช่วยให้สามารถควบคุมวิธีการทำให้วัตถุเป็นอนุกรมได้มากขึ้นและฉันอยากรู้ว่าทำไมตะขอดังกล่าวดูเหมือนจะไม่มีอยู่ใน Python
calvinkrishy

นั่นเป็นแนวทางแรกของฉัน แต่มันไม่ได้ผลจริง (สำหรับฉันแล้ว)
zeekay

1
>>> json.dumps(foobar('x', 'y'), cls=MyEncoder) <<< '["x", "y"]'
zeekay

19
อ่าใน python 2.7+ _iterencode ไม่ใช่เมธอดของ JSONEncoder อีกต่อไป
zeekay

2
@calvin ขอบคุณฉันพบว่า namedtuple มีประโยชน์เช่นกันหวังว่าจะมีทางออกที่ดีกว่าในการเข้ารหัสซ้ำเป็น JSON @zeekay ใช่ดูเหมือนว่าใน 2.7+ พวกเขาซ่อนไว้ดังนั้นจึงไม่สามารถลบล้างได้อีกต่อไป ที่น่าผิดหวังคือ
samplebias

77

หากเป็นเพียงหนึ่งเดียวที่namedtupleคุณต้องการทำให้เป็นอนุกรมการใช้_asdict()วิธีนี้จะได้ผล (กับ Python> = 2.7)

>>> from collections import namedtuple
>>> import json
>>> FB = namedtuple("FB", ("foo", "bar"))
>>> fb = FB(123, 456)
>>> json.dumps(fb._asdict())
'{"foo": 123, "bar": 456}'

4
ฉันได้รับ AttributeError: วัตถุ 'FB' ไม่มีแอตทริบิวต์ ' dict ' เมื่อเรียกใช้รหัสนั้นใน Python 2.7 (x64) บน Windows อย่างไรก็ตาม fb._asdict () ใช้งานได้ดี
geographika

5
fb._asdict()หรือvars(fb)จะดีกว่า
jpmc26

1
@ jpmc26: คุณไม่สามารถใช้varsกับวัตถุโดยไม่มีไฟล์__dict__.
Rufflewind

@Rufflewind คุณไม่สามารถใช้__dict__กับสิ่งเหล่านั้นได้เช่นกัน =)
jpmc26

4
ใน python 3 __dict__ถูกลบออก _asdictดูเหมือนจะใช้ได้ทั้งสองอย่าง
Andy Hayden

21

ดูเหมือนว่าคุณเคยสามารถซับคลาสsimplejson.JSONEncoderเพื่อให้ทำงานนี้ได้ แต่ด้วยโค้ด simplejson ล่าสุดนั้นไม่เป็นเช่นนั้นอีกต่อไป: คุณต้องแก้ไขโค้ดโครงการจริงๆ ฉันเห็นเหตุผลว่าทำไม simplejson ไม่ควรสนับสนุน namedtuples ดังนั้นฉันคดเคี้ยวโครงการสนับสนุน namedtuple เพิ่มและฉันขณะนี้กำลังรอสำหรับสาขาของฉันที่จะถูกดึงกลับเข้ามาในโครงการหลัก หากคุณต้องการการแก้ไขตอนนี้เพียงแค่ดึงจากส้อมของฉัน

แก้ไข : หน้าตาเหมือนรุ่นล่าสุดของsimplejsonตอนนี้รองรับกับตัวเลือกที่เริ่มต้นที่namedtuple_as_objectTrue


3
การแก้ไขของคุณคือคำตอบที่ถูกต้อง simplejson ทำให้เป็นอนุกรม namestuples แตกต่างกัน (ความคิดเห็นของฉัน: ดีกว่า) มากกว่า json สิ่งนี้ทำให้รูปแบบ: "try: import simplejson เป็น json ยกเว้น: import json" มีความเสี่ยงเนื่องจากคุณอาจมีพฤติกรรมที่แตกต่างกันในบางเครื่องขึ้นอยู่กับว่ามีการติดตั้ง simplejson หรือไม่ ด้วยเหตุนั้นตอนนี้ฉันจึงต้องการ simplejson ในไฟล์ติดตั้งจำนวนมากและละเว้นจากรูปแบบนั้น
marr75

1
@ marr75 - Ditto for ujsonซึ่งเป็นเรื่องแปลกและคาดเดาไม่ได้มากขึ้นในกรณีขอบเช่นนี้ ...
mac

ฉันสามารถรับ namestuple แบบเรียกซ้ำต่อเนื่องเป็น (พิมพ์สวย) โดยใช้: simplejson.dumps(my_tuple, indent=4)
KFL

5

ฉันเขียนห้องสมุดเพื่อทำสิ่งนี้: https://github.com/ltworf/typedload

สามารถเปลี่ยนจากและไปยังชื่อทูเพิลและย้อนกลับได้

สนับสนุนโครงสร้างที่ซ้อนกันค่อนข้างซับซ้อนมีรายการชุด enums สหภาพแรงงานค่าเริ่มต้น ควรครอบคลุมกรณีที่พบบ่อยที่สุด

แก้ไข: ไลบรารียังรองรับคลาส dataclass และ attr


2

มันจะแปลงข้อมูล namedTuple ซ้ำเป็น json

print(m1)
## Message(id=2, agent=Agent(id=1, first_name='asd', last_name='asd', mail='2@mai.com'), customer=Customer(id=1, first_name='asd', last_name='asd', mail='2@mai.com', phone_number=123123), type='image', content='text', media_url='h.com', la=123123, ls=4512313)

def reqursive_to_json(obj):
    _json = {}

    if isinstance(obj, tuple):
        datas = obj._asdict()
        for data in datas:
            if isinstance(datas[data], tuple):
                _json[data] = (reqursive_to_json(datas[data]))
            else:
                 print(datas[data])
                _json[data] = (datas[data])
    return _json

data = reqursive_to_json(m1)
print(data)
{'agent': {'first_name': 'asd',
'last_name': 'asd',
'mail': '2@mai.com',
'id': 1},
'content': 'text',
'customer': {'first_name': 'asd',
'last_name': 'asd',
'mail': '2@mai.com',
'phone_number': 123123,
'id': 1},
'id': 2,
'la': 123123,
'ls': 4512313,
'media_url': 'h.com',
'type': 'image'}

1
+1 ฉันทำเกือบเหมือนกัน แต่การกลับมาของคุณคือ dict ไม่ใช่ json คุณต้องมีคำว่า "not" และหากค่าในวัตถุของคุณเป็นบูลีนค่านั้นจะไม่ถูกแปลงเป็นจริงฉันคิดว่าการแปลงเป็น dict จะปลอดภัยกว่าแล้วใช้ json.dumps เพื่อแปลงเป็น json
Fred Laurent

2

มีวิธีที่สะดวกกว่าคือการใช้มัณฑนากร (ใช้ช่องที่มีการป้องกัน_fields)

Python 2.7+:

import json
from collections import namedtuple, OrderedDict

def json_serializable(cls):
    def as_dict(self):
        yield OrderedDict(
            (name, value) for name, value in zip(
                self._fields,
                iter(super(cls, self).__iter__())))
    cls.__iter__ = as_dict
    return cls

#Usage:

C = json_serializable(namedtuple('C', 'a b c'))
print json.dumps(C('abc', True, 3.14))

# or

@json_serializable
class D(namedtuple('D', 'a b c')):
    pass

print json.dumps(D('abc', True, 3.14))

Python 3.6.6+:

import json
from typing import TupleName

def json_serializable(cls):
    def as_dict(self):
        yield {name: value for name, value in zip(
            self._fields,
            iter(super(cls, self).__iter__()))}
    cls.__iter__ = as_dict
    return cls

# Usage:

@json_serializable
class C(NamedTuple):
    a: str
    b: bool
    c: float

print(json.dumps(C('abc', True, 3.14))

อย่าทำอย่างนั้นพวกเขาเปลี่ยน API ภายในตลอดเวลา ไลบรารี typedload ของฉันมีหลายกรณีสำหรับเวอร์ชัน py ที่แตกต่างกัน
LtWorf

ใช่มันชัดเจน อย่างไรก็ตามไม่มีใครควรย้ายไปใช้ Python เวอร์ชันใหม่โดยไม่ต้องทดสอบ และวิธีแก้ปัญหาอื่น ๆ ใช้_asdictซึ่งเป็นสมาชิกชั้นเรียนที่ "ได้รับการป้องกัน" เช่นกัน
Dmitry T.

1
LtWorf ห้องสมุดของคุณคือ GPL และใช้ไม่ได้กับ Frozensets
Thomas Grainger

2
@LtWorf ห้องสมุดของคุณยังใช้_fields;-) github.com/ltworf/typedload/blob/master/typedload/datadumper.py ซึ่งเป็นส่วนหนึ่งของ API สาธารณะของ namedtuple จริงๆแล้ว: docs.python.org/3.7/library/…ผู้คนสับสน ขีดล่าง (ไม่น่าแปลกใจ!) เป็นการออกแบบที่ไม่ดี แต่ฉันไม่รู้ว่าพวกเขามีทางเลือกอะไร
quant_dev

1
สิ่งใด? เมื่อไหร่? คุณสามารถอ้างอิงบันทึกประจำรุ่นได้ไหม
quant_dev

2

jsonplusห้องสมุดให้ serializer สำหรับกรณี NamedTuple ใช้โหมดความเข้ากันได้เพื่อส่งออกออบเจ็กต์ธรรมดาหากจำเป็น แต่ชอบค่าเริ่มต้นเนื่องจากมีประโยชน์ในการถอดรหัสกลับ


ฉันดูวิธีแก้ปัญหาอื่น ๆ ที่นี่และพบว่าการเพิ่มการอ้างอิงนี้ช่วยให้ฉันประหยัดเวลาได้มาก โดยเฉพาะอย่างยิ่งเนื่องจากฉันมีรายการ NamedTuples ที่ฉันต้องส่งผ่านเป็น json ในเซสชัน jsonplus ช่วยให้คุณได้รับรายชื่อสิ่งที่มีชื่อเข้าและออกจาก json โดยทั่วไป.dumps()และ.loads()ไม่มีการกำหนดค่ามันก็ใช้งานได้
Rob

1

เป็นไปไม่ได้ที่จะทำให้ชื่อไฟล์เป็นลำดับอย่างถูกต้องกับไลบรารี python json จะเห็น tuples เป็นรายการเสมอและเป็นไปไม่ได้ที่จะแทนที่ serializer เริ่มต้นเพื่อเปลี่ยนแปลงพฤติกรรมนี้ จะแย่ยิ่งกว่าถ้าวัตถุซ้อนกัน

ดีกว่าที่จะใช้ไลบรารีที่แข็งแกร่งกว่าเช่นorjson :

import orjson
from typing import NamedTuple

class Rectangle(NamedTuple):
    width: int
    height: int

def default(obj):
    if hasattr(obj, '_asdict'):
        return obj._asdict()

rectangle = Rectangle(width=10, height=20)
print(orjson.dumps(rectangle, default=default))

=>

{
    "width":10,
    "height":20
}

1
ฉันก็เป็นแฟนorjsonด้วย
CircleOnCircles

0

นี่เป็นคำถามเก่า อย่างไรก็ตาม:

ข้อเสนอแนะสำหรับผู้ที่มีคำถามเดียวกันให้พิจารณาอย่างรอบคอบเกี่ยวกับการใช้คุณลักษณะส่วนตัวหรือคุณลักษณะภายในNamedTupleเนื่องจากมีมาก่อนและจะเปลี่ยนแปลงอีกครั้ง

ตัวอย่างเช่นหากคุณNamedTupleเป็นวัตถุที่มีค่าคงที่และคุณสนใจที่จะทำให้มันเป็นอนุกรมเท่านั้นและไม่ใช่ในกรณีที่มันซ้อนอยู่ในวัตถุอื่นคุณสามารถหลีกเลี่ยงปัญหาที่จะเกิดขึ้นจาก__dict__การถูกลบหรือ_as_dict()เปลี่ยนแปลงและทำบางสิ่งเช่น (และใช่นี่คือ Python 3 เพราะคำตอบนี้มีไว้สำหรับปัจจุบัน):

from typing import NamedTuple

class ApiListRequest(NamedTuple):
  group: str="default"
  filter: str="*"

  def to_dict(self):
    return {
      'group': self.group,
      'filter': self.filter,
    }

  def to_json(self):
    return json.dumps(self.to_dict())

ฉันพยายามใช้defaultkwarg dumpsที่to_dict()โทรได้เพื่อทำการโทรหากมี แต่ไม่ได้รับการเรียกเนื่องจากNamedTupleสามารถแปลงเป็นรายการได้


3
_asdictเป็นส่วนหนึ่งของ namedtuple public API พวกเขาอธิบายเหตุผลในการขีดล่างdocs.python.org/3.7/library/… "นอกเหนือจากวิธีการที่สืบทอดมาจากทูเปิลชื่อทูเปิลสนับสนุนวิธีการเพิ่มเติมสามวิธีและสองแอตทริบิวต์เพื่อป้องกันความขัดแย้งกับชื่อเขตข้อมูลชื่อเมธอดและแอตทริบิวต์ เริ่มต้นด้วยขีดล่าง "
quant_dev

@quant_dev ขอบคุณฉันไม่เห็นคำอธิบายนั้น ไม่ใช่การรับประกันความเสถียรของ API แต่ช่วยให้วิธีการเหล่านั้นน่าเชื่อถือมากขึ้น ฉันชอบความสามารถในการอ่าน to_dict ที่ชัดเจน แต่ฉันเห็นว่าการติดตั้ง _as_dict ใหม่
dlamblin

0

นี่คือสิ่งที่ฉันใช้กับปัญหา มันทำให้เป็นอนุกรมของ NamedTuple ดูแล NamedTuples และ Lists ที่พับไว้ข้างใน

def recursive_to_dict(obj: Any) -> dict:
_dict = {}

if isinstance(obj, tuple):
    node = obj._asdict()
    for item in node:
        if isinstance(node[item], list): # Process as a list
            _dict[item] = [recursive_to_dict(x) for x in (node[item])]
        elif getattr(node[item], "_asdict", False): # Process as a NamedTuple
            _dict[item] = recursive_to_dict(node[item])
        else: # Process as a regular element
            _dict[item] = (node[item])
return _dict

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.