Làm cách nào để tránh bản thân bản thân.x = x; tự.y = y; self.z = z mẫu trong __init__?


170

Tôi thấy các mẫu như

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

khá thường xuyên, thường có nhiều thông số hơn. Có một cách tốt để tránh kiểu lặp đi lặp lại tẻ nhạt này? Lớp học nên kế thừa từ namedtuplethay thế?


31
Không phải tất cả sự tiếp thu là xấu. Hãy nhớ rằng mô hình lớp của Python không bao gồm định nghĩa rõ ràng về các thuộc tính cá thể, vì vậy các bài tập này là tương đương tự ghi lại.
chepner

4
@chepner: Chà, không yêu cầu định nghĩa rõ ràng. Bạn có thể sử dụng __slots__cho mục đích mặc dù ; đó là unpythonic nhẹ (tiết kiệm hơn để tiết kiệm bộ nhớ), nhưng tôi thích phần lớn để tránh nguy cơ tự động sinh động một thuộc tính hoàn toàn mới nếu tôi đánh máy tên.
ShadowRanger

2
Bất kỳ biên tập viên tốt sẽ có mẫu. Bạn gõ ini <shortcut> x, y, z): <shortcut>và bạn đã hoàn thành.
Gerenuk

3
Namedtuples là tuyệt vời, nếu bạn muốn một đối tượng giá trị bất biến. Nếu bạn muốn một lớp thường xuyên, có thể thay đổi, bạn không thể sử dụng chúng.
RemcoGerlich

4
"Đừng" là một tùy chọn tốt, mọi tùy chọn khả dụng sẽ giết chữ ký phương thức (và do đó có khả năng toàn bộ giao diện). Ngoài ra, nếu các lớp của bạn có số lượng trường không thể chịu được để khởi tạo, bạn có thể muốn xem xét tách chúng.
Kroltan

Câu trả lời:


87

Chỉnh sửa: Nếu bạn có python 3.7+, chỉ cần sử dụng dataclass

Một giải pháp trang trí giữ chữ ký:

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

2
câu trả lời hay, nhưng sẽ không hoạt động với python2.7: khôngsignature
MaxB

3
@alexis trình trang trí "decorator.decorator" tự động kết thúc chức năng
Siphor 4/2/2016

4
Tôi khá đau khổ về việc nên yêu nó hay ghét nó. Tôi đánh giá cao việc giữ gìn chữ ký.
Kyle Strand

14
"... Rõ ràng là tốt hơn ngầm. Đơn giản là tốt hơn phức tạp. ..." -Zen của Python
Jack Stout

9
-1 Khá thẳng thắn điều này thật kinh khủng. Tôi không biết mã này đang làm gì trong nháy mắt, và nó thực sự gấp mười lần số lượng mã. Thông minh cảm thấy mát mẻ và tất cả, nhưng đây là một lạm dụng của sự thông minh rõ ràng của bạn.
Ian Newson

108

Tuyên bố miễn trừ trách nhiệm: Có vẻ như nhiều người lo ngại về việc trình bày giải pháp này, vì vậy tôi sẽ cung cấp từ chối trách nhiệm rất rõ ràng. Bạn không nên sử dụng giải pháp này. Tôi chỉ cung cấp nó dưới dạng thông tin, vì vậy bạn biết rằng ngôn ngữ có khả năng này. Phần còn lại của câu trả lời chỉ là thể hiện khả năng ngôn ngữ, không xác nhận sử dụng chúng theo cách này.


Thực sự không có gì sai với việc sao chép rõ ràng các tham số vào các thuộc tính. Nếu bạn có quá nhiều tham số trong ctor, đôi khi nó được coi là mùi mã và có lẽ bạn nên nhóm các tham số này thành một đối tượng ít hơn. Những lần khác, nó là cần thiết và không có gì sai với nó. Dù sao, làm nó rõ ràng là cách để đi.

Tuy nhiên, vì bạn đang hỏi làm thế nào nó có thể được thực hiện (và không nên thực hiện nó), nên một giải pháp là:

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

16
câu trả lời tốt +1 ... mặc dù self.__dict__.update(kwargs)có thể hơi nhiều pythonic
Joran Beasley

44
Vấn đề với cách tiếp cận này là không có hồ sơ về những gì đối số A.__init__thực sự mong đợi và không có lỗi kiểm tra tên đối số bị nhầm lẫn.
MaxB

7
@JoranBeasley Cập nhật từ điển cá thể một cách mù quáng với các kwargslá bạn mở tương đương với một cuộc tấn công tiêm nhiễm SQL. Nếu đối tượng của bạn có một phương thức được đặt tên my_methodvà bạn truyền một đối số được đặt tên my_methodcho hàm tạo, thì update()từ điển, bạn chỉ cần ghi đè phương thức.
Pedro

3
Như những người khác nói, đề xuất là phong cách lập trình thực sự kém. Nó che giấu thông tin quan trọng. Bạn có thể hiển thị nó, nhưng rõ ràng bạn không khuyến khích OP sử dụng nó.
Gerenuk

3
@Pedro Có sự khác biệt về ngữ nghĩa nào giữa cú pháp của gruzczy và JoranBeasley không?
gerrit

29

Như những người khác đã đề cập, sự lặp lại không phải là xấu, nhưng trong một số trường hợp, một tên được đặt tên có thể rất phù hợp với loại vấn đề này. Điều này tránh sử dụng người địa phương () hoặc kwargs, thường là một ý tưởng tồi.

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

Tôi đã tìm thấy việc sử dụng hạn chế cho nó, nhưng bạn có thể thừa hưởng một tên được đặt tên như với bất kỳ đối tượng nào khác (ví dụ tiếp tục):

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

5
Đây là các bộ dữ liệu, vì vậy propertiesphương thức của bạn có thể được viết dưới dạng đơn giản return tuple(self), có thể duy trì nhiều hơn nếu trong tương lai có nhiều trường được thêm vào định nghĩa được đặt tên.
PaulMcG

1
Ngoài ra, chuỗi khai báo có tên của bạn không yêu cầu dấu phẩy giữa các tên trường, cũng XYZ = namedtuple("XYZ", "x y z")hoạt động tốt.
PaulMcG

Cảm ơn @PaulMcGuire. Tôi đã cố gắng nghĩ về một add-on thực sự đơn giản để thể hiện sự kế thừa và loại khoảng cách trên đó. Bạn đúng 100% và đó cũng là một tốc ký tuyệt vời với các đối tượng được thừa hưởng khác! Tôi có đề cập đến các tên trường có thể được phân tách bằng dấu phẩy hoặc dấu cách - Tôi thích CSV theo thói quen
Tập lệnh Shell nhỏ

1
Tôi thường sử dụng namedtuples cho mục đích chính xác này, đặc biệt là trong mã toán học trong đó một hàm có thể được tham số hóa cao và có một loạt các hệ số chỉ có ý nghĩa với nhau.
gièm pha

Vấn đề với namedtuplelà chúng chỉ đọc. Bạn không thể làm abc.x += 1hoặc bất cứ điều gì như thế.
hamstergene

29

rõ ràng là tốt hơn so với ngầm ... vì vậy chắc chắn bạn có thể làm cho nó ngắn gọn hơn:

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

Câu hỏi tốt hơn là bạn nên?

... điều đó nói rằng nếu bạn muốn một tuple có tên, tôi sẽ khuyên bạn nên sử dụng một tuple có tên (hãy nhớ các tuple có một số điều kiện kèm theo chúng) ... có lẽ bạn muốn có một lệnh đã ra lệnh hoặc thậm chí chỉ là một lệnh ...


Sau đó, đối tượng sẽ cần bộ sưu tập rác theo chu kỳ vì nó có thuộc tính
John La Rooy

3
@bernie (hoặc là bemie?), đôi khi ke r ning rất khó
mèo

4
Đối với các thử nghiệm hiệu quả hơn một chút, if k != "self":có thể được thay đổi thành if v is not self:, thử nghiệm nhận dạng giá rẻ, thay vì so sánh chuỗi. Tôi cho rằng về mặt kỹ thuật __init__có thể được gọi lần thứ hai sau khi xây dựng và thông qua selfnhư một cuộc tranh cãi tiếp theo, nhưng tôi thực sự không muốn nghĩ loại quái vật nào sẽ làm điều đó. :-)
ShadowRanger 4/2/2016

Điều đó có thể được thực hiện thành một hàm lấy giá trị trả về của locals: set_fields_from_locals(locals()). Sau đó, nó không còn lâu hơn các giải pháp dựa trên trang trí kỳ diệu hơn.
Lii

20

Để mở rộng gruszczycâu trả lời, tôi đã sử dụng một mẫu như:

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

Tôi thích phương pháp này vì nó:

  • tránh sự lặp lại
  • chống lại lỗi chính tả khi xây dựng một đối tượng
  • hoạt động tốt với phân lớp (chỉ có thể super().__init(...))
  • cho phép ghi lại các thuộc tính ở cấp độ lớp (nơi chúng thuộc về) chứ không phải trong X.__init__

Trước Python 3.6, điều này không kiểm soát thứ tự các thuộc tính được đặt, có thể là một vấn đề nếu một số thuộc tính là thuộc tính có setters truy cập các thuộc tính khác.

Nó có thể được cải thiện một chút, nhưng tôi là người sử dụng mã riêng của mình nên tôi không lo lắng về bất kỳ hình thức vệ sinh đầu vào nào. Có lẽ một AttributeErrorsẽ thích hợp hơn.


10

Bạn cũng có thể làm:

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

Tất nhiên, bạn sẽ phải nhập inspectmô-đun.


8

Đây là một giải pháp mà không cần nhập thêm.

Chức năng trợ giúp

Một chức năng trợ giúp nhỏ làm cho nó thuận tiện hơn và có thể sử dụng lại:

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)

Ứng dụng

Bạn cần gọi nó bằng locals():

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

Kiểm tra

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

Đầu ra:

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

Không hề thay đổi locals()

Nếu bạn không muốn thay đổi, hãy locals()sử dụng phiên bản này:

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)

docs.python.org/2/library/functions.html#locals locals() không nên được sửa đổi (nó có thể ảnh hưởng đến người phiên dịch, trong trường hợp của bạn, loại bỏ selftừ phạm vi chức năng gọi của)
MaxB

@MaxB Từ các tài liệu bạn trích dẫn: ... các thay đổi có thể không ảnh hưởng đến các giá trị của các biến cục bộ và biến miễn phí được sử dụng bởi trình thông dịch. selfvẫn có sẵn trong __init__.
Mike Müller

Đúng, người đọc hy vọng nó sẽ ảnh hưởng đến các biến cục bộ, nhưng nó có thể hoặc không , tùy thuộc vào một số trường hợp. Vấn đề là đó là UB.
MaxB

Trích dẫn: "Không nên sửa đổi nội dung của từ điển này"
MaxB

@MaxB Tôi đã thêm một phiên bản không thay đổi địa phương ().
Mike Müller

7

Một thư viện thú vị xử lý việc này (và tránh rất nhiều nồi hơi khác) là attrs . Ví dụ của bạn, ví dụ, có thể được giảm xuống này (giả sử lớp được gọi MyClass):

import attr

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

Bạn thậm chí không cần một __init__phương pháp nữa, trừ khi nó cũng làm những thứ khác. Đây là một lời giới thiệu hay của Glyph Lefkowitz .


Ở mức độ nào là chức năng của attrdự phòng được thực hiện bởi dataclasses?
gerrit

1
@gerrit Điều này được thảo luận trong tài liệu của gói attrs . Tbh, sự khác biệt dường như không còn lớn nữa.
Ivo Merchiers

5

0,02 đô la của tôi. Nó rất gần với câu trả lời của Joran Beasley, nhưng thanh lịch hơn:

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)

Ngoài ra, câu trả lời của Mike Müller (câu trả lời tốt nhất theo sở thích của tôi) có thể được giảm bớt bằng kỹ thuật này:

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

Và cuộc gọi chỉ auto_init(locals())từ bạn__init__


1
docs.python.org/2/library/functions.html#locals locals() không nên được sửa đổi (hành vi undefined)
MaxB

4

Đó là một cách tự nhiên để làm mọi thứ trong Python. Đừng cố gắng phát minh ra thứ gì đó thông minh hơn, nó sẽ dẫn đến mã quá thông minh mà không ai trong nhóm của bạn sẽ hiểu. Nếu bạn muốn trở thành một người chơi nhóm và sau đó tiếp tục viết nó theo cách này.


4

Python 3.7 trở đi

Trong Python 3.7, bạn có thể (ab) sử dụng trình dataclasstrang trí, có sẵn từ dataclassesmô-đun. Từ tài liệu:

Mô-đun này cung cấp một trình trang trí và các chức năng để tự động thêm các phương thức đặc biệt được tạo như __init__()__repr__() vào các lớp do người dùng định nghĩa. Ban đầu nó được mô tả trong PEP 557.

Các biến thành viên để sử dụng trong các phương thức được tạo này được xác định bằng cách sử dụng chú thích loại PEP 526. Ví dụ mã này:

@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

Sẽ thêm, trong số những thứ khác, một __init__()hình như:

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

Lưu ý rằng phương thức này được tự động thêm vào lớp: nó không được chỉ định trực tiếp trong định nghĩa InventoryItem được hiển thị ở trên.

Nếu lớp của bạn lớn và phức tạp, thể không phù hợp để sử dụng a dataclass. Tôi đang viết bài này vào ngày phát hành Python 3.7.0, vì vậy các mẫu sử dụng chưa được thiết lập tốt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.