Можете ли вы обезопасить методы исправления для основных типов в Python? - PullRequest
47 голосов
/ 10 октября 2008

Ruby может добавлять методы в класс Number и другие основные типы для получения таких эффектов:

1.should_equal(1)

Но похоже, что Python не может этого сделать. Это правда? И если так, то почему? Это как-то связано с тем, что type нельзя изменить?

Обновление: вместо того, чтобы говорить о различных определениях исправлений обезьян, я хотел бы просто сосредоточиться на примере выше. Я уже пришел к выводу, что это невозможно сделать, поскольку некоторые из вас ответили. Но я хотел бы получить более подробное объяснение того, почему этого нельзя сделать, и, возможно, какая функция, если она доступна в Python, позволит это сделать.

Чтобы ответить на некоторые из вас: причина, по которой я мог бы захотеть сделать это, заключается просто в эстетике / удобочитаемости.

 item.price.should_equal(19.99)

Это больше похоже на английский и ясно указывает, какое значение является проверенным, а какое - ожидаемым, как предполагается:

should_equal(item.price, 19.99)

Эта концепция является основой Rspec и некоторых других платформ Ruby.

Ответы [ 14 ]

68 голосов
/ 11 октября 2008

Нет, вы не можете. В Python все данные (классы, методы, функции и т. Д.), Определенные в модулях расширения C (включая встроенные), являются неизменяемыми. Это связано с тем, что модули C совместно используются несколькими интерпретаторами в одном и том же процессе, поэтому их извлечение также может повлиять на несвязанные интерпретаторы в одном и том же процессе. (Множество интерпретаторов в одном и том же процессе возможно через C API , и были предприняты некоторые усилия для их использования на уровне Python.)

Однако классы, определенные в коде Python, могут быть зашифрованы, потому что они локальны для этого интерпретатора.

39 голосов
/ 10 октября 2008

Что именно вы подразумеваете под Monkey Patch здесь? Есть несколько несколько разных определений .

Если вы имеете в виду «можете ли вы изменить методы класса во время выполнения?», То ответ решительно да:

class Foo:
  pass # dummy class

Foo.bar = lambda self: 42

x = Foo()
print x.bar()

Если вы имеете в виду, "можете ли вы изменить методы класса во время выполнения и сделать все экземпляры этого класса измененными после факта ?" тогда ответ да тоже. Просто слегка измените порядок:

class Foo:
  pass # dummy class

x = Foo()

Foo.bar = lambda self: 42

print x.bar()

Но вы не можете сделать это для некоторых встроенных классов, таких как int или float. Методы этих классов реализованы в C, и некоторые абстракции приносятся в жертву, чтобы сделать реализацию проще и эффективнее.

Мне не совсем понятно, на , почему вы все равно захотите изменить поведение встроенных числовых классов. Если вам нужно изменить их поведение, подкласс их !!

29 голосов
/ 26 октября 2010
def should_equal_def(self, value):
    if self != value:
        raise ValueError, "%r should equal %r" % (self, value)

class MyPatchedInt(int):
    should_equal=should_equal_def

class MyPatchedStr(str):
    should_equal=should_equal_def

import __builtin__
__builtin__.str = MyPatchedStr
__builtin__.int = MyPatchedInt

int(1).should_equal(1)
str("44").should_equal("44")

Веселись;)

23 голосов
/ 22 июня 2013

Вы можете сделать это, но это займет немного взлома. К счастью, теперь есть модуль под названием «Запретный плод», который дает вам возможность очень просто исправлять методы встроенных типов. Вы можете найти его на

http://clarete.github.io/forbiddenfruit/?goback=.gde_50788_member_228887816

или

https://pypi.python.org/pypi/forbiddenfruit/0.1.0

В исходном примере вопроса после того, как вы напишете функцию should_equal, вы просто сделаете

from forbiddenfruit import curse
curse(int, "should_equal", should_equal)

и ты в порядке! Также есть «обратная» функция для удаления пропатченного метода.

13 голосов
/ 11 октября 2008

Типы ядра Python неизменны, как указывали другие пользователи:

>>> int.frobnicate = lambda self: whatever()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'int'

Вы, конечно, могли бы достичь эффекта, который вы описали, создав подкласс, поскольку пользовательские типы в Python являются изменяемыми по умолчанию.

>>> class MyInt(int):
...   def frobnicate(self):
...     print 'frobnicating %r' % self
... 
>>> five = MyInt(5)
>>> five.frobnicate()
frobnicating 5
>>> five + 8
13

Нет необходимости делать общедоступным подкласс MyInt; с таким же успехом можно определить его встроенный непосредственно в функции или методе, который создает экземпляр.

Конечно, есть несколько ситуаций, когда программисты на Python, свободно владеющие идиомой, считают, что подобный подклассы - правильное решение. Например, os.stat() возвращает подкласс tuple, который добавляет именованные элементы именно для того, чтобы решить проблему читабельности, на которую вы ссылаетесь в своем примере.

>>> import os
>>> st = os.stat('.')
>>> st
(16877, 34996226, 65024L, 69, 1000, 1000, 4096, 1223697425, 1223699268, 1223699268)
>>> st[6]
4096
>>> st.st_size
4096

Тем не менее, в конкретном примере, который вы приводите, я не верю, что подкласс float в item.price (или где-либо еще) с большой вероятностью будет считаться делом Pythonic. Я могу легко представить, что кто-то решит добавить метод price_should_equal() к item, если это был основной вариант использования; если кто-то ищет что-то более общее, возможно, имеет смысл использовать именованные аргументы, чтобы прояснить предполагаемое значение, как в

should_equal(observed=item.price, expected=19.99)

или что-то в этом роде. Это немного многословно, но, без сомнения, его можно улучшить. Возможное преимущество такого подхода перед патчированием обезьян в стиле Ruby заключается в том, что should_equal() может легко выполнить сравнение для любого типа, а не только для int или float. Но, возможно, я слишком увлечен деталями конкретного примера, который вы предоставили.

7 голосов
/ 13 мая 2011

Вы не можете исправлять основные типы в python. Тем не менее, вы могли бы использовать pipe для написания более удобочитаемого кода:

from pipe import *

@Pipe
def should_equal(obj, val):
    if obj==val: return True
    return False

class dummy: pass
item=dummy()
item.value=19.99

print item.value | should_equal(19.99)
4 голосов
/ 08 мая 2009

Вот пример реализации item.price.should_equal, хотя в реальной программе я бы использовал Decimal вместо float:

class Price(float):
    def __init__(self, val=None):
        float.__init__(self)
        if val is not None:
            self = val

    def should_equal(self, val):
        assert self == val, (self, val)

class Item(object):
    def __init__(self, name, price=None):
        self.name = name
        self.price = Price(price)

item = Item("spam", 3.99)
item.price.should_equal(3.99)
3 голосов
/ 10 октября 2008

Если вы действительно действительно хотите сделать патч обезьяны в Python, вы можете сделать (sortof) хак с техникой "import foo as bar".

Если у вас есть такой класс, как TelnetConnection, и вы хотите расширить его, создайте подкласс в отдельном файле и назовите его чем-нибудь вроде TelnetConnectionExtended.

Затем в верхней части кода, где вы обычно говорите:

import TelnetConnection

изменить это на:

import TelnetConnectionExtended as TelnetConnection

и тогда везде в вашем коде, на который вы ссылаетесь, TelnetConnection будет фактически ссылаться на TelnetConnectionExtended.

К сожалению, это предполагает, что у вас есть доступ к этому классу, и «как» работает только в этом конкретном файле (это не глобальное переименование), но я обнаружил, что время от времени это полезно.

2 голосов
/ 10 октября 2008

Нет, но у вас есть UserDict UserString и UserList, которые были созданы именно для этого.

Если вы Google, вы найдете примеры для других типов, но это встроенные.

Как правило, исправления обезьян используются в Python меньше, чем в Ruby.

1 голос
/ 20 декабря 2012

Нет, к сожалению, вы не можете расширять типы, реализованные в C во время выполнения.

Вы можете создать подкласс int, хотя это нетривиально, возможно, вам придется переопределить __new__.

У вас также есть проблема с синтаксисом:

1.somemethod()  # invalid

Однако

(1).__eq__(1)  # valid
...