Bagaimana saya menghindari "self.x = x; self.y = y; self.z = z ”pola di __init__?

170

Saya melihat pola suka

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

cukup sering, sering dengan lebih banyak parameter. Apakah ada cara yang baik untuk menghindari pengulangan yang membosankan seperti ini? Haruskah kelas mewarisi dari namedtuplegantinya?

MaxB
sumber
31
Tidak semua penerimaan itu buruk. Perlu diingat bahwa model kelas Python tidak menyertakan definisi eksplisit dari atribut instance, jadi penugasan ini adalah yang setara dengan mendokumentasikan diri.
chepner
4
@ chepner: Ya, tidak memerlukan definisi eksplisit. Anda dapat menggunakan __slots__untuk tujuan itu ; itu agak unpythonic (lebih verbose untuk mendapatkan penghematan memori), tapi saya suka sebagian besar untuk menghindari risiko auto-vivifying atribut yang sama sekali baru jika saya mengetik nama.
ShadowRanger
2
Editor yang baik akan memiliki template. Anda mengetik ini <shortcut> x, y, z): <shortcut>dan selesai.
Gerenuk
3
Namedtuples mengagumkan, jika Anda menginginkan objek nilai yang tidak dapat diubah. Jika Anda ingin kelas reguler yang bisa berubah, Anda tidak bisa menggunakannya.
RemcoGerlich
4
"Jangan" adalah pilihan yang baik, opsi apa pun yang tersedia akan mematikan tanda tangan metode (dan karenanya berpotensi seluruh antarmuka). Selain itu, jika kelas Anda memiliki jumlah bidang yang tak tertahankan untuk diinisialisasi, Anda mungkin ingin mempertimbangkan untuk membaginya.
Kroltan

Jawaban:

87

Sunting: Jika Anda memiliki python 3.7+ gunakan dataclasses

Solusi dekorator yang menyimpan tanda tangan:

import decorator
import inspect
import sys


@decorator.decorator
def simple_init(func, self, *args, **kws):
    """
    @simple_init
    def __init__(self,a,b,...,z)
        dosomething()

    behaves like

    def __init__(self,a,b,...,z)
        self.a = a
        self.b = b
        ...
        self.z = z
        dosomething()
    """

    #init_argumentnames_without_self = ['a','b',...,'z']
    if sys.version_info.major == 2:
        init_argumentnames_without_self = inspect.getargspec(func).args[1:]
    else:
        init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]

    positional_values = args
    keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
    attribute_values = positional_values + keyword_values_in_correct_order

    for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):
        setattr(self,attribute_name,attribute_value)

    # call the original __init__
    func(self, *args, **kws)


class Test():
    @simple_init
    def __init__(self,a,b,c,d=4):
        print(self.a,self.b,self.c,self.d)

#prints 1 3 2 4
t = Test(1,c=2,b=3)
#keeps signature
#prints ['self', 'a', 'b', 'c', 'd']
if sys.version_info.major == 2:
    print(inspect.getargspec(Test.__init__).args)
else:
    print(inspect.signature(Test.__init__))
Siphor
sumber
2
jawaban yang bagus, tetapi tidak akan bekerja dengan python2.7: tidaksignature
MaxB
3
@alexis dekorator "decorator.decorator" secara otomatis membungkus fungsi
Siphor
4
Saya agak bingung apakah akan mencintai ini atau membencinya. Saya menghargai menjaga tanda tangan.
Kyle Strand
14
"... Eksplisit lebih baik daripada implisit. Sederhana lebih baik daripada kompleks. ..." -Zen of Python
Jack Stout
9
-1 Terus terang ini mengerikan. Saya tidak tahu apa yang dilakukan kode ini sekilas, dan ini benar-benar sepuluh kali jumlah kode. Menjadi pintar memang terasa keren, tapi ini adalah penyalahgunaan kepintaran Anda yang jelas.
Ian Newson
108

Penafian: Tampaknya beberapa orang khawatir tentang menghadirkan solusi ini, jadi saya akan memberikan penafian yang sangat jelas. Anda sebaiknya tidak menggunakan solusi ini. Saya hanya menyediakannya sebagai informasi, sehingga Anda tahu bahasanya mampu melakukannya. Jawaban lainnya hanya menunjukkan kemampuan bahasa, bukan mendukung menggunakannya dengan cara ini.


Tidak ada yang salah dengan menyalin parameter ke atribut secara eksplisit. Jika Anda memiliki terlalu banyak parameter dalam ctor, terkadang dianggap sebagai bau kode dan mungkin Anda harus mengelompokkan params ini menjadi objek yang lebih sedikit. Di lain waktu, itu perlu dan tidak ada yang salah dengan itu. Bagaimanapun, melakukannya secara eksplisit adalah cara untuk melakukannya.

Namun, karena Anda bertanya BAGAIMANA itu bisa dilakukan (dan bukan apakah itu harus dilakukan), maka salah satu solusinya adalah ini:

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

a = A(l=1, d=2)
a.l # will return 1
a.d # will return 2
gruszczy
sumber
16
jawaban yang bagus +1 ... walaupun self.__dict__.update(kwargs)mungkin sedikit lebih pythonic
Joran Beasley
44
Masalah dengan pendekatan ini adalah bahwa tidak ada catatan dari apa argumen A.__init__sebenarnya mengharapkan, dan tidak ada kesalahan memeriksa nama argumen yang salah ketik.
Maks
7
@JoranBeasley Memperbarui kamus contoh secara membabi buta dengan kwargsmembiarkan Anda membuka setara dengan serangan injeksi SQL. Jika objek Anda memiliki metode yang dinamai my_methoddan Anda meneruskan argumen yang dinamai my_methodke konstruktor, maka update()kamus, Anda hanya menimpa metode tersebut.
Pedro
3
Seperti yang orang lain katakan, sarannya adalah gaya pemrograman yang benar-benar buruk. Itu menyembunyikan informasi penting. Anda dapat menunjukkannya, tetapi Anda harus secara jelas mencegah OP untuk menggunakannya.
Gerenuk
3
@Pedro Apakah ada perbedaan semantik antara sintaksis gruzczy dan JoranBeasley?
gerrit
29

Seperti yang telah disebutkan orang lain, pengulangan itu tidak buruk, tetapi dalam beberapa kasus, namesupuple sangat cocok untuk jenis masalah ini. Ini menghindari penggunaan lokal () atau kwargs, yang biasanya merupakan ide yang buruk.

from collections import namedtuple
# declare a new object type with three properties; x y z
# the first arg of namedtuple is a typename
# the second arg is comma-separated or space-separated property names
XYZ = namedtuple("XYZ", "x, y, z")

# create an object of type XYZ. properties are in order
abc = XYZ("one", "two", 3)
print abc.x
print abc.y
print abc.z

Saya telah menemukan penggunaan terbatas untuk itu, tetapi Anda dapat mewarisi nama named seperti dengan objek lain (contoh lanjutan):

class MySuperXYZ(XYZ):
    """ I add a helper function which returns the original properties """
    def properties(self):
        return self.x, self.y, self.z

abc2 = MySuperXYZ(4, "five", "six")
print abc2.x
print abc2.y
print abc2.z
print abc2.properties()
Script Shell Kecil
sumber
5
Ini adalah tupel, sehingga propertiesmetode Anda dapat ditulis sebagai adil return tuple(self), yang lebih dapat dipertahankan jika di masa mendatang lebih banyak bidang ditambahkan ke definisi nameduple.
PaulMcG
1
Juga, string deklarasi namedtuple Anda tidak memerlukan koma di antara nama field, juga XYZ = namedtuple("XYZ", "x y z")berfungsi.
PaulMcG
Terima kasih @PaulMcGuire. Saya sedang mencoba memikirkan add-on yang sangat sederhana untuk menunjukkan warisan dan jenis spasi pada itu. Anda 100% benar dan ini adalah singkatan yang bagus dengan objek warisan lainnya, juga! Saya menyebutkan nama-nama bidang dapat berupa koma atau dipisahkan dengan ruang - Saya lebih suka CSV dari kebiasaan
A Small Shell Script
1
Saya sering menggunakan namedtuples untuk tujuan yang tepat ini, terutama dalam kode matematika di mana fungsi mungkin sangat parametris dan memiliki banyak koefisien yang hanya masuk akal bersama.
detly
Masalahnya namedtupleadalah mereka hanya baca. Anda tidak bisa melakukan abc.x += 1hal seperti itu.
hamstergene
29

eksplisit lebih baik daripada implisit ... jadi pastikan Anda bisa membuatnya lebih ringkas:

def __init__(self,a,b,c):
    for k,v in locals().items():
        if k != "self":
             setattr(self,k,v)

Pertanyaan yang lebih baik adalah seharusnya Anda?

... yang mengatakan jika Anda ingin nama tuple saya akan merekomendasikan menggunakan namatuple (ingat tuple memiliki kondisi tertentu yang menyertainya) ... mungkin Anda ingin memesan atau bahkan hanya perintah ...

Joran Beasley
sumber
Maka objek tersebut akan memerlukan pengumpulan sampah siklik karena memiliki atribut sendiri
John La Rooy
3
@bernie (atau ini bemie?), terkadang sulit untuk mengatasinya
cat
4
Untuk tes yang sedikit lebih efisien, if k != "self":bisa diubah menjadi if v is not self:, tes identitas murah, daripada perbandingan string. Saya kira secara teknis __init__bisa disebut kedua kalinya setelah konstruksi dan selfdinyatakan sebagai argumen berikutnya, tetapi saya benar-benar tidak ingin berpikir monster seperti apa yang akan melakukan itu. :-)
ShadowRanger
Itu bisa dibuat menjadi fungsi yang mengambil nilai kembali dari locals: set_fields_from_locals(locals()). Maka itu tidak lebih dari solusi berbasis dekorator yang lebih ajaib.
Lii
20

Untuk memperluas gruszczyjawaban s, saya telah menggunakan pola seperti:

class X:
    x = None
    y = None
    z = None
    def __init__(self, **kwargs):
        for (k, v) in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)
            else:
                raise TypeError('Unknown keyword argument: {:s}'.format(k))

Saya suka metode ini karena:

  • menghindari pengulangan
  • tahan terhadap kesalahan ketik saat membangun objek
  • bekerja dengan baik dengan subclassing (bisa saja super().__init(...))
  • memungkinkan dokumentasi atribut pada tingkat kelas (di mana mereka berada) daripada di X.__init__

Sebelum Python 3.6, ini tidak memberikan kontrol atas urutan atribut ditetapkan, yang bisa menjadi masalah jika beberapa atribut adalah properti dengan setter yang mengakses atribut lainnya.

Mungkin bisa ditingkatkan sedikit, tapi saya satu-satunya pengguna kode saya sendiri jadi saya tidak khawatir tentang segala bentuk sanitasi input. Mungkin AttributeErrorakan lebih tepat.

gerrit
sumber
10

Anda juga bisa:

locs = locals()
for arg in inspect.getargspec(self.__init__)[0][1:]:
    setattr(self, arg, locs[arg])

Tentu saja, Anda harus mengimpor inspectmodul.

zondo
sumber
8

Ini adalah solusi tanpa impor tambahan.

Fungsi pembantu

Fungsi pembantu kecil membuatnya lebih nyaman dan dapat digunakan kembali:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    self = local_name_space.pop('self')
    for name, value in local_name_space.items():
        setattr(self, name, value)

Aplikasi

Anda perlu menyebutnya dengan locals():

class A:
    def __init__(self, x, y, z):
        auto_init(locals())

Uji

a = A(1, 2, 3)
print(a.__dict__)

Keluaran:

{'y': 2, 'z': 3, 'x': 1}

Tanpa berubah locals()

Jika Anda tidak ingin mengubah, locals()gunakan versi ini:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    for name, value in local_name_space.items():
        if name != 'self': 
            setattr(local_name_space['self'], name, value)
Mike Müller
sumber
docs.python.org/2/library/functions.html#locals locals() tidak boleh dimodifikasi (ini dapat memengaruhi juru bahasa, dalam kasus Anda, menghapus selfdari ruang lingkup fungsi panggilan)
MaxB
@ MaxB Dari dokumen yang Anda kutip: ... perubahan mungkin tidak mempengaruhi nilai variabel lokal dan bebas yang digunakan oleh penerjemah. selfmasih tersedia di __init__.
Mike Müller
Benar, pembaca berharap itu mempengaruhi variabel lokal, tetapi mungkin atau mungkin tidak , tergantung pada sejumlah keadaan. Intinya adalah itu UB.
MaxB
Kutipan: "Isi kamus ini tidak boleh diubah"
MaxB
@ Maxb Saya menambahkan versi yang tidak mengubah lokal ().
Mike Müller
7

Pustaka menarik yang menangani ini (dan menghindari banyak pelat ketel lainnya) adalah attrs . Contoh Anda, misalnya, dapat direduksi menjadi ini (anggap kelasnya dipanggil MyClass):

import attr

@attr.s
class MyClass:
    x = attr.ib()
    y = attr.ib()
    z = attr.ib()

Anda bahkan tidak perlu __init__metode lagi, kecuali itu melakukan hal-hal lain juga. Ini pengantar yang bagus dari Glyph Lefkowitz .

RafG
sumber
Sejauh mana fungsi attrdibuat redundan dataclasses?
gerrit
1
@gerrit Ini dibahas dalam dokumentasi paket attrs . Tbh, perbedaannya tidak terlalu besar lagi.
Ivo Merchiers
5

$ 0,02 saya Sangat dekat dengan jawaban Joran Beasley, tetapi lebih elegan:

def __init__(self, a, b, c, d, e, f):
    vars(self).update((k, v) for k, v in locals().items() if v is not self)

Selain itu, jawaban Mike Müller (yang terbaik untuk seleraku) dapat dikurangi dengan teknik ini:

def auto_init(ns):
    self = ns.pop('self')
    vars(self).update(ns)

Dan telepon auto_init(locals())dari Anda__init__

bgusach
sumber
1
docs.python.org/2/library/functions.html#locals locals() tidak boleh dimodifikasi (perilaku tidak terdefinisi)
MaxB
4

Ini adalah cara alami untuk melakukan sesuatu dengan Python. Jangan mencoba untuk menciptakan sesuatu yang lebih pintar, itu akan mengarah pada kode yang terlalu pintar sehingga tidak seorang pun di tim Anda akan mengerti. Jika Anda ingin menjadi pemain tim dan terus menulis seperti ini.

Peter Krumins
sumber
4

Python 3.7 dan seterusnya

Dalam Python 3.7, Anda dapat (ab) menggunakan dataclassdekorator, tersedia dari dataclassesmodul. Dari dokumentasi:

Modul ini menyediakan dekorator dan fungsi untuk secara otomatis menambahkan metode khusus yang dihasilkan seperti __init__()dan __repr__()ke kelas yang ditentukan pengguna. Awalnya dijelaskan dalam PEP 557.

Variabel anggota untuk digunakan dalam metode yang dihasilkan ini didefinisikan menggunakan penjelasan jenis PEP 526. Misalnya kode ini:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Akan menambahkan, antara lain, __init__()yang terlihat seperti:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
      self.name = name
      self.unit_price = unit_price
      self.quantity_on_hand = quantity_on_hand

Perhatikan bahwa metode ini secara otomatis ditambahkan ke kelas: itu tidak secara langsung ditentukan dalam definisi InventoryItem yang ditunjukkan di atas.

Jika kelas Anda besar dan kompleks, mungkin tidak pantas menggunakan a dataclass. Saya menulis ini pada hari rilis Python 3.7.0, jadi pola penggunaannya belum mapan.

gerrit
sumber