Jump to content
marcelsema

Correctly let python free a result

Recommended Posts

Hello everyone,

I am looking to destroy an object once it is no longer referenced in python.


I am struggeling to correctly do this.
 

unit Unit1;



interface

uses
  SysUtils, Classes,
  Windows, Messages, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, ComCtrls,
  PythonEngine, Vcl.PythonGUIInputOutput, WrapDelphi;

type
  TForm1 = class(TForm)
    Splitter1: TSplitter;
    Memo1: TMemo;
    PythonEngine1: TPythonEngine;
    PythonModule1: TPythonModule;
    Panel1: TPanel;
    Button1: TButton;
    PythonGUIInputOutput1: TPythonGUIInputOutput;
    Memo2: TMemo;
    PyDelphiWrapper1: TPyDelphiWrapper;


    procedure Button1Click(Sender: TObject);
  private
  public
    destructor Destroy; override;
    function GetDelphiWrapper: TPyDelphiWrapper;
    function GetPythonEngine: TPythonEngine;
  end;

  TEntry = class(TObject)
  strict private
    FstrName: string;
    FclsStringList: TStringList;
  public
    constructor Create(strName: string);
    destructor Destory;
    function Name: string;
  end;

  TManager = class(TObject)
  public
    function GetNewEntry: PPyObject;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}



procedure TForm1.Button1Click(Sender: TObject);
var
  clsManager: TManager;
  pyManager: PPyObject;
begin
  clsManager := TManager.Create;
  pyManager := PyDelphiWrapper1.Wrap(clsManager, soReference);
  PythonModule1.SetVar( 'manager', pyManager );
  PythonEngine1.ExecStrings( memo1.Lines );
  PythonEngine1.Py_DecRef(pyManager);
end;


{ TManager }

function TManager.GetNewEntry: PPyObject;
begin
  // I want TEntry to be destroyed by python once it is no longer used
  Result := Form1.GetDelphiWrapper.Wrap(TEntry.Create('Test'), soOwned);
  Form1.GetPythonEngine.Py_DECREF(Result);
end;

destructor TForm1.Destroy;
begin
  FreeAndNil(PythonModule1);
  FreeAndNil(PythonEngine1);
   inherited;
end;


function TForm1.GetDelphiWrapper: TPyDelphiWrapper;
begin
  Result := PyDelphiWrapper1;
end;

function TForm1.GetPythonEngine: TPythonEngine;
begin
  Result := PythonEngine1;
end;

{ TEntry }

constructor TEntry.Create(strName: string);
begin
  inherited Create;
  FclsStringList := TStringList.Create;
  FstrName := strName;
end;

destructor TEntry.Destory;
begin
  FreeAndNil(FclsStringList);
  inherited;
end;

function TEntry.Name: string;
begin
  Result := FstrName;
end;

end.


 

from spam import *

entry = manager.GetNewEntry()
print(entry.Name())

 

 

After clicking the button, I get an exception accessing an invalid pointer.
 

I also tried returning the object directly instead of wraping it to a PPyObject, but that did not work.

 

Thank you for your help.

Best regards,

Marcel

Share this post


Link to post
function TManager.GetNewEntry: PPyObject; begin
  // I want TEntry to be destroyed by python once it is no longer used
  Result := Form1.GetDelphiWrapper.Wrap(TEntry.Create('Test'), soOwned);
  Form1.GetPythonEngine.Py_DECREF(Result);
end;

In the code above Py_DECREF will destroy the TEntry instance immediately,   You can confirm that by debugging and putting a brekpoint at TEntry.Destroy.
Just remove the Py_DECREF statement.

Explanation.: Wrap returns a python object with reference count of 1.  When you decrease the reference count, it goes down to 0, and because the wrapped Delphi object is owned it gets destroyed 
 

The difference with

clsManager := TManager.Create;
pyManager := PyDelphiWrapper1.Wrap(clsManager, soReference);
PythonModule1.SetVar( 'manager', pyManager );
PythonEngine1.ExecStrings( memo1.Lines );
PythonEngine1.Py_DecRef(pyManager);

is that you call SetVar which increases the reference count.  However the above code leaks clsManager.   It does not get destroyed because you  set the Ownership to soReference.    Either change that or call clsManager.Free at the end.

 

Conclusion:   Python reference counting can be tricky. You should try to understand the rules behind reference counting. (e.g. Reference Counting in Python (tripod.com)) if you use the Python API directly.   High level P4D modules (WrapDelphi and VarPython) to a large extent insulate you from that and ensure correct use of reference counting.

Edited by pyscripter

Share this post


Link to post

Hey,

Thank you for your answer.

 

The problem why I included  Form1.GetPythonEngine.Py_DECREF(Result); is that TEntry.Destory is otherwise never called and I just want TEntry destroyed after I executed the script.

 

You are right, clsManager gets leaked.

 

I assume that I have to look into DEMO32 for the correct way to use the WrapDelphi ?

Best regards,

Marcel

Share this post


Link to post
3 hours ago, marcelsema said:

TEntry.Destory is otherwise never called

It will be called if you change your python script to:

entry = manager.GetNewEntry()
print(entry.Name())
entry = None  

 

Edited by pyscripter

Share this post


Link to post

Interesting. I am unable to get this to work.

I adjusted the python script, removed the Py_DECREF and no destructor is called.

 

Is it also possible to clean up variables after the ExecStrings?

unit Unit1;



interface

uses
  SysUtils, Classes,
  Windows, Messages, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, ComCtrls,
  PythonEngine, Vcl.PythonGUIInputOutput, WrapDelphi;

type
  TForm1 = class(TForm)
    Splitter1: TSplitter;
    Memo1: TMemo;
    PythonEngine1: TPythonEngine;
    PythonModule1: TPythonModule;
    Panel1: TPanel;
    Button1: TButton;
    PythonGUIInputOutput1: TPythonGUIInputOutput;
    Memo2: TMemo;
    PyDelphiWrapper1: TPyDelphiWrapper;


    procedure Button1Click(Sender: TObject);
  private
  public
    destructor Destroy; override;
    function GetDelphiWrapper: TPyDelphiWrapper;
    function GetPythonEngine: TPythonEngine;
  end;

  TEntry = class(TObject)
  strict private
    FstrName: string;
    FclsStringList: TStringList;
  public
    constructor Create(strName: string);
    destructor Destory;
    function Name: string;
  end;

  TManager = class(TObject)
  public
    function GetNewEntry: PPyObject;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}



procedure TForm1.Button1Click(Sender: TObject);
var
  clsManager: TManager;
  pyManager: PPyObject;
begin
  try
    clsManager := TManager.Create;
    pyManager := PyDelphiWrapper1.Wrap(clsManager, soReference);
    PythonModule1.SetVar( 'manager', pyManager );
    PythonEngine1.ExecStrings( memo1.Lines );
    PythonEngine1.Py_DecRef(pyManager);
  finally
    FreeAndNil(clsManager);
  end;
end;


{ TManager }

function TManager.GetNewEntry: PPyObject;
begin
  // I want TEntry to be destroyed by python once it is no longer used
  Result := Form1.GetDelphiWrapper.Wrap(TEntry.Create('Test'), soOwned);
  //Form1.GetPythonEngine.Py_DECREF(Result);
end;

destructor TForm1.Destroy;
begin
  FreeAndNil(PythonModule1);
  FreeAndNil(PythonEngine1);
   inherited;
end;


function TForm1.GetDelphiWrapper: TPyDelphiWrapper;
begin
  Result := PyDelphiWrapper1;
end;

function TForm1.GetPythonEngine: TPythonEngine;
begin
  Result := PythonEngine1;
end;

{ TEntry }

constructor TEntry.Create(strName: string);
begin
  inherited Create;
  FclsStringList := TStringList.Create;
  FstrName := strName;
end;

destructor TEntry.Destory;
begin
  FreeAndNil(FclsStringList);
  inherited;
end;

function TEntry.Name: string;
begin
  Result := FstrName;
end;

end.

 

object Form1: TForm1
  Left = 214
  Top = 174
  Width = 592
  Height = 422
  VertScrollBar.Range = 210
  ActiveControl = Memo1
  Caption = 'Form1'
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = 11
  Font.Name = 'MS Sans Serif'
  Font.Pitch = fpVariable
  Font.Style = []
  TextHeight = 13
  object Splitter1: TSplitter
    Left = 0
    Top = 113
    Width = 576
    Height = 3
    Cursor = crVSplit
    Align = alTop
    ExplicitTop = 169
  end
  object Memo1: TMemo
    Left = 0
    Top = 116
    Width = 576
    Height = 226
    Align = alClient
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -13
    Font.Name = 'Consolas'
    Font.Pitch = fpVariable
    Font.Style = []
    Lines.Strings = (
      'from spam import *'
      ''
      'entry = manager.GetNewEntry()'
      'print(entry.Name())'
      'entry = None')
    ParentFont = False
    ScrollBars = ssVertical
    TabOrder = 0
    WordWrap = False
    ExplicitWidth = 570
    ExplicitHeight = 209
  end
  object Panel1: TPanel
    Left = 0
    Top = 342
    Width = 576
    Height = 41
    Align = alBottom
    BevelOuter = bvNone
    TabOrder = 1
    ExplicitTop = 325
    ExplicitWidth = 570
    object Button1: TButton
      Left = 8
      Top = 8
      Width = 75
      Height = 25
      Caption = 'Execute'
      TabOrder = 0
      OnClick = Button1Click
    end
  end
  object Memo2: TMemo
    Left = 0
    Top = 0
    Width = 576
    Height = 113
    Align = alTop
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -13
    Font.Name = 'Consolas'
    Font.Pitch = fpVariable
    Font.Style = []
    ParentFont = False
    TabOrder = 2
    ExplicitWidth = 570
  end
  object PythonEngine1: TPythonEngine
    DllPath = 'D:\temp\PythonPkg'
    IO = PythonGUIInputOutput1
    Left = 48
    Top = 24
  end
  object PythonModule1: TPythonModule
    Engine = PythonEngine1
    ModuleName = 'spam'
    Errors = <
      item
        Name = 'PointError'
        ErrorType = etClass
      end
      item
        Name = 'EBadPoint'
        ErrorType = etClass
        ParentClass.Name = 'PointError'
      end>
    Left = 152
    Top = 24
  end
  object PythonGUIInputOutput1: TPythonGUIInputOutput
    UnicodeIO = True
    RawOutput = False
    Output = Memo2
    Left = 376
    Top = 24
  end
  object PyDelphiWrapper1: TPyDelphiWrapper
    Engine = PythonEngine1
    Module = PythonModule1
    Left = 248
    Top = 24
  end
end

 

Edited by marcelsema

Share this post


Link to post
36 minutes ago, marcelsema said:

Is it also possible to clean up variables after the ExecStrings?

Please search this forum for answers/discussion of this question.

Share this post


Link to post
  TEntry = class(TObject)
  strict private
    FstrName: string;
    FclsStringList: TStringList;
  public
    constructor Create(strName: string);
    destructor Destory;
    function Name: string;
  end;

 

Destroy misspelled and not declared with override.  If you fix this TEntry gets destroyed.

 

But the better way is:

 

function TManager.GetNewEntry: TEntry;
begin
  Result := TEntry.Create('Test');
end;

and in python

 

from spam import *

entry = manager.GetNewEntry()
print(entry.Name())
entry.__owned__ = True
entry = None


Explanation:  When P4D wraps the result of GetNewEntry it does not know who owns the return value.  The default is that Delphi does.   But the ownership can be changed in python using the __owned__ property.

Edited by pyscripter
  • Like 1

Share this post


Link to post
3 minutes ago, marcelsema said:

Not really. 

 

You can use the following ExecStrings overload

    procedure  ExecStrings(strings: TStrings; locals, globals: PPyObject; const FileName: string = '<string>'); overload;

providing your own globals and locals so that you do not mess up the python namespace.

 

Please search in this forum.   There was a discussion about this.

Share this post


Link to post

For the benefit of anyone reading this thread,  the most powerful way to allow your python script to create instances of a Delphi class (e.g. TEntry)  is to create a wrapper:

 

type
  TEntryClassWrapper = class(TPyClassWrapper<TEntry>)
    constructor CreateWith(APythonType: TPythonType; args, kwds: PPyObject); overload; override;
  end;

{ TEntryClassWrapper }

constructor TEntryClassWrapper.CreateWith(APythonType: TPythonType; args,
  kwds: PPyObject);
var
  _obj : PPyObject;
begin
  if GetPythonEngine.PyArg_ParseTuple( args, 'O:CreteWith',@_obj ) <> 0 then
  begin
    Create(APythonType);
    DelphiObject := TEntry.Create(GetPythonEngine.PyObjectAsString(_obj));
  end;
end;

You need to register and initialize the wrapper in your Form.Create:

 

procedure TForm1.FormCreate(Sender: TObject);
begin
  PyDelphiWrapper1.RegisterDelphiWrapper(TEntryClassWrapper).Initialize;
end;

and then in python:

from spam import *

entry = Entry('Test')
print(entry.Name())
entry = None

The Delphi TEntry class is exposed as the python type Entry in your python code.

Edited by pyscripter
  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×