Bagaimana Anda bisa mengatur atribut kelas dari argumen variabel (kwargs) di python

119

Misalkan saya memiliki kelas dengan konstruktor (atau fungsi lain) yang mengambil sejumlah variabel argumen dan kemudian menetapkannya sebagai atribut kelas secara kondisional.

Saya dapat mengaturnya secara manual, tetapi tampaknya parameter variabel cukup umum di python sehingga harus ada idiom umum untuk melakukan ini. Tapi saya tidak yakin bagaimana melakukan ini secara dinamis.

Saya punya contoh menggunakan eval, tapi itu hampir tidak aman. Saya ingin tahu cara yang tepat untuk melakukan ini - mungkin dengan lambda?

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])
fijiaaron
sumber
Sepertinya pertanyaan-pertanyaan ini serupa: stackoverflow.com/questions/3884612/… stackoverflow.com/questions/356718/… stackoverflow.com/questions/1446555/… jadi sepertinya yang saya inginkan adalah mungkin ini-- self .__ dict__ [key] = kwargs [key]
fijiaaron
Tidak terlalu relevan dengan pertanyaan Anda, tetapi Anda mungkin ingin memeriksa PEP8 untuk beberapa petunjuk tentang gaya Python konvensional.
Thomas Orozco
Ada pustaka fantastis untuk ini yang disebut attrs. cukup pip install attrs, hiasi kelas Anda dengan @attr.s, dan setel argumen sebagai a = attr.ib(); b = attr.ib()dll. Baca lebih lanjut di sini .
Adam Barnes
Apakah saya melewatkan sesuatu di sini? Anda masih perlu melakukan self.x = kwargs.get'x '] Anda membuka diri terhadap kesalahan ketik dari pemanggil Anda harus membuat klien dengan karakter ekstra instance = Class (** {}) Jika Anda tidak melompati lingkaran dengan the self.x = kwargs.get'x '] keduniawian, bukankah itu akan menggigitmu nanti? yaitu Alih-alih self.x, Anda akan berakhir dengan self .__ dict __ ['x'] ke bawah, kan? Atau getattr () Baik mengetik lebih dari diri sendiri.
JGFMK

Jawaban:

148

Anda dapat memperbarui __dict__atribut (yang mewakili atribut kelas dalam bentuk kamus) dengan argumen kata kunci:

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

maka kamu bisa:

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

dan dengan sesuatu seperti:

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

Anda bisa menyaring kunci terlebih dahulu (gunakan iteritemsbukan itemsjika Anda masih menggunakan Python 2.x).

fqxp
sumber
2
Lebih baik lagi jika Anda menggunakan self.__dict__.update(locals())untuk menyalin juga argumen posisi.
Giancarlo Sportelli
2
Saya pikir Anda akan membutuhkan ini sekarang .. kwargs [0] .items () daripada kwargs.iteritems () - (Saya menggunakan Python 3.6.5 pada saat penulisan)
JGFMK
@JGFMK Mengapa kwargs[0]bukan hanya kwargs? kwargsBahkan dapat memiliki kunci integer? Saya cukup yakin mereka harus string.
wjandrea
146

Anda dapat menggunakan setattr()metode ini:

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

Ada analoginya getattr() metode untuk mengambil atribut.

larsks
sumber
@larsks terima kasih, tetapi ada ide bagaimana kami hanya dapat membongkar kunci kamus? stackoverflow.com/questions/41792761/…
JinSnow
Apakah Anda perlu menggunakan .getattr()? Atau dapatkah Anda mengakses atribut dengan Foo.key?
Stevoisiak
@StevenVascellaro tentu saja dapat Anda gunakan Foo.attrname. Saya pikir saya hanya menunjukkan fakta bahwa getattrmetode itu ada. Ini juga berguna jika Anda ingin memberikan nilai default ketika atribut bernama tidak tersedia.
Larsks
3
Apa bedanya dengan jawaban yang diterima? . Apa pro dan kontra mereka?
Eduardo Pignatelli
15

Sebagian besar jawaban di sini tidak mencakup cara yang baik untuk menginisialisasi semua atribut yang diizinkan menjadi hanya satu nilai default. Jadi, untuk menambah jawaban yang diberikan oleh @fqxp dan @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)
Yiannis Kontochristopoulos
sumber
Saya rasa ini adalah jawaban paling lengkap karena inisialisasi kepada False. Poin bagus!
Kyrol
9

Saya mengusulkan variasi jawaban fqxp , yang, selain atribut yang diizinkan , memungkinkan Anda menetapkan nilai default untuk atribut:

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)

Ini adalah kode Python 3.x, untuk Python 2.x Anda membutuhkan setidaknya satu penyesuaian, iteritems()sebagai gantinya items().

mmj
sumber
1
Ini adalah jawaban paling fleksibel, merangkum pendekatan lain di utas ini. Ini menetapkan atribut, memungkinkan nilai default dan hanya menambahkan nama atribut yang diizinkan. Bekerja dengan baik dengan python 3.x seperti yang ditunjukkan di sini.
squarespiral
7

Namun varian lain berdasarkan jawaban luar biasa oleh mmj dan fqxp . Bagaimana jika kita mau

  1. Hindari melakukan hardcode daftar atribut yang diizinkan
  2. Secara langsung dan eksplisit menetapkan nilai default untuk setiap atribut dalam konstruktor
  3. Batasi kwargs ke atribut yang telah ditentukan sebelumnya dengan salah satunya
    • diam-diam menolak argumen yang tidak valid atau , sebagai alternatif,
    • menimbulkan kesalahan.

Yang saya maksud dengan "langsung" adalah menghindari default_attributeskamus asing .

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))

Bukan terobosan besar, tapi mungkin berguna bagi seseorang ...

EDIT: Jika kelas kita menggunakan @propertydekorator untuk merangkum atribut "dilindungi" dengan getter dan setter, dan jika kita ingin dapat mengatur properti ini dengan konstruktor kita, kita mungkin ingin memperluasallowed_keys daftar dengan nilai dari dir(self), sebagai berikut:

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

Kode di atas tidak termasuk

  • variabel tersembunyi apa pun dari dir()(pengecualian berdasarkan keberadaan "__"), dan
  • metode apa pun dir()yang namanya tidak ditemukan di akhir nama atribut (dilindungi atau sebaliknya) dari__dict__.keys() , sehingga kemungkinan hanya menyimpan metode dekorasi @property.

Hasil edit ini kemungkinan hanya valid untuk Python 3 dan yang lebih baru.

billjoie
sumber
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

Saya memanggil kelas SymbolDictkarena pada dasarnya itu adalah kamus yang beroperasi menggunakan simbol, bukan string. Dengan kata lain, yang Anda lakukan x.foobukannya x['foo']tetapi di balik selimut itu benar-benar terjadi hal yang sama.

wberry
sumber
2

Solusi berikut vars(self).update(kwargs)atau self.__dict__.update(**kwargs)tidak kuat, karena pengguna dapat memasukkan kamus apa pun tanpa pesan kesalahan. Jika saya perlu memeriksa apakah pengguna memasukkan tanda tangan berikut ('a1', 'a2', 'a3', 'a4', 'a5') solusinya tidak berfungsi. Selain itu, pengguna harus dapat menggunakan objek dengan melewatkan "parameter posisi" atau "parameter pasangan nilai kay".

Jadi saya menyarankan solusi berikut dengan menggunakan 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))
antonjs
sumber
1

Mereka mungkin solusi yang lebih baik tetapi yang terlintas dalam pikiran saya adalah:

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}
Tom
sumber
Yang saya cari adalah menetapkan atribut secara bersyarat berdasarkan validasi. Saya menyadari bahwa masalah dengan menggunakan kwargs adalah bahwa itu tidak memvalidasi (atau mendokumentasikan) atribut mana yang dapat diterima
fijiaaron
Ya saya menyadari jawaban @larsks lebih baik. Pelajari sesuatu yang baru setiap hari di SO!
Tom
1

yang ini paling mudah melalui larsks

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

contoh saya:

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
Oleg_Kornilov
sumber
dapatkah seseorang menjelaskan apa itu kwargs.items ()?
Oleg_Kornilov
kwargsadalah kamus argumen kata kunci, dan items()merupakan metode yang mengembalikan salinan daftar (key, value)pasangan kamus .
harryscholes
-1

Saya curiga akan lebih baik dalam banyak kasus untuk menggunakan args bernama (untuk kode pendokumentasian diri yang lebih baik) sehingga mungkin terlihat seperti ini:

class Foo:
    def setAll(a=None, b=None, c=None):
        for key, value in (a, b, c):
            if (value != None):
                settattr(self, key, value)
fijiaaron
sumber
Iterasi ini tidak bekerja:for key, value in (a, b, c)
rerx