Ngăn tạo các thuộc tính mới bên ngoài __init__


82

Tôi muốn có thể tạo một lớp (bằng Python) mà sau khi được khởi tạo __init__, không chấp nhận các thuộc tính mới, nhưng chấp nhận các sửa đổi của các thuộc tính hiện có. Có một số cách hack-ish mà tôi có thể thấy để thực hiện việc này, chẳng hạn như có một __setattr__phương pháp như

def __setattr__(self, attribute, value):
    if not attribute in self.__dict__:
        print "Cannot set %s" % attribute
    else:
        self.__dict__[attribute] = value

và sau đó chỉnh sửa __dict__trực tiếp bên trong __init__, nhưng tôi tự hỏi liệu có cách nào 'thích hợp' để làm điều này không?


1
katrielalex mang lại những điểm tốt. Không có gì khó hiểu về nó. Bạn có thể tránh sử dụng __setattr__nhưng điều đó có thể sẽ rất khó.
aaronasterling

Tôi không hiểu tại sao điều này là hacky? Đó là giải pháp tốt nhất mà tôi có thể nghĩ ra và ngắn gọn hơn rất nhiều so với một số giải pháp khác được đề xuất.
Chris B

Câu trả lời:


80

Tôi sẽ không sử dụng __dict__trực tiếp, nhưng bạn có thể thêm một hàm để "đóng băng" một cách rõ ràng một phiên bản:

class FrozenClass(object):
    __isfrozen = False
    def __setattr__(self, key, value):
        if self.__isfrozen and not hasattr(self, key):
            raise TypeError( "%r is a frozen class" % self )
        object.__setattr__(self, key, value)

    def _freeze(self):
        self.__isfrozen = True

class Test(FrozenClass):
    def __init__(self):
        self.x = 42#
        self.y = 2**3

        self._freeze() # no new attributes after this point.

a,b = Test(), Test()
a.x = 10
b.z = 10 # fails

Rất tuyệt! Tôi nghĩ tôi sẽ lấy đoạn mã đó và bắt đầu sử dụng nó. (Hmm, tôi tự hỏi nếu nó có thể được thực hiện như một trang trí, hoặc nếu điều đó sẽ không phải là một ý tưởng tốt ...)
weronika

5
Nhận xét muộn: Tôi đã sử dụng công thức này thành công trong một thời gian, cho đến khi tôi thay đổi một thuộc tính thành một thuộc tính, nơi getter đang tạo ra một NotImplementedError. Tôi đã mất một thời gian dài để phát hiện ra rằng điều này là do thực tế là hasattrcác cuộc gọi actuall getattr, bỏ qua kết quả và trả về False trong trường hợp có lỗi, hãy xem blog này . Tìm thấy một giải pháp thay thế not hasattr(self, key)bằng key not in dir(self). Điều này có thể chậm hơn, nhưng đã giải quyết được vấn đề cho tôi.
Bas Swinckels

31

Nếu ai đó quan tâm đến việc làm điều đó với một người trang trí, đây là một giải pháp hiệu quả:

from functools import wraps

def froze_it(cls):
    cls.__frozen = False

    def frozensetattr(self, key, value):
        if self.__frozen and not hasattr(self, key):
            print("Class {} is frozen. Cannot set {} = {}"
                  .format(cls.__name__, key, value))
        else:
            object.__setattr__(self, key, value)

    def init_decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            func(self, *args, **kwargs)
            self.__frozen = True
        return wrapper

    cls.__setattr__ = frozensetattr
    cls.__init__ = init_decorator(cls.__init__)

    return cls

Khá đơn giản để sử dụng:

@froze_it 
class Foo(object):
    def __init__(self):
        self.bar = 10

foo = Foo()
foo.bar = 42
foo.foobar = "no way"

Kết quả:

>>> Class Foo is frozen. Cannot set foobar = no way

+1 cho phiên bản trang trí. Đó là những gì tôi sẽ sử dụng cho một dự án lớn hơn, trong một tập lệnh lớn hơn, điều này là quá mức cần thiết (có thể nếu họ có nó trong thư viện tiêu chuẩn ...). Hiện tại chỉ có "cảnh báo kiểu IDE".
Tomasz Gandor

2
Giải pháp này hoạt động như thế nào với di sản? Ví dụ: nếu tôi có một lớp con của Foo, lớp con này theo mặc định là một lớp đông lạnh?
mrgiesel

Có gói pypi cho trình trang trí này không?
winni2k

Làm thế nào người ta có thể nâng cao trình trang trí để nó hoạt động cho các lớp kế thừa?
Ivan Nechipayko

30

Slots là cách để đi:

Cách pythonic là sử dụng các khe thay vì chơi xung quanh __setter__. Mặc dù nó có thể giải quyết vấn đề, nhưng nó không cải thiện hiệu suất. Các thuộc tính của các đối tượng được lưu trữ trong một từ điển " __dict__", đây là lý do, tại sao bạn có thể thêm động các thuộc tính vào các đối tượng của các lớp mà chúng tôi đã tạo cho đến nay. Việc sử dụng từ điển để lưu trữ thuộc tính rất thuận tiện, nhưng nó có thể gây lãng phí không gian cho các đối tượng chỉ có một lượng nhỏ biến cá thể.

Khe cắm là một cách tốt để giải quyết vấn đề tiêu thụ không gian này. Thay vì có một lệnh động cho phép thêm các thuộc tính động vào các đối tượng, các vị trí cung cấp một cấu trúc tĩnh ngăn cấm việc bổ sung sau khi tạo một thể hiện.

Khi chúng tôi thiết kế một lớp, chúng tôi có thể sử dụng các vị trí để ngăn việc tạo động các thuộc tính. Để xác định vị trí, bạn phải xác định danh sách với tên __slots__. Danh sách phải chứa tất cả các thuộc tính mà bạn muốn sử dụng. Chúng tôi chứng minh điều này trong lớp sau, trong đó danh sách vị trí chỉ chứa tên cho một thuộc tính "val".

class S(object):

    __slots__ = ['val']

    def __init__(self, v):
        self.val = v


x = S(42)
print(x.val)

x.new = "not possible"

=> Không tạo được thuộc tính "new":

42 
Traceback (most recent call last):
  File "slots_ex.py", line 12, in <module>
    x.new = "not possible"
AttributeError: 'S' object has no attribute 'new'

NB:

  1. Kể từ Python 3.3, lợi thế tối ưu hóa tiêu thụ không gian không còn ấn tượng nữa. Với Python 3.3 , Từ điển chia sẻ khóa được sử dụng để lưu trữ các đối tượng. Các thuộc tính của các cá thể có khả năng chia sẻ một phần bộ nhớ trong của chúng với nhau, tức là phần lưu trữ các khóa và các hàm băm tương ứng của chúng. Điều này giúp giảm mức tiêu thụ bộ nhớ của các chương trình, vốn tạo ra nhiều trường hợp của các loại không phải nội trang. Nhưng vẫn là cách để tránh các thuộc tính được tạo động.
  1. Sử dụng khe cắm cũng có chi phí riêng. Nó sẽ phá vỡ tuần tự hóa (ví dụ: dưa chua). Nó cũng sẽ phá vỡ đa kế thừa. Một lớp không thể kế thừa từ nhiều hơn một lớp xác định vị trí hoặc có bố cục cá thể được xác định trong mã C (như danh sách, tuple hoặc int).

20

Thực ra, bạn không muốn __setattr__, bạn muốn __slots__. Thêm __slots__ = ('foo', 'bar', 'baz')vào nội dung lớp và Python sẽ đảm bảo rằng chỉ có foo, bar và baz trên bất kỳ trường hợp nào. Nhưng hãy đọc kỹ các danh sách tài liệu!


12
Sử dụng __slots__tác phẩm, nhưng nó sẽ phá vỡ serialization (ví dụ như dưa), trong số những thứ khác ... Nó thường là một ý tưởng tồi để khe sử dụng để tạo thuộc tính kiểm soát, chứ không phải là giảm chi phí bộ nhớ, theo ý kiến của tôi, dù sao ...
Joe Kington

Tôi biết, và tôi hestiate để sử dụng nó bản thân mình - nhưng làm thêm công việc để không cho phép các thuộc tính mới thường là một ý tưởng tồi, quá;)

2
Việc sử dụng __slots__cũng phá vỡ tính đa kế thừa. Một lớp học không thể kế thừa từ nhiều hơn một lớp mà một trong hai định nghĩa khe hoặc mũ một bố cục dụ định nghĩa trong mã C (như list, tuplehoặc int).
Feuermurmel

Nếu làm __slots__vỡ dưa chua của bạn, bạn đang sử dụng một quy trình muối chua cổ xưa. Chuyển protocol=-1đến các phương thức pickle cho giao thức mới nhất hiện có, là 2 trong Python 2 (được giới thiệu vào năm 2003 ). Cả hai giao thức mặc định và mới nhất trong Python 3 (3 và 4 tương ứng) đều xử lý được __slots__.
Nick Matteo

tốt, hầu hết thời gian tôi gió lên hối hận về sử dụng dưa tại tất cả: benfrederickson.com/dont-pickle-your-data
Erik Aronesty

7

Cách thích hợp là ghi đè __setattr__. Đó là những gì nó ở đó để làm.


Sau đó, cách thích hợp để đặt các biến là __init__gì? Có phải đặt chúng vào __dict__trực tiếp không?
astrofrog

1
Tôi sẽ ghi đè __setattr__trong __init__, bởi self.__setattr__ = <new-function-that-you-just-defined>.
Katriel

6
@katrielalex: điều đó sẽ không hoạt động đối với các lớp kiểu mới vì __xxx__các phương thức chỉ được tra cứu trên lớp chứ không phải trên trường hợp.
Ethan Furman,

6

Tôi rất thích giải pháp sử dụng trình trang trí, vì nó dễ sử dụng cho nhiều lớp trong một dự án, với các bổ sung tối thiểu cho mỗi lớp. Nhưng nó không hoạt động tốt với kế thừa. Vì vậy, đây là phiên bản của tôi: Nó chỉ ghi đè hàm __setattr__ - nếu thuộc tính không tồn tại và hàm người gọi không phải là __init__, nó sẽ in thông báo lỗi.

import inspect                                                                                                                             

def froze_it(cls):                                                                                                                      

    def frozensetattr(self, key, value):                                                                                                   
        if not hasattr(self, key) and inspect.stack()[1][3] != "__init__":                                                                 
            print("Class {} is frozen. Cannot set {} = {}"                                                                                 
                  .format(cls.__name__, key, value))                                                                                       
        else:                                                                                                                              
            self.__dict__[key] = value                                                                                                     

    cls.__setattr__ = frozensetattr                                                                                                        
    return cls                                                                                                                             

@froze_it                                                                                                                                  
class A:                                                                                                                                   
    def __init__(self):                                                                                                                    
        self._a = 0                                                                                                                        

a = A()                                                                                                                                    
a._a = 1                                                                                                                                   
a._b = 2 # error

4

Cái này thì sao:

class A():
    __allowed_attr=('_x', '_y')

    def __init__(self,x=0,y=0):
        self._x=x
        self._y=y

    def __setattr__(self,attribute,value):
        if not attribute in self.__class__.__allowed_attr:
            raise AttributeError
        else:
            super().__setattr__(attribute,value)

2

Đây là cách tiếp cận mà tôi đã đưa ra mà không cần thuộc tính _frozen hoặc phương thức để đóng băng () trong init.

Trong quá trình init, tôi chỉ cần thêm tất cả các thuộc tính của lớp vào cá thể.

Tôi thích điều này vì không có _frozen, freeze () và _frozen cũng không hiển thị trong đầu ra vars (instance).

class MetaModel(type):
    def __setattr__(self, name, value):
        raise AttributeError("Model classes do not accept arbitrary attributes")

class Model(object):
    __metaclass__ = MetaModel

    # init will take all CLASS attributes, and add them as SELF/INSTANCE attributes
    def __init__(self):
        for k, v in self.__class__.__dict__.iteritems():
            if not k.startswith("_"):
                self.__setattr__(k, v)

    # setattr, won't allow any attributes to be set on the SELF/INSTANCE that don't already exist
    def __setattr__(self, name, value):
        if not hasattr(self, name):
            raise AttributeError("Model instances do not accept arbitrary attributes")
        else:
            object.__setattr__(self, name, value)


# Example using            
class Dog(Model):
    name = ''
    kind = 'canine'

d, e = Dog(), Dog()
print vars(d)
print vars(e)
e.junk = 'stuff' # fails

Điều này dường như không hoạt động nếu một trong các trường là danh sách. Hãy nói names=[]. Sau đó, d.names.append['Fido']sẽ chèn 'Fido'vào cả hai d.namese.names. Tôi không biết đủ về Python để hiểu tại sao.
Reinier Torenbeek

2

pystrictmột trình trang trí có thể cài đặt pypi lấy cảm hứng từ câu hỏi stackoverflow này có thể được sử dụng với các lớp để đóng băng chúng. Có một ví dụ cho README cho thấy lý do tại sao cần một người trang trí như thế này ngay cả khi bạn có mypy và pylint đang chạy trong dự án của mình:

pip install pystrict

Sau đó, chỉ cần sử dụng trình trang trí @strict:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1

1

Tôi thích "Frozen" của Jochen Ritzel. Điều bất tiện là biến isfrozen sau đó xuất hiện khi in Class .__ dict Tôi đã giải quyết vấn đề này theo cách này bằng cách tạo danh sách các thuộc tính được ủy quyền (tương tự như các vị trí ):

class Frozen(object):
    __List = []
    def __setattr__(self, key, value):
        setIsOK = False
        for item in self.__List:
            if key == item:
                setIsOK = True

        if setIsOK == True:
            object.__setattr__(self, key, value)
        else:
            raise TypeError( "%r has no attributes %r" % (self, key) )

class Test(Frozen):
    _Frozen__List = ["attr1","attr2"]
    def __init__(self):
        self.attr1   =  1
        self.attr2   =  1

1

Việc gọi FrozenClasscủa Jochen Ritzel rất tuyệt, nhưng việc gọi _frozen()khi khởi tạo một lớp mọi lúc không phải là điều tuyệt vời (và bạn cần phải chấp nhận rủi ro khi quên nó). Tôi đã thêm một __init_slots__chức năng:

class FrozenClass(object):
    __isfrozen = False
    def _freeze(self):
        self.__isfrozen = True
    def __init_slots__(self, slots):
        for key in slots:
            object.__setattr__(self, key, None)
        self._freeze()
    def __setattr__(self, key, value):
        if self.__isfrozen and not hasattr(self, key):
            raise TypeError( "%r is a frozen class" % self )
        object.__setattr__(self, key, value)
class Test(FrozenClass):
    def __init__(self):
        self.__init_slots__(["x", "y"])
        self.x = 42#
        self.y = 2**3


a,b = Test(), Test()
a.x = 10
b.z = 10 # fails
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.