Inisialisasi variabel instance secara otomatis?

90

Saya memiliki kelas python yang terlihat seperti ini:

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):

diikuti oleh:

        self.PID=PID
        self.PPID=PPID
        self.cmd=cmd
        ...

Apakah ada cara untuk menginisialisasi variabel instance ini secara otomatis, seperti daftar inisialisasi C ++? Ini akan menghemat banyak kode yang berlebihan.

Adam Matan
sumber
1
Lihat juga diskusi tentang autoassignresep aktivestate, dan autoargsimplementasi alternatif di: Apa cara terbaik untuk melakukan penetapan atribut otomatis dengan Python, dan apakah itu ide yang bagus? - Stack Overflow
nealmcb

Jawaban:

104

Anda dapat menggunakan dekorator:

from functools import wraps
import inspect

def initializer(func):
    """
    Automatically assigns the parameters.

    >>> class process:
    ...     @initializer
    ...     def __init__(self, cmd, reachable=False, user='root'):
    ...         pass
    >>> p = process('halt', True)
    >>> p.cmd, p.reachable, p.user
    ('halt', True, 'root')
    """
    names, varargs, keywords, defaults = inspect.getargspec(func)

    @wraps(func)
    def wrapper(self, *args, **kargs):
        for name, arg in list(zip(names[1:], args)) + list(kargs.items()):
            setattr(self, name, arg)

        for name, default in zip(reversed(names), reversed(defaults)):
            if not hasattr(self, name):
                setattr(self, name, default)

        func(self, *args, **kargs)

    return wrapper

Gunakan untuk menghias __init__metode:

class process:
    @initializer
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        pass

Keluaran:

>>> c = process(1, 2, 3, 4, 5, 6)
>>> c.PID
1
>>> dir(c)
['FDs', 'PID', 'PPID', '__doc__', '__init__', '__module__', 'cmd', 'reachable', 'user'
Nadia Alramli
sumber
5
Ini berfungsi dan menjawab pertanyaan jadi saya memilih. Tetapi saya tetap mempertahankan jawaban Ferdidand Beyer: "Eksplisit lebih baik daripada implisit"
Lucas Gabriel Sánchez
14
+1 Untuk jawaban hebat yang memecahkan masalah saya. Tetapi bukankah seharusnya itu menjadi fungsi inti dari bahasa tersebut? Apakah menurut Anda layak menulis PEP?
Adam Matan
3
Ini adalah jawaban yang sangat bagus - ini telah langsung masuk ke kotak peralatan saya.
Michael van der Westhuizen
3
@NadiaAlramli Saya rasa ada bug kecil di kode. Lihat di sini gist.github.com/pmav99/137dbf4681be9a58de74 (original.py)
pmav99
2
Contoh saat ini memiliki bug, dan tidak akan berfungsi jika tanda tangan tidak menyertakan argumen default. Anda perlu menyertakan tanda centang untuk memastikan nama dan default tidak ada. Contoh: jika nama dan default:
36

Jika Anda menggunakan Python 2.6 atau lebih tinggi, Anda dapat menggunakan collections.namedtuple :

>>> from collections import namedtuple
>>> Process = namedtuple('Process', 'PID PPID cmd')
>>> proc = Process(1, 2, 3)
>>> proc.PID
1
>>> proc.PPID
2

Ini tepat terutama ketika kelas Anda benar-benar hanya sekantong besar nilai.

Kiv
sumber
2
"Ini tepat terutama ketika kelas Anda benar-benar hanya berisi nilai-nilai yang besar." Dalam skenario seperti itu, Anda juga dapat melakukan ini: https://docs.python.org/3.3/tutorial/classes.html#odds-and-ends
Big Sharpie
35

Untuk Python 3.7+ Anda dapat menggunakan Kelas Data , yang merupakan cara yang sangat pythonic dan dapat dipelihara untuk melakukan apa yang Anda inginkan.

Ini memungkinkan Anda untuk menentukan bidang untuk kelas Anda, yang merupakan variabel instance yang diinisialisasi secara otomatis.

Ini akan terlihat seperti itu:

@dataclass
class Process:
    PID: int
    PPID: int
    cmd: str
    ...

Itu __init__ Metode sudah akan berada di kelas Anda.

Perhatikan bahwa di sini diperlukan petunjuk jenis , itulah sebabnya saya menggunakan intdan strdalam contoh. Jika Anda tidak mengetahui jenis bidang Anda, Anda dapat menggunakan Any from the typingmodule .

Kelas Data memiliki banyak keunggulan dibandingkan dengan solusi yang diusulkan:

  • Ini eksplisit : semua bidang terlihat, yang menghormati Zen of Python dan membuatnya dapat dibaca dan dipelihara. Bandingkan dengan penggunaan **kwargs.
  • Itu bisa memiliki metode . Sama seperti kelas lainnya.
  • Ini memungkinkan Anda untuk melampaui otomatis __init__menggunakan __post_init__metode ini.

EDIT: Alasan untuk menghindari penggunaan NamedTuples

Beberapa menyarankan penggunaan namedtupleuntuk kasus ini, tetapi tuple bernama memiliki beberapa perilaku yang berbeda dari kelas Python, yang pada awalnya tidak terlalu jelas dan harus diketahui dengan baik:

1. NamedTuple tidak dapat diubah

Kekekalan dapat berguna, tetapi mungkin itu bukan yang Anda inginkan untuk instance Anda. DataClasses juga bisa berubah jika Anda menggunakan argumen frozen=Truepada @dataclassdekorator.

2. NamedTuple __eq__berperilaku seperti Tuple's

Dengan Python, SomeNamedTuple(a=1, b=2) == AnotherNamedTuple(c=1, d=2)adalah True! The __eq__fungsi NamedTuple, yang digunakan dalam perbandingan, hanya mempertimbangkan nilai-nilai dan posisi nilai-nilai pada kasus dibandingkan, bukan nama kelas atau ladang mereka.

Jundiaius
sumber
Ini hanya boleh digunakan jika tujuan kelas adalah untuk menyimpan data.
JC Rocamonde
Atau untuk mengembangkan penyimpanan data.
JC Rocamonde
3
Apakah Anda akan menjelaskan mengapa dataclass harus digunakan hanya untuk kelas yang menyimpan data, daripada memiliki perilaku lain juga? Saya mungkin membuat posting SO baru untuk ini sepenuhnya untuk lebih memahami kasus penggunaan yang sesuai. Terima kasih.
Vahid Pazirandeh
Data Classes can be thought of as "mutable namedtuples with defaults". - PEP557
aafulei
26

Mengutip Zen of Python ,

Eksplisit lebih baik daripada implisit.

Ferdinand Beyer
sumber
10
Bukankah deklarasi daftar inisialisasi cukup eksplisit?
Adam Matan
Saya kira. Tapi saya tidak melihat alasan untuk menambahkan sesuatu seperti itu ke bahasa. Saya jelas lebih suka pernyataan tugas ganda daripada semacam sihir dekorator di belakang layar.
Ferdinand Beyer
30
@Ferdinand, saya setuju itu akan konyol untuk memiliki dalam bahasa sesuatu yang bisa dengan baik di stdlib, tapi, itu HARUS di stdlib, karena "cantik lebih baik daripada jelek" lebih diutamakan, dan banyak tugas berulang yang jelek (seperti segala bentuk pengulangan).
Alex Martelli
Nah, untuk melawan: DWIM> DWIS
Terrence Brannon
Saya setuju dekorator lebih cantik daripada daftar tugas tetapi PyCharm membuatnya lebih jelek dengan tidak memahaminya :-(
user110954
23

Hal lain yang dapat Anda lakukan:

class X(object):
    def __init__(self, a,b,c,d):
        vars = locals() # dict of local names
        self.__dict__.update(vars) # __dict__ holds and object's attributes
        del self.__dict__["self"] # don't need `self`

Tapi satu-satunya solusi yang saya rekomendasikan, selain hanya mengejanya, adalah "buat makro di editor Anda" ;-p

Jochen Ritzel
sumber
1
Cara yang bagus untuk menghapus 'diri'.
michael
15

Anda dapat melakukannya dengan mudah dengan argumen kata kunci, misalnya seperti ini:

>>> class D:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

>>> D(test='d').test
'd'

implementasi serupa untuk argumen posisi adalah:

>> class C:
    def __init__(self, *args):
        self.t, self.d = args


>>> C('abc', 'def').t
'abc'
>>> C('abc', 'def').d
'def'

yang bagi saya sepertinya tidak menyelesaikan masalah Anda.

SilentGhost
sumber
3
Variasi lain yang saya suka adalahself.__dict__.update( **kwargs )
S. Lot
Sebaiknya gunakan penduduk setempat () dan berikan argumen yang normal.
mk12
7

Solusi Nadia lebih baik dan lebih kuat, tapi menurut saya ini juga menarik:

def constructor(*arg_names):
  def __init__(self, *args):
    for name, val in zip(arg_names, args):
      self.__setattr__(name, val)
  return __init__


class MyClass(object):
  __init__ = constructor("var1", "var2", "var3")


>>> c = MyClass("fish", "cheese", "beans")
>>> c.var2
"cheese"
Andrew Magee
sumber
5

Saya membutuhkan sesuatu untuk tujuan yang sama, tetapi tidak ada jawaban yang mencakup semua kasus yang saya uji. Jawaban Nadia paling dekat dengan apa yang saya cari, jadi saya mulai dengan kodenya sebagai basis.

Dekorator di bawah ini berfungsi dengan semua kombinasi argumen yang valid:

Positional                                          __init__(self, a, b                )
Keyword                                             __init__(self, a=None, b=None      )
Positional + Keyword                                __init__(self, a, b, c=None, d=None)
Variable Positional                                 __init__(self, *a                  )
Variable Positional + Keyword                       __init__(self, *a, b=None          )
Variable Positional + Variable Keyword              __init__(self, *a, **kwargs        )
Positional + Variable Positional + Keyword          __init__(self, a, *b, c=None       )
Positional + Variable Positional + Variable Keyword __init__(self, a, *b, **kwargs     )
Keyword Only                                        __init__(self, *, a=None           )
Positional + Keyword Only                           __init__(self, a, *, b=None        )

Ini juga mengimplementasikan _konvensi -prefix standar untuk memungkinkan __init__variabel -private yang tidak akan ditetapkan ke instance kelas.


###  StdLib  ###
from functools import wraps
import inspect


###########################################################################################################################
#//////|   Decorator   |//////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def auto_assign_arguments(function):

  @wraps(function)
  def wrapped(self, *args, **kwargs):
    _assign_args(self, list(args), kwargs, function)
    function(self, *args, **kwargs)

  return wrapped


###########################################################################################################################
#//////|   Utils   |//////////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def _assign_args(instance, args, kwargs, function):

  def set_attribute(instance, parameter, default_arg):
    if not(parameter.startswith("_")):
      setattr(instance, parameter, default_arg)

  def assign_keyword_defaults(parameters, defaults):
    for parameter, default_arg in zip(reversed(parameters), reversed(defaults)):
      set_attribute(instance, parameter, default_arg)

  def assign_positional_args(parameters, args):
    for parameter, arg in zip(parameters, args.copy()):
      set_attribute(instance, parameter, arg)
      args.remove(arg)

  def assign_keyword_args(kwargs):
    for parameter, arg in kwargs.items():
      set_attribute(instance, parameter, arg)
  def assign_keyword_only_defaults(defaults):
    return assign_keyword_args(defaults)

  def assign_variable_args(parameter, args):
    set_attribute(instance, parameter, args)

  POSITIONAL_PARAMS, VARIABLE_PARAM, _, KEYWORD_DEFAULTS, _, KEYWORD_ONLY_DEFAULTS, _ = inspect.getfullargspec(function)
  POSITIONAL_PARAMS = POSITIONAL_PARAMS[1:] # remove 'self'

  if(KEYWORD_DEFAULTS     ): assign_keyword_defaults     (parameters=POSITIONAL_PARAMS,  defaults=KEYWORD_DEFAULTS)
  if(KEYWORD_ONLY_DEFAULTS): assign_keyword_only_defaults(defaults=KEYWORD_ONLY_DEFAULTS                          )
  if(args                 ): assign_positional_args      (parameters=POSITIONAL_PARAMS,  args=args                )
  if(kwargs               ): assign_keyword_args         (kwargs=kwargs                                           )
  if(VARIABLE_PARAM       ): assign_variable_args        (parameter=VARIABLE_PARAM,      args=args                )


###########################################################################################################################$#//////|   Tests   |//////////////////////////////////////////////////////////////////////////////////////////////////////#$###########################################################################################################################$$if __name__ == "__main__":$$#######|   Positional   |##################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2)$$#######|   Keyword   |#####################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a="KW_DEFAULT_1", b="KW_DEFAULT_2"):$      pass$$  t = T(a="kw_arg_1", b="kw_arg_2")$  assert (t.a == "kw_arg_1") and (t.b == "kw_arg_2")$$#######|   Positional + Keyword   |########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, c="KW_DEFAULT_1", d="KW_DEFAULT_2"):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, c="kw_arg_1")$  assert (t.a == 1) and (t.b == 2) and (t.c == "kw_arg_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, d="kw_arg_2")$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "kw_arg_2")$$#######|   Variable Positional   |#########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3])$$#######|   Variable Positional + Keyword   |###############################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, b="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3]) and (t.b == "KW_DEFAULT_1")$$  t = T(1, 2, 3, b="kw_arg_1")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1")$$#######|   Variable Positional + Variable Keyword   |######################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, **kwargs):$      pass$$  t = T(1, 2, 3, b="kw_arg_1", c="kw_arg_2")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1") and (t.c == "kw_arg_2")$$#######|   Positional + Variable Positional + Keyword   |##################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, c="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3, c="kw_arg_1")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1")$$#######|   Positional + Variable Positional + Variable Keyword   |#########################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, **kwargs):$      pass$$  t = T(1, 2, 3, c="kw_arg_1", d="kw_arg_2")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1") and (t.d == "kw_arg_2")$$#######|   Keyword Only   |################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *, a="KW_DEFAULT_1"):$      pass$$  t = T(a="kw_arg_1")$  assert (t.a == "kw_arg_1")$$#######|   Positional + Keyword Only   |###################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *, b="KW_DEFAULT_1"):$      pass$$  t = T(1)$  assert (t.a == 1) and (t.b == "KW_DEFAULT_1")$$  t = T(1, b="kw_arg_1")$  assert (t.a == 1) and (t.b == "kw_arg_1")$$#######|   Private __init__ Variables (underscored)   |####################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, _c):$      pass$$  t = T(1, 2, 3)$  assert hasattr(t, "a") and hasattr(t, "b") and not(hasattr(t, "_c"))

catatan:

Saya menyertakan tes, tetapi menciutkannya ke baris terakhir ( 58 ) agar lebih singkat. Anda dapat memperluas pengujian, yang merinci semua kasus penggunaan potensial, dengan find/replace-ing semua $karakter dengan baris baru.

Enteleform
sumber
3

Mungkin tidak perlu menginisialisasi variabel, karena local () sudah berisi nilainya!

class Dummy (objek):

def __init__(self, a, b, default='Fred'):
    self.params = {k:v for k,v in locals().items() if k != 'self'}

d = Dummy (2, 3)

d. parameter

{'a': 2, 'b': 3, 'default': 'Fred'}

d.params ['b']

3

Tentu saja, dalam kelas seseorang dapat menggunakan self.params

pengguna3215769
sumber
Ini adalah pendekatan yang bagus dan orisinal, tetapi d['b']ditulis dalam lingua franca Python sementara d.params['b']akan menyebabkan kebingungan bagi pembaca kode.
Adam Matan
3

Segera setelah getargspectidak digunakan lagi sejak Python 3.5, inilah solusi yang menggunakan inspect.signature:

from inspect import signature, Parameter
import functools


def auto_assign(func):
    # Signature:
    sig = signature(func)
    for name, param in sig.parameters.items():
        if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
            raise RuntimeError('Unable to auto assign if *args or **kwargs in signature.')
    # Wrapper:
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        for i, (name, param) in enumerate(sig.parameters.items()):
            # Skip 'self' param:
            if i == 0: continue
            # Search value in args, kwargs or defaults:
            if i - 1 < len(args):
                val = args[i - 1]
            elif name in kwargs:
                val = kwargs[name]
            else:
                val = param.default
            setattr(self, name, val)
        func(self, *args, **kwargs)
    return wrapper

Periksa apakah berfungsi:

class Foo(object):
    @auto_assign
    def __init__(self, a, b, c=None, d=None, e=3):
        pass

f = Foo(1, 2, d="a")
assert f.a == 1
assert f.b == 2
assert f.c is None
assert f.d == "a"
assert f.e == 3

print("Ok")
Mikhail Gerasimov
sumber
2

Untuk Python 3.3+:

from functools import wraps
from inspect import Parameter, signature


def instance_variables(f):
    sig = signature(f)
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        values = sig.bind(self, *args, **kwargs)
        for k, p in sig.parameters.items():
            if k != 'self':
                if k in values.arguments:
                    val = values.arguments[k]
                    if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
                        setattr(self, k, val)
                    elif p.kind == Parameter.VAR_KEYWORD:
                        for k, v in values.arguments[k].items():
                            setattr(self, k, v) 
                else:
                    setattr(self, k, p.default) 
    return wrapper

class Point(object):
    @instance_variables 
    def __init__(self, x, y, z=1, *, m='meh', **kwargs):
        pass

Demo:

>>> p = Point('foo', 'bar', r=100, u=200)
>>> p.x, p.y, p.z, p.m, p.r, p.u
('foo', 'bar', 1, 'meh', 100, 200)

Pendekatan non-dekorator untuk Python 2 dan 3 menggunakan bingkai:

import inspect


def populate_self(self):
    frame = inspect.getouterframes(inspect.currentframe())[1][0]
    for k, v in frame.f_locals.items():
        if k != 'self':
            setattr(self, k, v)


class Point(object):
    def __init__(self, x, y):
        populate_self(self)

Demo:

>>> p = Point('foo', 'bar')
>>> p.x
'foo'
>>> p.y
'bar'
Ashwini Chaudhary
sumber
1

nu11ptr telah membuat modul kecil, PyInstanceVars , yang menyertakan fungsionalitas ini sebagai dekorator fungsi. Dalam modul README dinyatakan bahwa kinerja " [...] sekarang hanya 30-40% lebih buruk daripada inisialisasi eksplisit di bawah CPython ".

Contoh penggunaan, diambil langsung dari dokumentasi modul :

>>> from instancevars import *
>>> class TestMe(object):
...     @instancevars(omit=['arg2_'])
...     def __init__(self, _arg1, arg2_, arg3='test'):
...             self.arg2 = arg2_ + 1
...
>>> testme = TestMe(1, 2)
>>> testme._arg1
1
>>> testme.arg2_
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'TestMe' object has no attribute 'arg2_'
>>> testme.arg2
3
>>> testme.arg3
'test'
benregn
sumber
0

Mungkin ini pertanyaan tertutup, tetapi saya ingin mengusulkan solusi saya untuk mengetahui pendapat Anda tentang hal itu. Saya telah menggunakan metaclass yang menerapkan dekorator ke metode init

import inspect

class AutoInit(type):
    def __new__(meta, classname, supers, classdict):
        classdict['__init__'] = wrapper(classdict['__init__'])
        return type.__new__(meta, classname, supers, classdict)

def wrapper(old_init):
    def autoinit(*args):
        formals = inspect.getfullargspec(old_init).args
        for name, value in zip(formals[1:], args[1:]):
            setattr(args[0], name, value)
    return autoinit
delca85
sumber
0

The attrs perpustakaan melakukan sesuatu seperti ini.

offby1
sumber
0

di akhir fungsi init :

for var in list(locals().keys()):
    setattr(self,var,locals()[var])

Untuk informasi lebih lanjut, setattr()silakan lihat di sini

LuWil
sumber
0

Ada fungsi pembantu untuk melakukan ini di lib fastcore https://fastcore.fast.ai/utils.html#store_attr .

from fastcore.utils import store_attr

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        store_attr() # this will do the same as self.PID = PID etc.
Alex
sumber