Làm thế nào để trang trí một lớp học?


129

Trong Python 2.5, có cách nào để tạo một trình trang trí trang trí một lớp không? Cụ thể, tôi muốn sử dụng một trình trang trí để thêm một thành viên vào một lớp và thay đổi hàm tạo để lấy giá trị cho thành viên đó.

Tìm kiếm một cái gì đó như sau (có lỗi cú pháp trên 'class Foo:':

def getId(self): return self.__id

class addID(original_class):
    def __init__(self, id, *args, **kws):
        self.__id = id
        self.getId = getId
        original_class.__init__(self, *args, **kws)

@addID
class Foo:
    def __init__(self, value1):
        self.value1 = value1

if __name__ == '__main__':
    foo1 = Foo(5,1)
    print foo1.value1, foo1.getId()
    foo2 = Foo(15,2)
    print foo2.value1, foo2.getId()

Tôi đoán những gì tôi thực sự theo đuổi là một cách để làm một cái gì đó giống như giao diện C # trong Python. Tôi cần phải chuyển đổi mô hình của tôi, tôi cho rằng.

Câu trả lời:


80

Tôi sẽ đưa ra quan điểm rằng bạn có thể muốn xem xét một lớp con thay vì cách tiếp cận bạn đã vạch ra. Tuy nhiên, không biết kịch bản cụ thể của bạn, YMMV :-)

Những gì bạn đang nghĩ là một siêu dữ liệu. Các __new__chức năng trong một metaclass được truyền định nghĩa đề xuất đầy đủ các lớp học, mà nó thì có thể viết lại trước khi lớp được tạo ra. Tại thời điểm đó, bạn có thể đặt ra hàm tạo cho một cái mới.

Thí dụ:

def substitute_init(self, id, *args, **kwargs):
    pass

class FooMeta(type):

    def __new__(cls, name, bases, attrs):
        attrs['__init__'] = substitute_init
        return super(FooMeta, cls).__new__(cls, name, bases, attrs)

class Foo(object):

    __metaclass__ = FooMeta

    def __init__(self, value1):
        pass

Thay thế các nhà xây dựng có lẽ là một chút kịch tính, nhưng ngôn ngữ cung cấp hỗ trợ cho loại nội tâm sâu sắc và sửa đổi năng động này.


Cảm ơn bạn, đó là những gì tôi đang tìm kiếm. Một lớp có thể sửa đổi bất kỳ số lượng các lớp khác sao cho tất cả chúng đều có một thành viên cụ thể. Lý do của tôi về việc không có các lớp kế thừa từ một lớp ID chung là tôi muốn có các phiên bản không phải ID của các lớp cũng như các phiên bản ID.
Robert Gowland

Metaclass từng là cách để thực hiện những việc như thế này trong Python2.5 trở lên, nhưng ngày nay bạn có thể rất thường xuyên sử dụng các trình trang trí lớp (xem câu trả lời của Steven), đơn giản hơn nhiều.
Jonathan Hartley

203

Ngoài câu hỏi liệu trang trí lớp có phải là giải pháp phù hợp cho vấn đề của bạn không:

Trong Python 2.6 trở lên, có các trình trang trí lớp với @ -syntax, vì vậy bạn có thể viết:

@addID
class Foo:
    pass

Trong các phiên bản cũ hơn, bạn có thể làm theo cách khác:

class Foo:
    pass

Foo = addID(Foo)

Tuy nhiên, lưu ý rằng điều này hoạt động tương tự như đối với các trình trang trí chức năng và trình trang trí sẽ trả về lớp mới (hoặc bản gốc đã sửa đổi), đây không phải là điều bạn đang làm trong ví dụ. Trình trang trí addID sẽ trông như thế này:

def addID(original_class):
    orig_init = original_class.__init__
    # Make copy of original __init__, so we can call it without recursion

    def __init__(self, id, *args, **kws):
        self.__id = id
        self.getId = getId
        orig_init(self, *args, **kws) # Call the original __init__

    original_class.__init__ = __init__ # Set the class' __init__ to the new one
    return original_class

Sau đó, bạn có thể sử dụng cú pháp thích hợp cho phiên bản Python của mình như được mô tả ở trên.

Nhưng tôi đồng ý với những người khác rằng sự kế thừa phù hợp hơn nếu bạn muốn ghi đè __init__.


2
... mặc dù câu hỏi đặc biệt đề cập đến Python 2.5 :-)
Jarret Hardie

5
Xin lỗi vì sự lộn xộn của dòng, nhưng các mẫu mã không hoàn toàn tuyệt vời trong các nhận xét ...: def addID (original_group): original_group .__ orig__init__ = original_group .__ init__ def init __ (self, * args, ** kws): in "decorator" self.id = 9 original_group .__ orig__init __ (self, * args, ** kws) original_group .__ init = init return original_group @addID class Foo: def __init __ (self): in "Foo" a = Foo () print a.id
Gerald Senarclens de Grancy

2
@Steven, @Gerald đã đúng, nếu bạn có thể cập nhật câu trả lời của mình, việc đọc mã của anh ấy sẽ dễ dàng hơn rất nhiều;)
Ngày

3
@Day: cảm ơn bạn đã nhắc nhở, tôi đã không nhận thấy các ý kiến ​​trước đó. Tuy nhiên: Tôi cũng nhận được độ sâu đệ quy tối đa vượt quá với mã Geralds. Tôi sẽ thử và tạo một phiên bản hoạt động ;-)
Steven

1
@Day và @Gerald: xin lỗi, vấn đề không nằm ở mã của Gerald, tôi đã nhầm lẫn vì mã bị sai lệch trong nhận xét ;-)
Steven

20

Không ai giải thích rằng bạn có thể xác định động các lớp. Vì vậy, bạn có thể có một trình trang trí xác định (và trả về) một lớp con:

def addId(cls):

    class AddId(cls):

        def __init__(self, id, *args, **kargs):
            super(AddId, self).__init__(*args, **kargs)
            self.__id = id

        def getId(self):
            return self.__id

    return AddId

Cái nào có thể được sử dụng trong Python 2 (nhận xét từ Blckknght giải thích tại sao bạn nên tiếp tục làm điều này trong 2.6+) như thế này:

class Foo:
    pass

FooId = addId(Foo)

Và trong Python 3 như thế này (nhưng hãy cẩn thận khi sử dụng super()trong các lớp của bạn):

@addId
class Foo:
    pass

Vì vậy, bạn có thể có bánh của bạn ăn nó - thừa kế trang trí!


4
Phân lớp trong trình trang trí rất nguy hiểm trong Python 2.6+, vì nó phá vỡ supercác cuộc gọi trong lớp gốc. Nếu Foocó một phương thức được foogọi là super(Foo, self).foo()nó sẽ lặp lại vô hạn, bởi vì tên Foođược liên kết với lớp con được trả về bởi trình trang trí, không phải là lớp gốc (không thể truy cập bằng bất kỳ tên nào). Đối số của Python 3 super()tránh được vấn đề này (tôi giả sử thông qua cùng một phép thuật trình biên dịch cho phép nó hoạt động hoàn toàn). Bạn cũng có thể giải quyết vấn đề bằng cách trang trí thủ công lớp dưới tên khác (như bạn đã làm trong ví dụ Python 2.5).
Blckknght

1
Huh. cảm ơn, tôi không có ý tưởng (tôi sử dụng python 3). sẽ thêm một bình luận.
rút cooke

13

Đó không phải là một thực hành tốt và không có cơ chế để làm điều đó vì điều đó. Cách đúng đắn để thực hiện những gì bạn muốn là thừa kế.

Hãy xem tài liệu của lớp .

Một ví dụ nhỏ:

class Employee(object):

    def __init__(self, age, sex, siblings=0):
        self.age = age
        self.sex = sex    
        self.siblings = siblings

    def born_on(self):    
        today = datetime.date.today()

        return today - datetime.timedelta(days=self.age*365)


class Boss(Employee):    
    def __init__(self, age, sex, siblings=0, bonus=0):
        self.bonus = bonus
        Employee.__init__(self, age, sex, siblings)

Cách này Boss có mọi thứ Employeecó, với cả __init__phương pháp và thành viên của riêng mình.


3
Tôi đoán những gì tôi muốn là có thuyết bất khả tri về lớp mà nó chứa. Đó là, có thể có hàng tá các lớp khác nhau mà tôi muốn áp dụng các tính năng của Boss cho. Tôi có còn lại với việc có hàng tá lớp kế thừa từ Boss không?
Robert Gowland

5
@Robert Gowland: Đó là lý do Python có nhiều kế thừa. Có, bạn nên kế thừa các khía cạnh khác nhau từ các lớp cha mẹ khác nhau.
S.Lott

7
@ S.Lott: Nói chung, nhiều kế thừa là một ý tưởng tồi, thậm chí quá nhiều mức độ thừa kế cũng xấu. Tôi sẽ khuyên bạn nên tránh xa nhiều di sản.
mpeterson

5
mpeterson: Có phải nhiều thừa kế trong python tệ hơn phương pháp này không? Có gì sai với nhiều thừa kế của python?
Arafangion

2
@Arafangion: Nhiều kế thừa thường được coi là một dấu hiệu cảnh báo sự cố. Nó làm cho hệ thống phân cấp phức tạp và các mối quan hệ khó theo dõi. Nếu miền vấn đề của bạn cho vay chính nó để đa kế thừa, (nó có thể được mô hình hóa theo thứ bậc không?) Thì đó là một lựa chọn tự nhiên cho nhiều người. Điều này áp dụng cho tất cả các ngôn ngữ cho phép đa kế thừa.
Morten Jensen

6

Tôi đồng ý thừa kế là phù hợp hơn cho vấn đề đặt ra.

Tôi thấy câu hỏi này thực sự tiện dụng mặc dù trên các lớp trang trí, cảm ơn tất cả.

Dưới đây là một vài ví dụ khác, dựa trên các câu trả lời khác, bao gồm cách kế thừa ảnh hưởng đến mọi thứ trong Python 2.7, (và @wraps , duy trì chuỗi tài liệu gốc của hàm, v.v.):

def dec(klass):
    old_foo = klass.foo
    @wraps(klass.foo)
    def decorated_foo(self, *args ,**kwargs):
        print('@decorator pre %s' % msg)
        old_foo(self, *args, **kwargs)
        print('@decorator post %s' % msg)
    klass.foo = decorated_foo
    return klass

@dec  # No parentheses
class Foo...

Thường thì bạn muốn thêm tham số vào trang trí của mình:

from functools import wraps

def dec(msg='default'):
    def decorator(klass):
        old_foo = klass.foo
        @wraps(klass.foo)
        def decorated_foo(self, *args ,**kwargs):
            print('@decorator pre %s' % msg)
            old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)
        klass.foo = decorated_foo
        return klass
    return decorator

@dec('foo decorator')  # You must add parentheses now, even if they're empty
class Foo(object):
    def foo(self, *args, **kwargs):
        print('foo.foo()')

@dec('subfoo decorator')
class SubFoo(Foo):
    def foo(self, *args, **kwargs):
        print('subfoo.foo() pre')
        super(SubFoo, self).foo(*args, **kwargs)
        print('subfoo.foo() post')

@dec('subsubfoo decorator')
class SubSubFoo(SubFoo):
    def foo(self, *args, **kwargs):
        print('subsubfoo.foo() pre')
        super(SubSubFoo, self).foo(*args, **kwargs)
        print('subsubfoo.foo() post')

SubSubFoo().foo()

Đầu ra:

@decorator pre subsubfoo decorator
subsubfoo.foo() pre
@decorator pre subfoo decorator
subfoo.foo() pre
@decorator pre foo decorator
foo.foo()
@decorator post foo decorator
subfoo.foo() post
@decorator post subfoo decorator
subsubfoo.foo() post
@decorator post subsubfoo decorator

Tôi đã sử dụng một trình trang trí chức năng, vì tôi thấy chúng ngắn gọn hơn. Đây là một lớp để trang trí một lớp:

class Dec(object):

    def __init__(self, msg):
        self.msg = msg

    def __call__(self, klass):
        old_foo = klass.foo
        msg = self.msg
        def decorated_foo(self, *args, **kwargs):
            print('@decorator pre %s' % msg)
            old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)
        klass.foo = decorated_foo
        return klass

Một phiên bản mạnh mẽ hơn để kiểm tra các dấu ngoặc đơn đó và hoạt động nếu các phương thức không tồn tại trên lớp được trang trí:

from inspect import isclass

def decorate_if(condition, decorator):
    return decorator if condition else lambda x: x

def dec(msg):
    # Only use if your decorator's first parameter is never a class
    assert not isclass(msg)

    def decorator(klass):
        old_foo = getattr(klass, 'foo', None)

        @decorate_if(old_foo, wraps(klass.foo))
        def decorated_foo(self, *args ,**kwargs):
            print('@decorator pre %s' % msg)
            if callable(old_foo):
                old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)

        klass.foo = decorated_foo
        return klass

    return decorator

Các assertkiểm tra mà trang trí đã không được sử dụng mà không có dấu ngoặc đơn. Nếu nó có, thì lớp được trang trí được chuyển đến msgtham số của trình trang trí, làm tăng một AssertionError.

@decorate_ifchỉ áp dụng decoratornếu conditionđánh giá True.

Các getattr, callablekiểm tra, và @decorate_ifđược sử dụng để trang trí các không phá vỡ nếu foo()phương pháp không tồn tại trên lớp được trang trí.


4

Thực sự có một triển khai khá tốt của một nhà trang trí lớp ở đây:

https://github.com/agiliq/Django-parsley/blob/master/parsley/decorators.py

Tôi thực sự nghĩ rằng đây là một thực hiện khá thú vị. Bởi vì nó phân lớp lớp mà nó trang trí, nó sẽ hoạt động chính xác như lớp này trong những thứ như isinstanceséc.

Nó có một lợi ích bổ sung: không có gì lạ khi __init__tuyên bố trong Mẫu django tùy chỉnh để thực hiện sửa đổi hoặc bổ sung để self.fieldstốt hơn cho những thay đổi self.fieldsxảy ra sau tất cả__init__ đã chạy cho lớp được đề cập.

Rất thông minh.

Tuy nhiên, trong lớp của bạn, bạn thực sự muốn trang trí thay đổi hàm tạo, điều mà tôi không nghĩ là trường hợp sử dụng tốt cho trình trang trí lớp.


0

Dưới đây là một ví dụ trả lời câu hỏi trả về các tham số của một lớp. Hơn nữa, nó vẫn tôn trọng chuỗi kế thừa, tức là chỉ các tham số của chính lớp được trả về. Hàm get_paramsnày được thêm vào như một ví dụ đơn giản, nhưng các chức năng khác có thể được thêm vào nhờ mô-đun kiểm tra.

import inspect 

class Parent:
    @classmethod
    def get_params(my_class):
        return list(inspect.signature(my_class).parameters.keys())

class OtherParent:
    def __init__(self, a, b, c):
        pass

class Child(Parent, OtherParent):
    def __init__(self, x, y, z):
        pass

print(Child.get_params())
>>['x', 'y', 'z']

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.