Làm thế nào để thay đổi động lớp cơ sở của các cá thể trong thời gian chạy?


77

Bài viết này có một đoạn mã hiển thị cách sử dụng __bases__để thay đổi động phân cấp kế thừa của một số mã Python, bằng cách thêm một lớp vào tập hợp các lớp hiện có của các lớp mà từ đó nó kế thừa. Ok, điều đó khó đọc, mã có lẽ rõ ràng hơn:

class Friendly:
    def hello(self):
        print 'Hello'

class Person: pass

p = Person()
Person.__bases__ = (Friendly,)
p.hello()  # prints "Hello"

Có nghĩa là, Personkhông kế thừa từ Friendlycấp nguồn, mà quan hệ kế thừa này được thêm động vào thời gian chạy bằng cách sửa đổi __bases__thuộc tính của lớp Người. Tuy nhiên, nếu bạn thay đổi FriendlyPersontrở thành các lớp kiểu mới (bằng cách kế thừa từ đối tượng), bạn sẽ gặp lỗi sau:

TypeError: __bases__ assignment: 'Friendly' deallocator differs from 'object'

Một chút của Google về điều này dường như chỉ ra một số điểm không tương thích giữa các lớp kiểu mới và kiểu cũ liên quan đến việc thay đổi hệ thống phân cấp kế thừa trong thời gian chạy. Cụ thể: "Các đối tượng lớp kiểu mới không hỗ trợ gán cho thuộc tính base của chúng " .

Câu hỏi của tôi, liệu có thể làm cho ví dụ Thân thiện / Người ở trên hoạt động bằng cách sử dụng các lớp kiểu mới trong Python 2.7+, có thể bằng cách sử dụng __mro__ thuộc tính không?

Tuyên bố từ chối trách nhiệm: Tôi hoàn toàn nhận ra rằng đây là mã tối nghĩa. Tôi hoàn toàn nhận thấy rằng trong các thủ thuật mã sản xuất thực tế như thế này có xu hướng không thể đọc được, đây hoàn toàn là một thử nghiệm suy nghĩ và đối với những người đam mê tìm hiểu điều gì đó về cách Python giải quyết các vấn đề liên quan đến đa kế thừa.


Nó cũng tốt đẹp cho người học để đọc này nếu họ không quen thuộc với metaclass, gõ (), ...: slideshare.net/gwiener/metaclasses-in-python :)
hkoosha

Đây là trường hợp sử dụng của tôi. Tôi đang nhập khẩu một thư viện mà có lớp B kế thừa từ lớp A.
FutureNerd

2
Đây là trường hợp sử dụng thực tế của tôi. Tôi đang nhập một thư viện có lớp B kế thừa từ lớp A. Tôi muốn tạo New_A kế thừa từ A, với new_A_method (). Bây giờ tôi muốn tạo New_B kế thừa từ ... à, từ B như thể B kế thừa từ New_A, để các phương thức của B, phương thức của A và new_A_method () đều có sẵn cho các bản sao của New_B. Làm thế nào tôi có thể làm điều này mà không cần vá lỗi lớp A hiện có?
FutureNerd

Bạn không thể có New_Bthừa kế từ cả hai BNew_A? Hãy nhớ Python hỗ trợ đa kế thừa.
Adam Parkin

2
Sau một chút xử lý trên googling, báo cáo lỗi python sau có vẻ có liên quan ... bug.python.org/issue672115
mgilson

Câu trả lời:


39

Ok, một lần nữa, đây không phải là điều bạn thường làm, điều này chỉ dành cho mục đích thông tin.

Nơi Python vẻ cho một phương thức trên một đối tượng dụ được xác định bởi các __mro__thuộc tính của lớp trong đó xác định rằng đối tượng ( M ethod R esolution O rder thuộc tính). Vì vậy, nếu chúng ta có thể thay đổi __mro__các Person, chúng tôi sẽ nhận được các hành vi mong muốn. Cái gì đó như:

setattr(Person, '__mro__', (Person, Friendly, object))

Vấn đề là đó __mro__là một thuộc tính chỉ đọc, và do đó setattr sẽ không hoạt động. Có thể nếu bạn là một guru Python thì có một cách để giải quyết vấn đề đó, nhưng rõ ràng là tôi không có tư cách là guru vì tôi không thể nghĩ ra.

Một giải pháp khả thi là chỉ cần xác định lại lớp:

def modify_Person_to_be_friendly():
    # so that we're modifying the global identifier 'Person'
    global Person

    # now just redefine the class using type(), specifying that the new
    # class should inherit from Friendly and have all attributes from
    # our old Person class
    Person = type('Person', (Friendly,), dict(Person.__dict__)) 

def main():
    modify_Person_to_be_friendly()
    p = Person()
    p.hello()  # works!

Điều này không làm là sửa đổi bất kỳ Persontrường hợp nào đã tạo trước đó để có hello()phương thức. Ví dụ (chỉ sửa đổi main()):

def main():
    oldperson = Person()
    ModifyPersonToBeFriendly()
    p = Person()
    p.hello()  
    # works!  But:
    oldperson.hello()
    # does not

Nếu chi tiết của typecuộc gọi không rõ ràng, thì hãy đọc câu trả lời tuyệt vời của e-thoả mãn về 'Metaclass trong Python là gì?' .


-1: tại sao buộc lớp mới kế thừa từ Frielndly chỉ khi bạn có thể bảo toàn tốt `__mro__` ban đầu bằng cách gọi: type('Person', (Friendly) + Person.__mro__, dict(Person.__dict__)) (và tốt hơn, hãy thêm các biện pháp bảo vệ để Thân thiện không kết thúc hai lần trong đó.) các vấn đề khác ở đây - như đối với nơi lớp "Person" thực sự được xác định và sử dụng: chức năng của bạn chỉ thay đổi nó trên mô-đun hiện tại - các mô-đun khác đang chạy Person sẽ không hoàn hảo - bạn nên thực hiện một bản khóa khỉ trên mô-đun Person được xác định. (và thậm chí có những vấn đề)
jsbueno

10
-1Bạn đã hoàn toàn bỏ lỡ lý do cho ngoại lệ ngay từ đầu. Bạn có thể vui vẻ sửa đổi Person.__class__.__bases__trong Python 2 và 3, miễn là Personkhông kế thừa từ object trực tiếp . Xem câu trả lời của akaRem và Sam Gulve bên dưới. Cách giải quyết này chỉ giải quyết sự hiểu lầm của chính bạn về vấn đề.
Carl Smith

2
Đây là một "giải pháp" rất lỗi. Trong số các vấn đề khác với mã này, nó phá vỡ __dict__thuộc tính của các đối tượng kết quả .
user2357112 hỗ trợ Monica

24

Tôi cũng đang đấu tranh với điều này và bị hấp dẫn bởi giải pháp của bạn, nhưng Python 3 đã loại bỏ chúng tôi:

AttributeError: attribute '__dict__' of 'type' objects is not writable

Tôi thực sự có nhu cầu chính đáng về một trình trang trí thay thế lớp cha (đơn) của lớp được trang trí. Nó sẽ yêu cầu mô tả quá dài để đưa vào đây (tôi đã thử, nhưng không thể làm cho nó có độ dài hợp lý và độ phức tạp hạn chế - nó được đưa ra trong bối cảnh được sử dụng bởi nhiều ứng dụng Python của máy chủ doanh nghiệp dựa trên Python nơi các ứng dụng khác nhau cần các biến thể hơi khác nhau của một số mã.)

Cuộc thảo luận trên trang này và những trang khác giống như nó đã cung cấp gợi ý rằng vấn đề gán cho __bases__chỉ xảy ra đối với các lớp không có lớp cha được định nghĩa (tức là lớp cha duy nhất của nó là đối tượng). Tôi đã có thể giải quyết vấn đề này (cho cả Python 2.7 và 3.2) bằng cách xác định các lớp có lớp cha mà tôi cần thay thế là lớp con của một lớp tầm thường:

## T is used so that the other classes are not direct subclasses of object,
## since classes whose base is object don't allow assignment to their __bases__ attribute.

class T: pass

class A(T):
    def __init__(self):
        print('Creating instance of {}'.format(self.__class__.__name__))

## ordinary inheritance
class B(A): pass

## dynamically specified inheritance
class C(T): pass

A()                 # -> Creating instance of A
B()                 # -> Creating instance of B
C.__bases__ = (A,)
C()                 # -> Creating instance of C

## attempt at dynamically specified inheritance starting with a direct subclass
## of object doesn't work
class D: pass

D.__bases__ = (A,)
D()

## Result is:
##     TypeError: __bases__ assignment: 'A' deallocator differs from 'object'

6

Tôi không thể đảm bảo về hậu quả, nhưng mã này thực hiện những gì bạn muốn tại py2.7.2.

class Friendly(object):
    def hello(self):
        print 'Hello'

class Person(object): pass

# we can't change the original classes, so we replace them
class newFriendly: pass
newFriendly.__dict__ = dict(Friendly.__dict__)
Friendly = newFriendly
class newPerson: pass
newPerson.__dict__ = dict(Person.__dict__)
Person = newPerson

p = Person()
Person.__bases__ = (Friendly,)
p.hello()  # prints "Hello"

Chúng tôi biết rằng điều này là có thể. Mát mẻ. Nhưng chúng tôi sẽ không bao giờ sử dụng nó!


3

Đúng vậy, tất cả các cảnh báo về việc xáo trộn hệ thống phân cấp lớp một cách linh hoạt đang có hiệu lực.

Nhưng nếu nó phải được thực hiện thì, rõ ràng, có một cuộc tấn công đã xảy ra xung quanh các "deallocator differs from 'object" issue when modifying the __bases__ attributelớp kiểu mới.

Bạn có thể xác định một đối tượng lớp

class Object(object): pass

Điều này dẫn xuất một lớp từ siêu kính tích hợp type. Vậy là xong, bây giờ các lớp kiểu mới của bạn có thể sửa đổi __bases__mà không gặp vấn đề gì.

Trong các thử nghiệm của tôi, điều này thực sự hoạt động rất tốt vì tất cả các trường hợp hiện có (trước khi thay đổi kế thừa) của nó và các lớp dẫn xuất của nó đều cảm thấy tác động của sự thay đổi bao gồm cả việc chúng mrođược cập nhật.


2

Tôi cần một giải pháp cho điều này:

  • Hoạt động với cả Python 2 (> = 2.7) và Python 3 (> = 3.2).
  • Cho phép các cơ sở lớp được thay đổi sau khi nhập động một phụ thuộc.
  • Cho phép các cơ sở lớp được thay đổi từ mã kiểm tra đơn vị.
  • Hoạt động với các loại có siêu kính tùy chỉnh.
  • Vẫn cho phép unittest.mock.patchhoạt động như mong đợi.

Đây là những gì tôi nghĩ ra:

def ensure_class_bases_begin_with(namespace, class_name, base_class):
    """ Ensure the named class's bases start with the base class.

        :param namespace: The namespace containing the class name.
        :param class_name: The name of the class to alter.
        :param base_class: The type to be the first base class for the
            newly created type.
        :return: ``None``.

        Call this function after ensuring `base_class` is
        available, before using the class named by `class_name`.

        """
    existing_class = namespace[class_name]
    assert isinstance(existing_class, type)

    bases = list(existing_class.__bases__)
    if base_class is bases[0]:
        # Already bound to a type with the right bases.
        return
    bases.insert(0, base_class)

    new_class_namespace = existing_class.__dict__.copy()
    # Type creation will assign the correct ‘__dict__’ attribute.
    del new_class_namespace['__dict__']

    metaclass = existing_class.__metaclass__
    new_class = metaclass(class_name, tuple(bases), new_class_namespace)

    namespace[class_name] = new_class

Được sử dụng như thế này trong ứng dụng:

# foo.py

# Type `Bar` is not available at first, so can't inherit from it yet.
class Foo(object):
    __metaclass__ = type

    def __init__(self):
        self.frob = "spam"

    def __unicode__(self): return "Foo"

# … later …
import bar
ensure_class_bases_begin_with(
        namespace=globals(),
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)

Sử dụng như thế này từ trong mã kiểm tra đơn vị:

# test_foo.py

""" Unit test for `foo` module. """

import unittest
import mock

import foo
import bar

ensure_class_bases_begin_with(
        namespace=foo.__dict__,
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)


class Foo_TestCase(unittest.TestCase):
    """ Test cases for `Foo` class. """

    def setUp(self):
        patcher_unicode = mock.patch.object(
                foo.Foo, '__unicode__')
        patcher_unicode.start()
        self.addCleanup(patcher_unicode.stop)

        self.test_instance = foo.Foo()

        patcher_frob = mock.patch.object(
                self.test_instance, 'frob')
        patcher_frob.start()
        self.addCleanup(patcher_frob.stop)

    def test_instantiate(self):
        """ Should create an instance of `Foo`. """
        instance = foo.Foo()

0

Các câu trả lời trên là tốt nếu bạn cần thay đổi một lớp hiện có trong thời gian chạy. Tuy nhiên, nếu bạn chỉ muốn tạo một lớp mới kế thừa bởi một số lớp khác, thì có một giải pháp gọn gàng hơn nhiều. Tôi lấy ý tưởng này từ https://stackoverflow.com/a/21060094/3533440 , nhưng tôi nghĩ ví dụ dưới đây minh họa rõ hơn một trường hợp sử dụng hợp pháp.

def make_default(Map, default_default=None):
    """Returns a class which behaves identically to the given
    Map class, except it gives a default value for unknown keys."""
    class DefaultMap(Map):
        def __init__(self, default=default_default, **kwargs):
            self._default = default
            super().__init__(**kwargs)

        def __missing__(self, key):
            return self._default

    return DefaultMap

DefaultDict = make_default(dict, default_default='wug')

d = DefaultDict(a=1, b=2)
assert d['a'] is 1
assert d['b'] is 2
assert d['c'] is 'wug'

Hãy sửa cho tôi nếu tôi sai, nhưng chiến lược này có vẻ rất dễ đọc đối với tôi và tôi sẽ sử dụng nó trong mã sản xuất. Điều này rất giống với functors trong OCaml.


1
Không thực sự chắc chắn điều này có liên quan gì đến câu hỏi vì câu hỏi thực sự là về việc thay đổi động các lớp cơ sở trong thời gian chạy. Trong mọi trường hợp, lợi thế của điều này là gì so với việc chỉ kế thừa từ Mapcác phương thức trực tiếp và ghi đè khi cần thiết (giống như những gì tiêu chuẩn collections.defaultdictlàm)? Vì nó make_defaultchỉ có thể trả về một loại thứ, vậy tại sao không chỉ tạo DefaultMapmã định danh cấp cao nhất thay vì phải gọi make_defaultđể yêu cầu lớp khởi tạo?
Adam Parkin

Cảm ơn vì bạn đã phản hồi! (1) Tôi đã hạ cánh ở đây trong khi cố gắng thực hiện kế thừa động, vì vậy tôi nghĩ rằng tôi có thể giúp một người nào đó theo cùng con đường của tôi. (2) Tôi tin rằng người ta có thể sử dụng make_defaultđể tạo phiên bản mặc định của một số loại lớp giống từ điển khác ( Map). Bạn không thể kế thừa từ Maptrực tiếp vì Mapkhông được xác định cho đến khi chạy. Trong trường hợp này, bạn sẽ muốn kế thừa từ dicttrực tiếp, nhưng chúng tôi tưởng tượng rằng có thể có trường hợp bạn không biết lớp giống như từ điển nào sẽ kế thừa cho đến khi chạy.
fredcallaway
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.