วิธีการตกแต่งชั้นเรียน?


129

ใน Python 2.5 มีวิธีสร้างมัณฑนากรที่ตกแต่งคลาสหรือไม่? โดยเฉพาะฉันต้องการใช้มัณฑนากรเพื่อเพิ่มสมาชิกในคลาสและเปลี่ยนตัวสร้างเพื่อรับค่าสำหรับสมาชิกนั้น

ค้นหาสิ่งต่อไปนี้ (ซึ่งมีข้อผิดพลาดทางไวยากรณ์ใน '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()

ฉันเดาว่าสิ่งที่ฉันตามมาคือวิธีทำบางอย่างเช่นอินเตอร์เฟส C # ใน Python ฉันต้องเปลี่ยนกระบวนทัศน์ของฉันฉันคิดว่า

คำตอบ:


80

ฉันจะสองความคิดที่ว่าคุณอาจต้องการพิจารณาคลาสย่อยแทนแนวทางที่คุณระบุไว้ อย่างไรก็ตามไม่ทราบสถานการณ์เฉพาะของคุณ YMMV :-)

สิ่งที่คุณคิดคือเมตาคลาสสิก __new__ฟังก์ชั่นใน metaclass จะผ่านความหมายที่นำเสนอเต็มรูปแบบของการเรียนซึ่งมันก็สามารถเขียนก่อนที่ชั้นจะถูกสร้างขึ้น ในเวลานั้นคุณสามารถย่อยตัวสร้างสำหรับตัวสร้างใหม่ได้ในเวลานั้น

ตัวอย่าง:

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

การเปลี่ยนคอนสตรัคเตอร์อาจเป็นเรื่องที่น่าทึ่ง แต่ภาษานั้นให้การสนับสนุนสำหรับการวิปัสสนาเชิงลึกและการปรับเปลี่ยนแบบไดนามิก


ขอบคุณนั่นคือสิ่งที่ฉันกำลังมองหา คลาสที่สามารถปรับเปลี่ยนคลาสอื่น ๆ จำนวนเท่าใดก็ได้เพื่อให้ทุกคนมีสมาชิกเฉพาะ เหตุผลของฉันที่ไม่มีคลาสที่สืบทอดมาจากคลาส ID ทั่วไปคือฉันต้องการมีคลาสเวอร์ชันที่ไม่ใช่ ID และเวอร์ชัน ID
Robert Gowland

Metaclasses เคยเป็นวิธีที่ใช้ในการทำสิ่งต่างๆเช่นนี้ใน Python2.5 หรือเก่ากว่า แต่ในปัจจุบันคุณสามารถใช้ class decorator ได้บ่อยมาก (ดูคำตอบของ Steven) ซึ่งง่ายกว่ามาก
Jonathan Hartley

203

นอกเหนือจากคำถามว่านักตกแต่งชั้นเรียนเป็นทางออกที่เหมาะสมสำหรับปัญหาของคุณหรือไม่:

ใน Python 2.6 ขึ้นไปมีคลาสมัณฑนากรที่มี @ -syntax ดังนั้นคุณสามารถเขียน:

@addID
class Foo:
    pass

ในเวอร์ชันเก่าคุณสามารถทำได้อีกวิธีหนึ่งดังนี้

class Foo:
    pass

Foo = addID(Foo)

อย่างไรก็ตามโปรดทราบว่าการทำงานนี้เหมือนกับฟังก์ชันมัณฑนากรและมัณฑนากรควรส่งคืนคลาสใหม่ (หรือแก้ไขต้นฉบับ) ซึ่งไม่ใช่สิ่งที่คุณกำลังทำในตัวอย่าง มัณฑนากร addID จะมีลักษณะดังนี้:

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

จากนั้นคุณสามารถใช้ไวยากรณ์ที่เหมาะสมสำหรับเวอร์ชัน Python ของคุณตามที่อธิบายไว้ข้างต้น

แต่ผมเห็นด้วยกับคนอื่น ๆ __init__ที่ได้รับมรดกเป็นความเหมาะสมดีกว่าถ้าคุณต้องการที่จะแทนที่


2
... แม้ว่าคำถามจะกล่าวถึง Python 2.5 โดยเฉพาะ :-)
Jarret Hardie

5
ขออภัยสำหรับความยุ่งเหยิงของบรรทัด แต่ตัวอย่างโค้ดไม่ค่อยดีนักในความคิดเห็น ... : def addID (original_class): original_class .__ orig__init__ = original_class .__ init__ def init __ (self, * args, ** kws): print "decorator" self.id = 9 original_class .__ orig__init __ (self, * args, ** kws) original_class .__ init = init return original_class @addID class Foo: def __init __ (self): พิมพ์ "Foo" a = Foo () พิมพ์ a.id
Gerald Senarclens de Grancy

2
@Steven, @Gerald พูดถูกถ้าคุณสามารถอัปเดตคำตอบของคุณได้มันจะง่ายกว่ามากในการอ่านรหัสของเขา;)
วันที่

3
@ วัน: ขอบคุณสำหรับการแจ้งเตือนฉันไม่เคยสังเกตความคิดเห็นมาก่อน อย่างไรก็ตาม: ฉันได้รับความลึกในการเรียกซ้ำสูงสุดเกินกว่ารหัส Geralds ด้วย ฉันจะพยายามสร้างเวอร์ชันที่ใช้งานได้ ;-)
สตีเวน

1
@ วันและ @ เจอรัลด์: ขออภัยปัญหาไม่ได้อยู่ที่รหัสของเจอรัลด์ฉันทำผิดเพราะรหัสที่สับสนในความคิดเห็น ;-)
สตีเวน

20

ไม่มีใครอธิบายว่าคุณสามารถกำหนดคลาสแบบไดนามิกได้ ดังนั้นคุณสามารถมีมัณฑนากรที่กำหนด (และส่งคืน) คลาสย่อย:

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

ซึ่งสามารถใช้ได้ใน Python 2 (ความคิดเห็นจาก Blckknght ซึ่งอธิบายว่าทำไมคุณควรทำสิ่งนี้ต่อไปใน 2.6+) ดังนี้:

class Foo:
    pass

FooId = addId(Foo)

และใน Python 3 เป็นเช่นนี้ (แต่โปรดใช้ความระมัดระวังsuper()ในชั้นเรียนของคุณ):

@addId
class Foo:
    pass

เพื่อให้คุณมีเค้กและกินได้ - มรดกและนักตกแต่ง!


4
คลาสย่อยในมัณฑนากรเป็นอันตรายใน Python 2.6+ เนื่องจากจะทำให้การsuperโทรในคลาสดั้งเดิมแตก หากFooมีชื่อเมธอดfooที่เรียกsuper(Foo, self).foo()มันจะเรียกคืนไม่สิ้นสุดเนื่องจากชื่อFooถูกผูกไว้กับคลาสย่อยที่มัณฑนากรส่งคืนไม่ใช่คลาสเดิม (ซึ่งไม่สามารถเข้าถึงได้ด้วยชื่อใด ๆ ) ไม่มีอาร์กิวเมนต์ของ Python 3 super()หลีกเลี่ยงปัญหานี้ (ฉันถือว่าผ่านเวทย์มนตร์คอมไพเลอร์เดียวกันที่อนุญาตให้ใช้งานได้เลย) คุณยังสามารถแก้ไขปัญหาได้โดยการตกแต่งคลาสด้วยตนเองภายใต้ชื่ออื่น (ตามที่คุณทำในตัวอย่าง Python 2.5)
Blckknght

1
ฮะ. ขอบคุณฉันไม่รู้ (ฉันใช้ python 3) จะเพิ่มความคิดเห็น
andrew cooke

13

นั่นไม่ใช่แนวทางปฏิบัติที่ดีและไม่มีกลไกใดที่จะทำเช่นนั้นได้ วิธีที่ถูกต้องในการบรรลุสิ่งที่คุณต้องการคือมรดก

ลองดูเข้ามาเป็นเอกสารชั้น

ตัวอย่างเล็กน้อย:

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)

วิธีนี้บอสมีทุกอย่างEmployeeพร้อม__init__วิธีการของตัวเองและสมาชิกของตัวเอง


3
ฉันเดาว่าสิ่งที่ฉันต้องการคือมี Boss ที่ไม่เชื่อเรื่องพระเจ้าของคลาสที่มีอยู่ นั่นคืออาจมีคลาสต่างๆมากมายที่ฉันต้องการใช้คุณสมบัติของบอส ฉันเหลือคลาสที่สืบทอดมาจากบอสเป็นโหลหรือไม่?
Robert Gowland

5
@ Robert Gowland: นั่นเป็นเหตุผลที่ Python มีการสืบทอดหลายรายการ ใช่คุณควรสืบทอดแง่มุมต่างๆจากคลาสผู้ปกครองต่างๆ
ล็อต

7
@ S.Lott: โดยทั่วไปการถ่ายทอดทางพันธุกรรมเป็นความคิดที่ไม่ดีแม้แต่การสืบทอดในระดับที่มากเกินไปก็ไม่ดีเช่นกัน ฉันจะแนะนำให้คุณอยู่ห่างจากมรดกหลาย ๆ
mpeterson

5
mpeterson: การสืบทอดหลายรายการใน python แย่กว่าวิธีนี้หรือไม่? เกิดอะไรขึ้นกับการสืบทอดหลายรายการของ python
Arafangion

2
@Arafangion: โดยทั่วไปแล้วการสืบทอดหลายอย่างถือเป็นสัญญาณเตือนของปัญหา ทำให้มีลำดับชั้นที่ซับซ้อนและความสัมพันธ์ที่ยากต่อการติดตาม หากโดเมนปัญหาของคุณยืมตัวเองได้ดีกับการสืบทอดแบบหลายชั้น (สามารถสร้างแบบจำลองตามลำดับชั้นได้หรือไม่) เป็นทางเลือกที่เป็นธรรมชาติสำหรับหลาย ๆ คน สิ่งนี้ใช้ได้กับทุกภาษาที่อนุญาตให้มีการสืบทอดหลายแบบ
Morten Jensen

6

ฉันยอมรับว่าการสืบทอดเป็นสิ่งที่ดีกว่าสำหรับปัญหาที่เกิดขึ้น

ฉันพบว่าคำถามนี้มีประโยชน์มากสำหรับชั้นเรียนตกแต่งขอบคุณทุกคน

นี่คือตัวอย่างอีกสองสามตัวอย่างจากคำตอบอื่น ๆ รวมถึงการสืบทอดส่งผลกระทบต่อสิ่งต่างๆใน Python 2.7 อย่างไร (และ@wrapsซึ่งคงไว้ซึ่ง docstring ของฟังก์ชันดั้งเดิมเป็นต้น):

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

บ่อยครั้งที่คุณต้องการเพิ่มพารามิเตอร์ให้กับมัณฑนากรของคุณ:

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

ขาออก:

@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

ฉันใช้มัณฑนากรฟังก์ชันแล้วเพราะฉันพบว่ามันกระชับมากขึ้น นี่คือคลาสสำหรับตกแต่งชั้นเรียน:

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

เวอร์ชันที่มีประสิทธิภาพมากขึ้นซึ่งตรวจสอบวงเล็บเหล่านั้นและใช้งานได้หากไม่มีวิธีการในคลาสที่ได้รับการตกแต่ง:

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

assertตรวจสอบว่ามัณฑนากรยังไม่ได้ถูกนำมาใช้โดยไม่ต้องวงเล็บ หากมีคลาสที่กำลังตกแต่งจะถูกส่งผ่านไปยังmsgพารามิเตอร์ของมัณฑนากรซึ่งเพิ่มAssertionErrorไฟล์.

@decorate_ifเพียง แต่นำไปใช้decoratorถ้าประเมินconditionTrue

getattr, callableการทดสอบและการ@decorate_ifถูกนำมาใช้เพื่อให้มัณฑนากรไม่ทำลายถ้าfoo()วิธีการไม่ได้อยู่ในระดับที่ถูกการตกแต่ง


4

มีการใช้งานมัณฑนากรชั้นเรียนที่ค่อนข้างดีที่นี่:

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

ฉันคิดว่านี่เป็นการใช้งานที่น่าสนใจทีเดียว เนื่องจากคลาสย่อยของคลาสที่ตกแต่งมันจะทำงานเหมือนกับคลาสนี้ในสิ่งต่างๆเช่นการisinstanceตรวจสอบ

มีประโยชน์เพิ่มเติม: ไม่ใช่เรื่องแปลกที่__init__คำสั่งในแบบฟอร์ม django ที่กำหนดเองจะทำการปรับเปลี่ยนหรือเพิ่มเติมเพื่อself.fieldsให้การเปลี่ยนแปลงself.fieldsเกิดขึ้นหลังจากทั้งหมด__init__ได้ดำเนินการสำหรับคลาสที่มีปัญหาแล้วจะดีกว่า

ฉลาดมาก.

อย่างไรก็ตามในชั้นเรียนของคุณคุณต้องการให้การตกแต่งเปลี่ยนคอนสตรัคเตอร์ซึ่งฉันไม่คิดว่าเป็นกรณีการใช้งานที่ดีสำหรับมัณฑนากรชั้นเรียน


0

นี่คือตัวอย่างที่ตอบคำถามในการส่งคืนพารามิเตอร์ของคลาส ยิ่งไปกว่านั้นมันยังคงเคารพห่วงโซ่แห่งการสืบทอดกล่าวคือจะส่งคืนเฉพาะพารามิเตอร์ของคลาสเท่านั้น ฟังก์ชันget_paramsนี้ถูกเพิ่มเป็นตัวอย่างง่ายๆ แต่สามารถเพิ่มฟังก์ชันอื่น ๆ ได้ด้วยโมดูลการตรวจสอบ

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']

0

Django มีmethod_decoratorมัณฑนากรที่เปลี่ยนมัณฑนากรใด ๆ ให้เป็นมัณฑนากรวิธีการคุณสามารถดูวิธีการใช้งานได้ในdjango.utils.decorators:

https://github.com/django/django/blob/50cf183d219face91822c75fa0a15fe2fe3cb32d/django/utils/decorators.py#L53

https://docs.djangoproject.com/en/3.0/topics/class-based-views/intro/#decorating-the-class

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.