คุณจะตั้งค่าแอตทริบิวต์คลาสจากอาร์กิวเมนต์ตัวแปร (kwargs) ใน python ได้อย่างไร


119

สมมติว่าฉันมีคลาสที่มีคอนสตรัคเตอร์ (หรือฟังก์ชันอื่น ๆ ) ที่รับอาร์กิวเมนต์จำนวนตัวแปรแล้วตั้งค่าเป็นคลาสแอ็ตทริบิวต์ตามเงื่อนไข

ฉันสามารถตั้งค่าได้ด้วยตนเอง แต่ดูเหมือนว่าพารามิเตอร์ตัวแปรจะเป็นเรื่องธรรมดามากพอใน python ที่ควรมีสำนวนทั่วไปสำหรับการทำเช่นนี้ แต่ฉันไม่แน่ใจว่าจะทำแบบไดนามิกได้อย่างไร

ฉันมีตัวอย่างการใช้ eval แต่แทบจะไม่ปลอดภัย ฉันต้องการทราบวิธีที่เหมาะสมในการทำเช่นนี้ - อาจใช้แลมด้า?

class Foo:
    def setAllManually(self, a=None, b=None, c=None):
        if a!=None: 
            self.a = a
        if b!=None:
            self.b = b
        if c!=None:
            self.c = c
    def setAllWithEval(self, **kwargs):
        for key in **kwargs:
            if kwargs[param] != None
                eval("self." + key + "=" + kwargs[param])

ดูเหมือนว่าคำถามเหล่านี้จะคล้ายกัน: stackoverflow.com/questions/3884612/... stackoverflow.com/questions/356718/... stackoverflow.com/questions/1446555/...จึงดูเหมือนว่าสิ่งที่ฉันต้องการอาจจะ this-- ตนเอง .__ dict__ [key] = kwargs [key]
fijiaaron

ไม่เกี่ยวข้องกับคำถามของคุณจริงๆ แต่คุณอาจต้องการตรวจสอบPEP8เพื่อดูคำแนะนำเล็กน้อยเกี่ยวกับสไตล์ Python แบบเดิม
Thomas Orozco

มีห้องสมุดที่ยอดเยี่ยมสำหรับสิ่งนี้ที่เรียกว่า attrs เพียงแค่pip install attrsตกแต่งชั้นเรียนของคุณด้วย@attr.sและตั้ง args เป็นa = attr.ib(); b = attr.ib()ฯลฯ อ่านรายละเอียดเพิ่มเติมที่นี่
Adam Barnes

ฉันขาดอะไรที่นี่? คุณยังต้องทำ self.x = kwargs.get'x '] คุณเปิดโอกาสให้ตัวเองพิมพ์ผิดจากผู้โทรคุณต้องสร้างไคลเอนต์ที่มีอินสแตนซ์ตัวอักษรพิเศษ = คลาส (** {}) หากคุณไม่กระโดดข้ามห่วงด้วย self.x = kwargs.get'x '] ความธรรมดามันจะไม่กัดคุณในภายหลังใช่ไหม เช่นแทนที่จะเป็น self.x คุณจะจบลงด้วย self. __ dict __ ['x'] ลงบรรทัดใช่ไหม? หรือ getattr () พิมพ์มากกว่าตนเอง
JGFMK

คำตอบ:


149

คุณสามารถอัปเดต__dict__แอตทริบิวต์ (ซึ่งแสดงถึงแอตทริบิวต์คลาสในรูปแบบของพจนานุกรม) ด้วยอาร์กิวเมนต์คำหลัก:

class Bar(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

จากนั้นคุณสามารถ:

>>> bar = Bar(a=1, b=2)
>>> bar.a
1

และด้วยสิ่งที่ชอบ:

allowed_keys = {'a', 'b', 'c'}
self.__dict__.update((k, v) for k, v in kwargs.items() if k in allowed_keys)

คุณสามารถกรองคีย์ล่วงหน้าได้ (ใช้iteritemsแทนitemsหากคุณยังใช้ Python 2.x)


2
จะดีกว่าถ้าคุณใช้self.__dict__.update(locals())เพื่อคัดลอกอาร์กิวเมนต์ตำแหน่งด้วย
Giancarlo Sportelli

2
ฉันคิดว่าคุณจะต้องใช้สิ่งนี้ในปัจจุบัน .. kwargs [0] .items () แทน kwargs.iteritems () - (ฉันใช้ Python 3.6.5 ในขณะที่เขียน)
JGFMK

@JGFMK ทำไมkwargs[0]แทนที่จะเป็นเพียงkwargs? สามารถkwargsมีคีย์จำนวนเต็มได้หรือไม่? ฉันค่อนข้างมั่นใจว่าพวกเขาต้องเป็นสตริง
wjandrea

146

คุณสามารถใช้setattr()วิธีการ:

class Foo:
  def setAllWithKwArgs(self, **kwargs):
    for key, value in kwargs.items():
      setattr(self, key, value)

มีgetattr()วิธีการที่คล้ายคลึงกันสำหรับการดึงแอตทริบิวต์


@larsks ขอบคุณ แต่มีความคิดอย่างไรว่าเราจะแกะเฉพาะคีย์พจนานุกรมได้อย่างไร? stackoverflow.com/questions/41792761/…
JinSnow

ต้องใช้.getattr()ไหม? หรือคุณสามารถเข้าถึงแอตทริบิวต์ด้วยFoo.key?
Stevoisiak

@StevenVascellaro แน่นอนว่าคุณสามารถFoo.attrnameใช้ได้ ฉันคิดว่าฉันแค่ชี้ให้เห็นความจริงที่ว่ามีgetattrวิธีการอยู่ นอกจากนี้ยังมีประโยชน์หากคุณต้องการระบุค่าเริ่มต้นเมื่อแอตทริบิวต์ที่ระบุชื่อไม่พร้อมใช้งาน
ลา

3
อะไรคือความแตกต่างกับคำตอบที่ยอมรับ? . ข้อดีข้อเสียของพวกเขาคืออะไร?
Eduardo Pignatelli

15

คำตอบส่วนใหญ่ที่นี่ไม่ครอบคลุมวิธีที่ดีในการเริ่มต้นแอตทริบิวต์ที่อนุญาตทั้งหมดให้เป็นค่าเริ่มต้นเพียงค่าเดียว ดังนั้นเพื่อเพิ่มคำตอบที่ให้โดย@fqxpและ@mmj :

class Myclass:

    def __init__(self, **kwargs):
        # all those keys will be initialized as class attributes
        allowed_keys = set(['attr1','attr2','attr3'])
        # initialize all allowed keys to false
        self.__dict__.update((key, False) for key in allowed_keys)
        # and update the given keys by their given values
        self.__dict__.update((key, value) for key, value in kwargs.items() if key in allowed_keys)

ผมคิดว่านี่เป็นคำตอบที่สมบูรณ์แบบที่สุดเนื่องจาก inizialization Falseไป จุดดี!
Kyrol

9

ฉันเสนอรูปแบบของคำตอบของ fqxpซึ่งนอกเหนือจากแอตทริบิวต์ที่อนุญาตให้คุณตั้งค่าเริ่มต้นสำหรับแอตทริบิวต์:

class Foo():
    def __init__(self, **kwargs):
        # define default attributes
        default_attr = dict(a=0, b=None, c=True)
        # define (additional) allowed attributes with no default value
        more_allowed_attr = ['d','e','f']
        allowed_attr = list(default_attr.keys()) + more_allowed_attr
        default_attr.update(kwargs)
        self.__dict__.update((k,v) for k,v in default_attr.items() if k in allowed_attr)

นี่คือรหัส Python 3.x สำหรับ Python 2.x คุณต้องมีการปรับเปลี่ยนอย่างน้อยหนึ่งรายการiteritems()แทนitems().


1
นี่คือคำตอบที่ยืดหยุ่นที่สุดโดยสรุปแนวทางอื่น ๆ ในชุดข้อความนี้ ตั้งค่าแอตทริบิวต์อนุญาตสำหรับค่าเริ่มต้นและเพิ่มเฉพาะชื่อแอตทริบิวต์ที่อนุญาต ทำงานได้ดีกับ python 3.x ดังที่แสดงไว้ที่นี่
squarespiral

7

แต่ตัวแปรอื่นขึ้นอยู่กับคำตอบที่ดีเยี่ยมโดยMMJและfqxp ถ้าเราต้องการ

  1. หลีกเลี่ยงการเข้ารหัสรายการแอตทริบิวต์ที่อนุญาต
  2. ตั้งค่าเริ่มต้นโดยตรงและชัดเจนสำหรับแต่ละแอตทริบิวต์ในตัวสร้าง
  3. จำกัด kwargs เฉพาะแอตทริบิวต์ที่กำหนดไว้ล่วงหน้าโดยอย่างใดอย่างหนึ่ง
    • ปฏิเสธข้อโต้แย้งที่ไม่ถูกต้องอย่างเงียบ ๆหรืออีกทางหนึ่งคือ
    • เพิ่มข้อผิดพลาด

โดย "โดยตรง" ฉันหมายถึงการหลีกเลี่ยงdefault_attributesพจนานุกรมที่ไม่เกี่ยวข้อง

class Bar(object):
    def __init__(self, **kwargs):

        # Predefine attributes with default values
        self.a = 0
        self.b = 0
        self.A = True
        self.B = True

        # get a list of all predefined values directly from __dict__
        allowed_keys = list(self.__dict__.keys())

        # Update __dict__ but only for keys that have been predefined 
        # (silently ignore others)
        self.__dict__.update((key, value) for key, value in kwargs.items() 
                             if key in allowed_keys)

        # To NOT silently ignore rejected keys
        rejected_keys = set(kwargs.keys()) - set(allowed_keys)
        if rejected_keys:
            raise ValueError("Invalid arguments in constructor:{}".format(rejected_keys))

ไม่ใช่การพัฒนาที่สำคัญ แต่อาจมีประโยชน์กับใครบางคน ...

แก้ไข: ถ้าคลาสของเราใช้@propertyมัณฑนากรในการห่อหุ้มแอตทริบิวต์ "ป้องกัน" ด้วย getters และ setters และถ้าเราต้องการให้สามารถตั้งค่าคุณสมบัติเหล่านี้ด้วยตัวสร้างของเราเราอาจต้องการขยายallowed_keysรายการด้วยค่าจากdir(self)ดังนี้:

allowed_keys = [i for i in dir(self) if "__" not in i and any([j.endswith(i) for j in self.__dict__.keys()])]

รหัสข้างต้นไม่รวม

  • ตัวแปรที่ซ่อนอยู่จากdir()(การยกเว้นตามการมีอยู่ของ "__") และ
  • เมธอดใด ๆdir()ที่ไม่พบชื่อในส่วนท้ายของชื่อแอตทริบิวต์ (ป้องกันหรืออื่น ๆ ) จาก__dict__.keys()ดังนั้นจึงน่าจะเก็บเฉพาะวิธีการตกแต่ง @property เท่านั้น

การแก้ไขนี้น่าจะใช้ได้กับ Python 3 ขึ้นไปเท่านั้น


2
class SymbolDict(object):
  def __init__(self, **kwargs):
    for key in kwargs:
      setattr(self, key, kwargs[key])

x = SymbolDict(foo=1, bar='3')
assert x.foo == 1

ฉันเรียกคลาสนี้SymbolDictเพราะโดยพื้นฐานแล้วมันเป็นพจนานุกรมที่ทำงานโดยใช้สัญลักษณ์แทนสตริง กล่าวอีกนัยหนึ่งคุณทำx.fooแทนที่จะทำx['foo']แต่ภายใต้ผ้าคลุมมันเป็นสิ่งเดียวกันที่เกิดขึ้นจริงๆ


2

แนวทางแก้ไขต่อไปนี้vars(self).update(kwargs)หรือself.__dict__.update(**kwargs)ไม่ได้ผลเนื่องจากผู้ใช้สามารถป้อนพจนานุกรมใดก็ได้โดยไม่มีข้อความแสดงข้อผิดพลาด หากฉันต้องการตรวจสอบว่าผู้ใช้ใส่ลายเซ็นต่อไปนี้ ('a1', 'a2', 'a3', 'a4', 'a5') วิธีแก้ปัญหาไม่ได้ผล ยิ่งไปกว่านั้นผู้ใช้ควรจะสามารถใช้ออบเจ็กต์ได้โดยส่ง "พารามิเตอร์ตำแหน่ง" หรือ "พารามิเตอร์คู่ค่าคายัค"

ดังนั้นฉันขอแนะนำวิธีแก้ปัญหาต่อไปนี้โดยใช้ metaclass

from inspect import Parameter, Signature

class StructMeta(type):
    def __new__(cls, name, bases, dict):
        clsobj = super().__new__(cls, name, bases, dict)
        sig = cls.make_signature(clsobj._fields)
        setattr(clsobj, '__signature__', sig)
        return clsobj

def make_signature(names):
    return Signature(
        Parameter(v, Parameter.POSITIONAL_OR_KEYWORD) for v in names
    )

class Structure(metaclass = StructMeta):
    _fields = []
    def __init__(self, *args, **kwargs):
        bond = self.__signature__.bind(*args, **kwargs)
        for name, val in bond.arguments.items():
            setattr(self, name, val)

if __name__ == 'main':

   class A(Structure):
      _fields = ['a1', 'a2']

   if __name__ == '__main__':
      a = A(a1 = 1, a2 = 2)
      print(vars(a))

      a = A(**{a1: 1, a2: 2})
      print(vars(a))

1

อาจเป็นทางออกที่ดีกว่า แต่สิ่งที่อยู่ในใจสำหรับฉันคือ:

class Test:
    def __init__(self, *args, **kwargs):
        self.args=dict(**kwargs)

    def getkwargs(self):
        print(self.args)

t=Test(a=1, b=2, c="cats")
t.getkwargs()


python Test.py 
{'a': 1, 'c': 'cats', 'b': 2}

สิ่งที่ฉันกำลังมองหาคือการตั้งค่าคุณสมบัติตามเงื่อนไขตามการตรวจสอบความถูกต้อง ฉันตระหนักว่าปัญหาในการใช้ kwargs คือไม่ได้ตรวจสอบความถูกต้อง (หรือเอกสาร) แอตทริบิวต์ใดที่ยอมรับได้
fijiaaron

ใช่ฉันรู้ว่าคำตอบของ @larsks ดีกว่า เรียนรู้สิ่งใหม่ ๆ ทุกวันที่ SO!
ทอม

1

อันนี้ง่ายที่สุดผ่านlarsks

class Foo:
    def setAllWithKwArgs(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

ตัวอย่างของฉัน:

class Foo:
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

door = Foo(size='180x70', color='red chestnut', material='oak')
print(door.size) #180x70

smb สามารถอธิบายได้ว่า kwargs.items () คืออะไร?
Oleg_Kornilov

kwargsเป็นพจนานุกรมของอาร์กิวเมนต์คำสำคัญและitems()เป็นวิธีการที่ส่งคืนสำเนาของรายการ(key, value)คู่ของพจนานุกรม
harryscholes

-1

ฉันสงสัยว่ามันอาจจะดีกว่าในกรณีส่วนใหญ่ที่จะใช้ชื่อ args (สำหรับรหัสการจัดทำเอกสารด้วยตนเองที่ดีขึ้น) ดังนั้นจึงอาจมีลักษณะดังนี้:

class Foo:
    def setAll(a=None, b=None, c=None):
        for key, value in (a, b, c):
            if (value != None):
                settattr(self, key, value)

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